RTMP规范要求
上一篇文章,我们分析了rtmp模块接受用户请求的流程,主要介绍了一个用户请求到达server端之后,nginx rtmp模块如何接收请求,并为用户创建session,以及session的初始化等一系列操作;本章,我们将分析rtmp模块中的握手流程。
根据rtmp协议的规定,rtmp存在三次握手的过程,简单的来说,就是客户端发送C0、C1请求,服务端收到之后,发送S0、S1给客户端,客户端在收到S1之后需要发送C2,服务端在收到C1之后需要发送S2;只有当客户端收到S2之后,并且服务端收到C2之后,握手才算完成,接下来,就是发送正常的数据;主要流程如下图:
好了,上面的流程图提现了rtmp的握手交互过程,需要注意的是,C1和S0、S1没有严格发送的顺序,server可以在接收到C1之后的发送S0、S1,也可以在接收到C1之前就发送;
Nginx rtmp模块握手实现
RTMP服务端握手
上面主要介绍了rtmp的一些协议要求规范,接下来我们看一下nginx rtmp模块是如何实现这个握手的过程的。
nginx rtmp模块中,实现这一过程的代码主要存放在ngx_rtmp_handshake.c文件中,在介绍用户连接建立的文章中,我们可以看到,在用户连接建立完成之后,会调用方法ngx_rtmp_handshake,来设置基于该连接的后续处理方法。让我们来看一下该函数中做了哪些操作:
void ngx_rtmp_handshake(ngx_rtmp_session_t *s)
{
ngx_connection_t *c;
//由于传入的参数是用户的session,而网络方法调用是基于连接来实现的,所以需要从session中获取到连接对象
c = s->connection;
c->read->handler = ngx_rtmp_handshake_recv; //为连接对象设置接收数据的回调函数
c->write->handler = ngx_rtmp_handshake_send; //为连接对象设置发送数据的回调函数
ngx_log_debug0(NGX_LOG_DEBUG_RTMP, s->connection->log, 0,
"handshake: start server handshake");
s->hs_buf = ngx_rtmp_alloc_handshake_buffer(s); //为握手分配数据存储空间
s->hs_stage = NGX_RTMP_HANDSHAKE_SERVER_RECV_CHALLENGE; //作为服务端,设置当前初始状态
ngx_rtmp_handshake_recv(c->read); //开始接受数据第一个发送给该连接的数据包
}
需要注意的是,nginx rtmp模块的握手,是使用状态机来实现的,它定义了几个状态,当收到消息之后,就会触发状态的改变,不同的状态,会触发相应的行为事件;
接下来我们看一下ngx_rtmp_handshake_recv方法:
static void ngx_rtmp_handshake_recv(ngx_event_t *rev)
{
ssize_t n;
ngx_connection_t *c;
ngx_rtmp_session_t *s;
ngx_buf_t *b;
c = rev->data; //由于传进来的参数是一个read的事件结构体,我们现在需要获取到该事件上的连接句柄
s = c->data; //通过连接句柄获取到该连接上的session对象
if (c->destroyed) { //针对于连接进行一个判断,查看连接是否关闭
return;
}
if (rev->timedout) { //该事件是否已经超时
ngx_log_error(NGX_LOG_INFO, c->log, NGX_ETIMEDOUT,
"handshake: recv: client timed out");
c->timedout = 1;
ngx_rtmp_finalize_session(s);
return;
}
if (rev->timer_set) { //是否设置过超时事件,如果设置过,则去除掉定时事件
ngx_del_timer(rev);
}
b = s->hs_buf; //获取握手数据缓存区
while (b->last != b->end) {
n = c->recv(c, b->last, b->end - b->last); //接受握手数据
if (n == NGX_ERROR || n == 0) {
ngx_rtmp_finalize_session(s);
return;
}
if (n == NGX_AGAIN) {
ngx_add_timer(rev, s->timeout);
if (ngx_handle_read_event(c->read, 0) != NGX_OK) {
ngx_rtmp_finalize_session(s);
}
return;
}
b->last += n;
}
if (rev->active) { //由于数据已经读取,所以删除read事件
ngx_del_event(rev, NGX_READ_EVENT, 0);
}
++s->hs_stage; //由于接收到了数据,所以需要改变当前握手的状态
ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0,
"handshake: stage %ui", 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_log_error(NGX_LOG_INFO, c->log, 0,
"handshake: error parsing challenge");
ngx_rtmp_finalize_session(s);
return;
}
if (s->hs_old) { //当前缓存区中,解析完之后,剩余了部分原始数据,这是由于rtmp是基于tcp协议进行传输的,存在粘包的情况,所以需要进行分包处理,
ngx_log_debug0(NGX_LOG_DEBUG_RTMP, s->connection->log, 0,
"handshake: old-style challenge");
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_log_error(NGX_LOG_INFO, c->log, 0,
"handshake: error creating challenge");
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_log_error(NGX_LOG_INFO, c->log, 0,
"handshake: error parsing challenge");
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_log_error(NGX_LOG_INFO, c->log, 0,
"handshake: response error");
ngx_rtmp_finalize_session(s);
return;
}
ngx_rtmp_handshake_send(c->write);
break;
}
}
上面是作为接收到消息时的处理方式,接下来我们看一下,当发送消息的时候,会做什么,发送消息的代码在ngx_rtmp_handshake_send方法中;
static void
ngx_rtmp_handshake_send(ngx_event_t *wev)
{
ngx_int_t n;
ngx_connection_t *c;
ngx_rtmp_session_t *s;
ngx_buf_t *b;
c = wev->data; //由于该函数传输进来的参数是写事件,所以获取该事件的连接对象
s = c->data; //通过连接对象获取到会话session
if (c->destroyed) {
return;
}
if (wev->timedout) {
ngx_log_error(NGX_LOG_INFO, c->log, NGX_ETIMEDOUT,
"handshake: send: client timed out");
c->timedout = 1;
ngx_rtmp_finalize_session(s);
return;
}
if (wev->timer_set) {
ngx_del_timer(wev);
}
b = s->hs_buf;
while(b->pos != b->last) {
n = c->send(c, b->pos, b->last - b->pos); //发送握手数据
if (n == NGX_ERROR) {
ngx_rtmp_finalize_session(s);
return;
}
if (n == NGX_AGAIN || n == 0) {
ngx_add_timer(c->write, s->timeout);
if (ngx_handle_write_event(c->write, 0) != NGX_OK) {
ngx_rtmp_finalize_session(s);
}
return;
}
b->pos += n;
}
if (wev->active) {
ngx_del_event(wev, NGX_WRITE_EVENT, 0);
}
++s->hs_stage; //设置当前状态
ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0,
"handshake: stage %ui", s->hs_stage);
switch (s->hs_stage) {//根据不同的状态做不同的处理
case NGX_RTMP_HANDSHAKE_SERVER_SEND_RESPONSE:
if (s->hs_old) {
ngx_log_debug0(NGX_LOG_DEBUG_RTMP, s->connection->log, 0,
"handshake: old-style response");
s->hs_buf->pos = s->hs_buf->start + 1;
s->hs_buf->last = s->hs_buf->end;
} else if (ngx_rtmp_handshake_create_response(s) != NGX_OK) {
ngx_log_error(NGX_LOG_INFO, c->log, 0,
"handshake: response error");
ngx_rtmp_finalize_session(s);
return;
}
ngx_rtmp_handshake_send(wev);
break;
case NGX_RTMP_HANDSHAKE_SERVER_RECV_RESPONSE:
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_RECV_CHALLENGE:
s->hs_buf->pos = s->hs_buf->last = s->hs_buf->start;
ngx_rtmp_handshake_recv(c->read);
break;
case NGX_RTMP_HANDSHAKE_CLIENT_DONE:
ngx_rtmp_handshake_done(s);
break;
}
}
上面的两个方法是nginx-rtmp模块实现握手的主要流程的主要代码;在上面的代码中,我们可以看到,使用了状态机方式,由于rtmp协议中,针对于服务端和客户端握手消息做了规定,所以只需要根据消息到达可发送的顺序,就可以知道,当前处于的握手状态;基于这个原因,nginx的rtmp模块非常巧妙的使用了状态机来处理;
server端状态机变化:
NGX_RTMP_HANDSHAKE_SERVER_RECV_CHALLENGE
NGX_RTMP_HANDSHAKE_SERVER_SEND_CHALLENGE
NGX_RTMP_HANDSHAKE_SERVER_SEND_RESPONSE
NGX_RTMP_HANDSHAKE_SERVER_RECV_RESPONSE
NGX_RTMP_HANDSHAKE_SERVER_DONE
RTMP客户端握手
在rtmp模块中,不仅实现了服务端的握手功能,还实现了客户端的握手功能;
实现服务端的握手功能很好理解,毕竟nginx是一个高性能的服务程序;但是作为客户端这是为啥呢?那是因为,nginx rtmp模块,可以作为cdn使用,当它作为cdn的边缘节点时候,它就需要去向根节点请求数据,这个时候,他就是一个客户端,所以它需要实现客户端的握手功能;接下来我们看一下作为服务端的实现:
void ngx_rtmp_client_handshake(ngx_rtmp_session_t *s, unsigned async)
{
ngx_connection_t *c;
c = s->connection; //从session中获取到连接句柄
c->read->handler = ngx_rtmp_handshake_recv; //为read事件设置回调函数,该方法和上面作为server端的时候的回调
c->write->handler = ngx_rtmp_handshake_send; //为write事件设置回调函数,该方法和上面作为server端的时候的回调
ngx_log_debug0(NGX_LOG_DEBUG_RTMP, s->connection->log, 0,
"handshake: start client handshake");
s->hs_buf = ngx_rtmp_alloc_handshake_buffer(s); //分配握手需要的数据缓存区
s->hs_stage = NGX_RTMP_HANDSHAKE_CLIENT_SEND_CHALLENGE; //设置初始化句柄
//创建C0数据包
if (ngx_rtmp_handshake_create_challenge(s,
ngx_rtmp_client_version,
&ngx_rtmp_client_partial_key) != NGX_OK)
{
ngx_rtmp_finalize_session(s);
return;
}
if (async) {//异步网络发送判断
ngx_add_timer(c->write, s->timeout);
if (ngx_handle_write_event(c->write, 0) != NGX_OK) {
ngx_rtmp_finalize_session(s);
}
return;
}
//发送数据到对端,这个方法和上面server端的代码相同用
ngx_rtmp_handshake_send(c->write);
}
从上面的rtmp作为客户端的代码来看,可以看出来,其实和作为server端调用的函数是相同的,作为server端和作为client端的主要区别就是第一个调用方法不同,作为client端,首先调用的是ngx_rtmp_client_handshake方法,而作为serevr端,首先调用的是ngx_rtmp_handshake端;
client状态机变化:
NGX_RTMP_HANDSHAKE_CLIENT_SEND_CHALLENGE
NGX_RTMP_HANDSHAKE_CLIENT_RECV_CHALLENGE
NGX_RTMP_HANDSHAKE_CLIENT_RECV_RESPONSE
NGX_RTMP_HANDSHAKE_CLIENT_SEND_RESPONSE
NGX_RTMP_HANDSHAKE_CLIENT_DONE
在握手完成中之后,会调用方法ngx_rtmp_handshake_done