RTMP握手协议
作者原创,转载请联系作者
RTMP简介
Real Time Messaging Protocol(实时消息传送协议协议)是Adobe Systems公司为Flash Player和服务器之间音频,视频和数据传输开发的私有协议,adobe目前提供了一个并不完整的rtmp specification给大众使用,所以在使用rtmp协议时需要按flash player返回的包进行解析.
目前rtmp有以下几个变种:
- rtmp是工作在TCP之上的明文协议,默认使用1935端口
- rtmps是rtmp使用TLS/SSL连接
- rtmpe是adobe使用自己的加密机制对rtmp进行加密的,虽然加密机制是使用了行业标准,并且内部实现也是专有的,但rtmpe设计基本上错误的,它本身也不提供任何的安全性.
- rtmpt是对rtmp协议提供了一个http的封装,主要是为了防止防火墙对其进行拦截.
包结构
rtmp消息包使用的是二进制数据流,它们使用AMF0/AMF3进行编码.与其它协议一样,rtmp消息也是也包括消息头与消息体,而消息头又可以分为basic header,chunk header,timestamp.
- basic header是此包的唯一不变的部分,并且由一个独立的byte构成,这其中包括了2个作重要的标志位
- chunk type以及stream id.chunk type决定了消息头的编码格式,该字段的长度完全依赖于stream id,stream id是一个可变长的字段.
- message header该字段包含了将要发送的消息的信息(或者是一部分,一个消息拆成多个chunk的情况下是一部分)该字段的长度由chunk basic header中的trunk type决定.
- timestamp扩展时间戳就比较好理解的,就是当chunk message header的时间戳大于等于0xffffff的时候chunk message header后面的四个字节就代表扩展时间.
握手协议
在rtmp连接建立后,服务端与客户端需要通过3次交换报文完成握手.
握手其他的协议不同,是由三个静态大小的块,而不是可变大小的块组成的,客户端与服务器发送相同的三个chunk,客户端发送c0,c1,c2 chunk,服务端发送s0,s1,s2 chunk.
-发送顺序
握手开始时,客户端将发送c0,c1 chunk,此时客户端必须等待,直到收到s1 chunk,才能发送c2 chunk.
此时服务端必须等待,直到已收到c0后才能发送s0和s1,当然也可能会等到接收c1后才发送.
当服务器收到c2后才能再发送的其他数据,同理,当客户端收到s2后才能发送其它数据.
- 握手包格式
-
c0与s0格式
c0和s0包是一个1字节,可以看作是一个byte
目前rtmp版本定义为3,0-2是早期的专利产品所使用的值,现已经废弃,4-31是预留值,32-255是禁用值(这样做是为了区分基于文本的协议,因为这些协议通常都是以一个可打印的字符开始),如果服务端不能识别客户请求的版本,那么它应该发送3的响应,客户端这时可以选择下降到版本3,也可以放弃这次握手. -
c1与s1格式
c1与s1长度为1536个字节,它们由以下字段组成
时间戳:该字段占4字节,包含了一个时间戳,它是所有从这个端点发送出去的将来数据块的起始点,它可以是零,或是任意值,为了同步多个数据块流,端点可能会将这个字段设成其它数据块流时间戳的当前值.
0:此标记位占4字节,并且必须是0
随机数:该字段占1528字节,可以是任意值,因为每个端点必须区分已经初始化的握手和对等端点初始化的握手的响应,所以这个数据要足够的随机,当然这个也不需要密码级的随机或是动态值. -
c2与s2格式
c2和s2包长都是1536字节,几乎是s1和c1的回显.
time1:该字段占4字节,包含有对方发送过来s1或c1的时间戳
time2:该字段占4字节,包含有对方发送过来的前一个包(s1或者c1)的时间戳
随机数回显:该字段占1528字节,包含有对方发送过来的随机数据字段,每个通信端点可以使用time和time2字段,以及当前的时间戳,来快速估计带宽和/或连接延时,但这个数值基本上没法用.
-
握手状态
1)未初始化:在这个阶段,协议版本被发送,客户和服务端都是未初始化的,客户端在包c1中发送协议版本,如果服务端支持这个版本,它将会发送s0和s1作为响应,如果不支持,则服务端会用相应的动作来响应,在RTMP中这个动作是结束这个连接.
- 版本发送完成:客户端和服务端在未初始化状态之后都进入到版本发送完成状态,客户端等待包s1,而服务端等待包c1,在收到相应的包后,客户端发送包c2,而服务端发磅包s2,状态变成询问发送完成.
- 询问发送完成:客户端和服务端等待s2和c2.
- 握手完成:客户端和服务端开始交换消息.
代码示例
-
注册回调,设置初始态,等待接受客户端握手消息
void ngx_rtmp_handshake(ngx_rtmp_session_t *s)
{
ngx_connection_t *c;
c = s->connection;
c->read->handler = ngx_rtmp_handshake_recv;
c->write->handler = ngx_rtmp_handshake_send;
s->hs_buf = ngx_rtmp_alloc_handshake_buffer(s);
s->hs_stage = NGX_RTMP_HANDSHAKE_SERVER_RECV_CHALLENGE;
ngx_rtmp_handshake_recv(c->read);
} -
收到握手消息处理
++s->hs_stage;
switch (s->hs_stage) {
case NGX_RTMP_HANDSHAKE_SERVER_SEND_CHALLENGE:
if (ngx_rtmp_handshake_parse_challenge(s,
&ngx_rtmp_client_partial_key,
&ngx_rtmp_server_full_key) != NGX_OK)
{
ngx_rtmp_finalize_session(s);
return;
}
if (s->hs_old) {
s->hs_buf->pos = s->hs_buf->start;
s->hs_buf->last = s->hs_buf->end;
} else if (ngx_rtmp_handshake_create_challenge(s,
ngx_rtmp_server_version,
&ngx_rtmp_server_partial_key) != NGX_OK)
{
ngx_rtmp_finalize_session(s);
return;
}
ngx_rtmp_handshake_send(c->write);
break;case NGX_RTMP_HANDSHAKE_SERVER_DONE: ngx_rtmp_handshake_done(s); break; case NGX_RTMP_HANDSHAKE_CLIENT_RECV_RESPONSE: if (ngx_rtmp_handshake_parse_challenge(s, &ngx_rtmp_server_partial_key, &ngx_rtmp_client_full_key) != NGX_OK) { ngx_rtmp_finalize_session(s); return; } s->hs_buf->pos = s->hs_buf->last = s->hs_buf->start + 1; ngx_rtmp_handshake_recv(c->read); break; case NGX_RTMP_HANDSHAKE_CLIENT_SEND_RESPONSE: if (ngx_rtmp_handshake_create_response(s) != NGX_OK) { ngx_rtmp_finalize_session(s); return; } ngx_rtmp_handshake_send(c->write); break; }
-
握手完成处理
static void ngx_rtmp_handshake_done(ngx_rtmp_session_t *s)
{
ngx_rtmp_free_handshake_buffers(s);
if (ngx_rtmp_fire_event(s, NGX_RTMP_HANDSHAKE_DONE,
NULL, NULL) != NGX_OK)
{
ngx_rtmp_finalize_session(s);
return;
}
ngx_rtmp_cycle(s);
}
其中处理NGX_RTMP_HANDSHAKE_DONE的回调如前文所ngx_rtmp_relay_module模块中注册的回调:ngx_rtmp_relay_handshake_done。其主要做三部分工作:
1)将自己的chunk发送给对方:ngx_rtmp_send_chunk_size
2)将ack_size发送给对方:ngx_rtmp_send_ack_size
3)将amf发送给对方:ngx_rtmp_send_amf,主要是:
{ NGX_RTMP_AMF_STRING, ngx_null_string, "connect", 0 },