1. 点播的配置

假设配置文件 nginx.conf 中对 rtmp 配置如下:

# 创建的子进程数
worker_processes 1; #error_log logs/error.log;
error_log stderr debug;
#error_log logs/error.log info; #pid logs/nginx.pid;
#关闭以守护进程方式运行,方便进行调试
daemon off; master_process on; events {
worker_connections 1024;
} rtmp {
server {
listen 1935; # rtmp传输端口
chunk_size 4096; # 数据传输块大小
application vod {
play /home/xxx/samba/nginx/vod; # 视频文件存放位置
}
}
}

2. handshake 过程

2.1 ngx_rtmp_init_connection

void ngx_rtmp_init_connection(ngx_connection_t *c)
{
ngx_uint_t i;
ngx_rtmp_port_t *port;
struct sockaddr *sa;
struct sockaddr_in *sin;
ngx_rtmp_in_addr_t *addr;
ngx_rtmp_session_t *s;
ngx_rtmp_addr_conf_t *addr_conf;
ngx_int_t unix_socket;
#if (NGX_HAVE_INET6)
struct sockaddr_in6 *sin6;
ngx_rtmp_in6_addr_t *addr6;
#endif /* rtmp 连接的计数值加 1 */
++ngx_rtmp_naccepted; /* find the server configuration for the address:port */ /* AF_INET only */ port = c->listening->servers;
unix_socket = 0; if (port->naddrs > 1) { /*
* There are several addresses on this port and one of them
* is the "*:port" wildcard so getsockname() is needed to determine
* the server address.
*
* AcceptEx() already gave this address.
*/ if (ngx_connection_local_sockaddr(c, NULL, 0) != NGX_OK) {
ngx_rtmp_close_connection(c);
return;
} sa = c->local_sockaddr; switch (sa->sa_family) { #if (NGX_HAVE_INET6)
case AF_INET6:
sin6 = (struct sockaddr_in6 *) sa; addr6 = port->addrs; /* the last address is "*" */ for (i = 0; i < port->naddrs - 1; i++) {
if (ngx_memcmp(&addr6[i].addr6, &sin6->sin6_addr, 16) == 0) {
break;
}
} addr_conf = &addr6[i].conf; break;
#endif case AF_UNIX:
unix_socket = 1;
/* fall through */ default: /* AF_INET */
sin = (struct sockaddr_in *) sa; addr = port->addrs; /* the last address is "*" */ for (i = 0; i < port->naddrs - 1; i++) {
if (addr[i].addr == sin->sin_addr.s_addr) {
break;
}
} addr_conf = &addr[i].conf; break;
} } else {
switch (c->local_sockaddr->sa_family) { #if (NGX_HAVE_INET6)
case AF_INET6:
addr6 = port->addrs;
addr_conf = &addr6[0].conf;
break;
#endif case AF_UNIX:
unix_socket = 1;
/* fall through */ default: /* AF_INET */
addr = port->addrs;
addr_conf = &addr[0].conf;
break;
}
} ngx_log_error(NGX_LOG_INFO, c->log, 0, "*%ui client connected '%V'",
c->number, &c->addr_text); /* 创建并初始化一个 rtmp 会话 */
s = ngx_rtmp_init_session(c, addr_conf);
if (s == NULL) {
return;
} /* only auto-pushed connections are
* done through unix socket */ s->auto_pushed = unix_socket; /* 检测是否是开启了代理服务 */
if (addr_conf->proxy_protocol) {
ngx_rtmp_proxy_protocol(s); } else {
/* 开始 rtmp 的握手过程 */
ngx_rtmp_handshake(s);
}
}

2.2 ngx_rtmp_init_session

ngx_rtmp_session_t *ngx_rtmp_init_session(ngx_connection_t *c, ngx_rtmp_addr_conf_t *addr_conf)
{
ngx_rtmp_session_t *s;
ngx_rtmp_core_srv_conf_t *cscf;
ngx_rtmp_error_log_ctx_t *ctx; /* 为该会话分配内存 */
s = ngx_pcalloc(c->pool, sizeof(ngx_rtmp_session_t) +
sizeof(ngx_chain_t *) * ((ngx_rtmp_core_srv_conf_t *)
addr_conf->ctx-> srv_conf[ngx_rtmp_core_module
.ctx_index])->out_queue);
if (s == NULL) {
ngx_rtmp_close_connection(c);
return NULL;
} /* 指向 server{} 下所属的 ngx_rtmp_conf_ctx_t 下的 main_conf 指针数组 */
s->main_conf = addr_conf->ctx->main_conf;
/* 指向 server{} 下所属的 ngx_rtmp_conf_ctx_t 下的 srv_conf 指针数组 */
s->srv_conf = addr_conf->ctx->srv_conf; s->addr_text = &addr_conf->addr_text; /* 将 c->data 指向该会话结构体首地址 */
c->data = s;
s->connection = c; ctx = ngx_palloc(c->pool, sizeof(ngx_rtmp_error_log_ctx_t));
if (ctx == NULL) {
ngx_rtmp_close_connection(c);
return NULL;
} ctx->client = &c->addr_text;
ctx->session = s; c->log->connection = c->number;
c->log->handler = ngx_rtmp_log_error;
c->log->data = ctx;
c->log->action = NULL; c->log_error = NGX_ERROR_INFO; s->ctx = ngx_pcalloc(c->pool, sizeof(void *) * ngx_rtmp_max_module);
if (s->ctx == NULL) {
ngx_rtmp_close_connection(c);
return NULL;
} /* 获取 server{} 下创建的 ngx_rtmp_core_module 模块创建的
* ngx_rtmp_core_srv_conf_t 配置项结构体,该结构体也对应着
* 当前这个server{},因此也就保存着当前 server{} 下所有的
* 配置项 */
cscf = ngx_rtmp_get_module_srv_conf(s, ngx_rtmp_core_module); /* 输出队列的个数 */
s->out_queue = cscf->out_queue;
s->out_cork = cscf->out_cork;
s->in_streams = ngx_pcalloc(c->pool, sizeof(ngx_rtmp_stream_t)
* cscf->max_streams);
if (s->in_streams == NULL) {
ngx_rtmp_close_connection(c);
return NULL;
} #if (nginx_version >= 1007005)
ngx_queue_init(&s->posted_dry_events);
#endif s->epoch = ngx_current_msec;
s->timeout = cscf->timeout;
s->buflen = cscf->buflen;
/* 设置 */
ngx_rtmp_set_chunk_size(s, NGX_RTMP_DEFAULT_CHUNK_SIZE); /* 遍历 events 数组中保存的每个 RTMP 模块的 NGX_RTMP_CONNECT 事件,
* 这里仅有 TMP_LIMIT_MODULE 设置了该事件 */
if (ngx_rtmp_fire_event(s, NGX_RTMP_CONNECT, NULL, NULL) != NGX_OK) {
ngx_rtmp_finalize_session(s);
return NULL;
} return s;
}

2.2.1 ngx_rtmp_set_chunk_size

ngx_int_t ngx_rtmp_set_chunk_size(ngx_rtmp_session_t *s, ngx_uint_t size)
{
ngx_rtmp_core_srv_conf_t *cscf;
ngx_chain_t *li, *fli, *lo, *flo;
ngx_buf_t *bi, *bo;
ngx_int_t n; ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0,
"setting chunk_size=%ui", size); if (size > NGX_RTMP_MAX_CHUNK_SIZE) {
ngx_log_error(NGX_LOG_ALERT, s->connection->log, 0,
"too big RTMP chunk size:%ui", size);
return NGX_ERROR;
} /* 获取 server{} 所属的 ngx_rtmp_conf_ctx_t 的 srv_conf[0],即对应的
* ngx_rtmp_core_module 的srv级别配置结构体,也对应该 server{} 的 配置结构体 */
cscf = ngx_rtmp_get_module_srv_conf(s, ngx_rtmp_core_module); s->in_old_pool = s->in_pool;
/* 设置 rtmp 块的大小 */
s->in_chunk_size = size;
s->in_pool = ngx_create_pool(4096, s->connection->log); /* copy existing chunk data */
if (s->in_old_pool) {
s->in_chunk_size_changing = 1;
s->in_streams[0].in = NULL; for(n = 1; n < cscf->max_streams; ++n) {
/* stream buffer is circular
* for all streams except for the current one
* (which caused this chunk size change);
* we can simply ignore it */
li = s->in_streams[n].in;
if (li == NULL || li->next == NULL) {
s->in_streams[n].in = NULL;
continue;
}
/* move from last to the first */
li = li->next;
fli = li;
lo = ngx_rtmp_alloc_in_buf(s);
if (lo == NULL) {
return NGX_ERROR;
}
flo = lo;
for ( ;; ) {
bi = li->buf;
bo = lo->buf; if (bo->end - bo->last >= bi->last - bi->pos) {
bo->last = ngx_cpymem(bo->last, bi->pos,
bi->last - bi->pos);
li = li->next;
if (li == fli) {
lo->next = flo;
s->in_streams[n].in = lo;
break;
}
continue;
} bi->pos += (ngx_cpymem(bo->last, bi->pos,
bo->end - bo->last) - bo->last);
lo->next = ngx_rtmp_alloc_in_buf(s);
lo = lo->next;
if (lo == NULL) {
return NGX_ERROR;
}
}
}
} return NGX_OK;
}

该函数主要是设置 s->in_chunk_size(即 rtmp 块大小) 的大小,并为 s->in_pool 重新分配一个 4096 大小的内存池,

最后检测旧的内存池中是否有块数据,有则拷贝过来。

2.2.2 ngx_rtmp_fire_event


ngx_int_t ngx_rtmp_fire_event(ngx_rtmp_session_t *s, ngx_uint_t evt,
ngx_rtmp_header_t *h, ngx_chain_t *in)
{
ngx_rtmp_core_main_conf_t *cmcf;
ngx_array_t *ch;
ngx_rtmp_handler_pt *hh;
size_t n; /* 获取 server{} 所属的 ngx_rtmp_conf_ctx_t 的 main_conf[0],即对应的
* ngx_rtmp_core_module 的main级别配置结构体 */
cmcf = ngx_rtmp_get_module_main_conf(s, ngx_rtmp_core_module); /* 根据 evt 的值在events数组中找到对应的存储着
* 所有 RTMP 对这类 evt 事件的回调方法,该回调
* 方法一般是在每个 RTMP 模块的 postconfiguration
* 方法中添加的 */
ch = &cmcf->events[evt];
hh = ch->elts;
for(n = 0; n < ch->nelts; ++n, ++hh) {
if (*hh && (*hh)(s, h, in) != NGX_OK) {
return NGX_ERROR;
}
}
return NGX_OK;
}

该函数主要是找到所有 RTMP 模块在 postconfiguration 方法中根据 evt 值保存在 events 数组中的回调函数,然后

一一调用它们,没有找到则返回。

2.2.3 NGX_RTMP_CONNECT 事件

该事件只有 ngx_rtmp_limit_module 模块设置有回调函数:

static ngx_int_t ngx_rtmp_limit_postconfiguration(ngx_conf_t *cf)
{
ngx_rtmp_core_main_conf_t *cmcf;
ngx_rtmp_handler_pt *h; ... cmcf = ngx_rtmp_conf_get_module_main_conf(cf, ngx_rtmp_core_module); h = ngx_array_push(&cmcf->events[NGX_RTMP_CONNECT]);
*h = ngx_rtmp_limit_connect; ...
}

因此,在 ngx_rtmp_fire_event 方法中调用的对应 NGX_RTMP_CONNECT 事件的回调方法为 ngx_rtmp_limit_connect:

static ngx_int_t ngx_rtmp_limit_connect(ngx_rtmp_session_t *s, ngx_rtmp_header_t *h,
ngx_chain_t *in)
{
ngx_rtmp_limit_main_conf_t *lmcf;
ngx_slab_pool_t *shpool;
ngx_shm_zone_t *shm_zone;
uint32_t *nconn, n;
ngx_int_t rc; /* 获取 server{} 所属的 ngx_rtmp_conf_ctx_t 的 main_conf 指针数组中
* ngx_rtmp_limit_module 对应的序号的 main 级别配置结构体 */
lmcf = ngx_rtmp_get_module_main_conf(s, ngx_rtmp_limit_module);
/* 检测 nginx.conf 配置文件中是否设置了 max_connections 配置项,若没有
* 则直接返回 */
if (lmcf->max_conn == NGX_CONF_UNSET) {
return NGX_OK;
} /* 下面是在配置文件有 max_connections 配置项的情况下检测当前的
* 连接数是否大于 max_connections 的值 */
shm_zone = lmcf->shm_zone;
shpool = (ngx_slab_pool_t *) shm_zone->shm.addr;
nconn = shm_zone->data; ngx_shmtx_lock(&shpool->mutex);
n = ++*nconn;
ngx_shmtx_unlock(&shpool->mutex); rc = n > (ngx_uint_t) lmcf->max_conn ? NGX_ERROR : NGX_OK; ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0,
"limit: inc conection counter: %uD", n); if (rc != NGX_OK) {
ngx_log_error(NGX_LOG_ERR, s->connection->log, 0,
"limit: too many connections: %uD > %i",
n, lmcf->max_conn);
} return rc;
}

2.3 ngx_rtmp_handshake

在没有开启代理服务的情况下,初始化完一个 ngx_rtmp_session_t 后,直接进行 rtmp 的 handshake 过程。

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; ngx_log_debug0(NGX_LOG_DEBUG_RTMP, s->connection->log, 0,
"handshake: start server handshake"); /* 分配 handshake 过程的数据缓存 */
s->hs_buf = ngx_rtmp_alloc_handshake_buffer(s);
/* 初始化当前 handshake 的阶段 */
s->hs_stage = NGX_RTMP_HANDSHAKE_SERVER_RECV_CHALLENGE; /* 开始读取客户端发来的 C0,C1 数据 */
ngx_rtmp_handshake_recv(c->read);
}

该函数主要设置读写事件的回调函数,分配 handhshake 过程的数据缓存空间,并初始化当前 handshake 阶段。

2.4 ngx_rtmp_handshake_recv

/* RTMP handshake :
*
* =peer1= =peer2=
* challenge ----> (.....[digest1]......) ----> 1537 bytes
* response <---- (...........[digest2]) <---- 1536 bytes
*
*
* - both packets contain random bytes except for digests
* - digest1 position is calculated on random packet bytes
* - digest2 is always at the end of the packet
*
* digest1: HMAC_SHA256(packet, peer1_partial_key)
* digest2: HMAC_SHA256(packet, HMAC_SHA256(digest1, peer2_full_key))
*/ /* Handshake keys */
static u_char
ngx_rtmp_server_key[] = {
'G', 'e', 'n', 'u', 'i', 'n', 'e', ' ', 'A', 'd', 'o', 'b', 'e', ' ',
'F', 'l', 'a', 's', 'h', ' ', 'M', 'e', 'd', 'i', 'a', ' ',
'S', 'e', 'r', 'v', 'e', 'r', ' ',
'0', '0', '1', 0xF0, 0xEE, 0xC2, 0x4A, 0x80, 0x68, 0xBE, 0xE8, 0x2E, 0x00, 0xD0, 0xD1,
0x02, 0x9E, 0x7E, 0x57, 0x6E, 0xEC, 0x5D, 0x2D, 0x29, 0x80, 0x6F, 0xAB,
0x93, 0xB8, 0xE6, 0x36, 0xCF, 0xEB, 0x31, 0xAE
}; static u_char
ngx_rtmp_client_key[] = {
'G', 'e', 'n', 'u', 'i', 'n', 'e', ' ', 'A', 'd', 'o', 'b', 'e', ' ',
'F', 'l', 'a', 's', 'h', ' ', 'P', 'l', 'a', 'y', 'e', 'r', ' ',
'0', '0', '1', 0xF0, 0xEE, 0xC2, 0x4A, 0x80, 0x68, 0xBE, 0xE8, 0x2E, 0x00, 0xD0, 0xD1,
0x02, 0x9E, 0x7E, 0x57, 0x6E, 0xEC, 0x5D, 0x2D, 0x29, 0x80, 0x6F, 0xAB,
0x93, 0xB8, 0xE6, 0x36, 0xCF, 0xEB, 0x31, 0xAE
}; static const u_char
ngx_rtmp_server_version[4] = {
0x0D, 0x0E, 0x0A, 0x0D
}; static const u_char
ngx_rtmp_client_version[4] = {
0x0C, 0x00, 0x0D, 0x0E
}; static ngx_str_t ngx_rtmp_server_full_key
= { sizeof(ngx_rtmp_server_key), ngx_rtmp_server_key };
static ngx_str_t ngx_rtmp_server_partial_key
= { 36, ngx_rtmp_server_key }; static ngx_str_t ngx_rtmp_client_full_key
= { sizeof(ngx_rtmp_client_key), ngx_rtmp_client_key };
static ngx_str_t ngx_rtmp_client_partial_key
= { 30, ngx_rtmp_client_key }; 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;
s = c->data; /* 检测该连接是否已经被销毁了 */
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;
} /*
* 每次调用recv没有接收到客户端的数据时,都会把该事件添加到epoll的
* 读事件中,同时将该事件也添加到定时器中,并设置该读事件的回调函数为
* ngx_rtmp_handshake_recv方法,因此,当监听到客户端发送的数据而再次
* 调用该方法时,需要将该事件从定时器中移除。
*/
if (rev->timer_set) {
ngx_del_timer(rev);
} /* b 指向 handshake 的数据缓存首地址 */
b = s->hs_buf; /* 当 handshake 的缓存未满时 */
while (b->last != b->end) {
/* 调用 c->recv 指向的回调函数接收数据,实际调用的是
* ngx_unix_recv 方法 */
n = c->recv(c, b->last, b->end - b->last); if (n == NGX_ERROR || n == 0) {
ngx_rtmp_finalize_session(s);
return;
} /* 若返回值为 NGX_AGAIN,则将该读事件再次添加到定时器中,并将
* 也添加到 epoll 等监控机制中 */
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 指针 */
b->last += n;
} /* 将该读事件从 epoll 等事件监控机制中删除 */
if (rev->active) {
ngx_del_event(rev, NGX_READ_EVENT, 0);
} /* 接收到客户端发来的数据后,更新当前 handshake 阶段 */
++s->hs_stage;
ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0,
"handshake: stage %ui", s->hs_stage); switch (s->hs_stage) {
/* 服务端发送 S0、S1 阶段 */
case NGX_RTMP_HANDSHAKE_SERVER_SEND_CHALLENGE:
/*
* 此时接收到包应是 client 发来的 RTMP complex handshake 的 C0 和 C1 包.
* 因此解析 C0+C1,并进行验证. */
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) {
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;
}
/* 解析完客户端发送来的 C0 和 C1 并进行验证成功后,接着构建 S0+S1,
* 发送给客户端. */
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;
}
/* 发送 S0+S1,然后该函数接着构建 S2 并发送 */
ngx_rtmp_handshake_send(c->write);
break; /* 服务端握手结束阶段 */
case NGX_RTMP_HANDSHAKE_SERVER_DONE:
/* 这里表示接收到了客户端发来的 handshake 过程的最后一个packet C2 */
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;
}
}

2.5 ngx_rtmp_handshake_parse_challenge

/*
* RTMP Complex handshake:
*
* 抓包知:C0+C1共1537 bytes,C0 1 byte,C1 1536 bytes.
*
* C0: 1byte,即rtmp的版本号"\x03"
*
* C1和S1包含两部分数据:key和digest,分别为如下:
* - time: 4 bytes
* - version: 4 bytes
* - key: 764 bytes
* - digest: 764 bytes
*
* key和digest的顺序是不确定的,也有可能是:(nginx-rtmp中是如下的顺序)
* - time: 4 bytes
* - version: 4 bytes
* - digest: 764 bytes
* - key: 764 bytes *
* 764 bytes key 结构:
* - random-data: (offset) bytes
* - key-data: 128 bytes
* - random-data: (764 - offset - 128 - 4) bytes
* - offset: 4 bytes
*
* 764 bytes digest结构:
* - offset: 4 bytes
* - random-data: (offset) bytes
* - digest-data: 32 bytes
* - random-data: (764 - 4 - offset - 32) bytes
*/
static ngx_int_t ngx_rtmp_handshake_parse_challenge(ngx_rtmp_session_t *s,
ngx_str_t *peer_key, ngx_str_t *key)
{
ngx_buf_t *b;
u_char *p;
ngx_int_t offs; /* C0或S0: 第一个字节必须是 RTMP 协议的版本号"03" */
b = s->hs_buf;
if (*b->pos != '\x03') {
ngx_log_error(NGX_LOG_INFO, s->connection->log, 0,
"handshake: unexpected RTMP version: %i",
(ngx_int_t)*b->pos);
return NGX_ERROR;
} /* 接下来是 C1 或 S1 */ /* 版本号之后是客户端的 epoch 时间 */
++b->pos;
s->peer_epoch = 0;
ngx_rtmp_rmemcpy(&s->peer_epoch, b->pos, 4); /* 再接下来的四字节是客户端的版本号 */
p = b->pos + 4;
ngx_log_debug5(NGX_LOG_DEBUG_RTMP, s->connection->log, 0,
"handshake: peer version=%i.%i.%i.%i epoch=%uD",
(ngx_int_t)p[3], (ngx_int_t)p[2],
(ngx_int_t)p[1], (ngx_int_t)p[0],
(uint32_t)s->peer_epoch);
if (*(uint32_t *)p == 0) {
s->hs_old = 1;
return NGX_OK;
} /* 找到key和digest,进行验证 */
offs = ngx_rtmp_find_digest(b, peer_key, 772, s->connection->log);
if (offs == NGX_ERROR) {
offs = ngx_rtmp_find_digest(b, peer_key, 8, s->connection->log);
}
if (offs == NGX_ERROR) {
ngx_log_error(NGX_LOG_INFO, s->connection->log, 0,
"handshake: digest not found");
s->hs_old = 1;
return NGX_OK;
}
ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0,
"handshake: digest found at pos=%i", offs);
/* 这里 b->pos 和 b->last 分别指向找到 digest-data 数据区的起始和结尾地址 */
b->pos += offs;
b->last = b->pos + NGX_RTMP_HANDSHAKE_KEYLEN;
/* 下面是计算服务器的 digest */
s->hs_digest = ngx_palloc(s->connection->pool, NGX_RTMP_HANDSHAKE_KEYLEN);
if (ngx_rtmp_make_digest(key, b, NULL, s->hs_digest, s->connection->log)
!= NGX_OK)
{
return NGX_ERROR;
}
return NGX_OK;
}

2.5.1 ngx_rtmp_find_digest

/* 当 base 为 772 时,此时假设 C1 数据的结构如下:
* - time: 4 bytes
* - version: 4 bytes
* - key: 764 bytes
* - digest: 764 bytes
* 此时查找 digest,base 为 772,即跳过 C1 数据的 time、version、keys 这前三部分共 772 字节的数据,
* 从 digest 数据开始查找真正的 digest-data.
*
* 当 base 为 8 时,此时假设 C1 数据的结构如下:
* - time: 4 bytes
* - version: 4 bytes
* - digest: 764 bytes
* - key: 764 bytes
* 此时查找 digest,base 为 8,即跳过 C1 数据的 time、version 这前两部分共 8 字节的数据,
* 从 digest 数据开始查找真正的 digest-data.
*
* 764 bytes digest结构:
* - offset: 4 bytes
* - random-data: (offset) bytes
* - digest-data: 32 bytes
* - random-data: (764 - 4 - offset - 32 = 728 - offset) bytes
*
* 参考上面的digest结构,查找算法是:将 digest 数据开始的前 4 字节相加得到 offs,即 offset 值,
* 然后将 offs 除以 728 后取余数,再加上 base + 4,得出 digest-data 的在 C1 数据中的偏移值
*
*/
static ngx_int_t ngx_rtmp_find_digest(ngx_buf_t *b, ngx_str_t *key, size_t base, ngx_log_t *log)
{
size_t n, offs;
u_char digest[NGX_RTMP_HANDSHAKE_KEYLEN];
u_char *p; offs = 0;
/* 将 digest 数据开始的前 4 字节的值相加 得到 offset */
for (n = 0; n < 4; ++n) {
offs += b->pos[base + n];
}
offs = (offs % 728) + base + 4;
/* p 指向 digest-data 的起始地址 */
p = b->pos + offs; if (ngx_rtmp_make_digest(key, b, p, digest, log) != NGX_OK) {
return NGX_ERROR;
} /* 校验计算出来的 digest 与 p 指向的 digest-data 是否相同,相同表示校验
* 成功,即找到了正确的 digest-data 的偏移值 */
if (ngx_memcmp(digest, p, NGX_RTMP_HANDSHAKE_KEYLEN) == 0) {
return offs;
} return NGX_ERROR;
}

2.5.2 ngx_rtmp_make_digest

static ngx_int_t ngx_rtmp_make_digest(ngx_str_t *key, ngx_buf_t *src,
u_char *skip, u_char *dst, ngx_log_t *log)
{
static HMAC_CTX *hmac;
unsigned int len; if (hmac == NULL) {
#if OPENSSL_VERSION_NUMBER < 0x10100000L
static HMAC_CTX shmac;
hmac = &shmac;
/* 初始化 hmac,在计算 MAC 之前必须调用此函数 */
HMAC_CTX_init(hmac);
#else
hmac = HMAC_CTX_new();
if (hmac == NULL) {
return NGX_ERROR;
}
#endif
} /* 初始化 hmac 数据,在 hmac 中复制 EVP_256() 方法,密钥数据,
* 密钥数据长度len,最后一个参数为 NULL */
HMAC_Init_ex(hmac, key->data, key->len, EVP_sha256(), NULL); if (skip && src->pos <= skip && skip <= src->last) {
if (skip != src->pos) {
/* 将数据块加入到计算结果中,数据缓冲区 src->pos,数据长度skip - src->pos */
HMAC_Update(hmac, src->pos, skip - src->pos);
}
if (src->last != skip + NGX_RTMP_HANDSHAKE_KEYLEN) {
HMAC_Update(hmac, skip + NGX_RTMP_HANDSHAKE_KEYLEN,
src->last - skip - NGX_RTMP_HANDSHAKE_KEYLEN);
}
} else {
HMAC_Update(hmac, src->pos, src->last - src->pos);
} /* 计算出的 MAC 数据放置在 dst 中,用户需要保证 dst 数据有足够长的空间,长度为 len */
HMAC_Final(hmac, dst, &len); return NGX_OK;
}

散列消息鉴别码,简称 HMAC,是一种基于消息鉴别码 MAC(Message Authentication Code)的鉴别机制。使用 HMAC 时,

消息通讯的双方,通过验证消息中加入的鉴别密钥 K 来鉴别消息的真伪.

2.6 ngx_rtmp_handshake_create_challenge

/*
* RTMP complex handshake: S0+S1
*
* 抓包知,S0 1 byte,S1和S2都为1536 bytes
*
* S0: 1 byte,为rtmp版本号"\x03"
*
* S1: 1536 bytes (key和digest有些会调换一下)
* - time: 4 bytes
* - version: 4 bytes
* - key: 764 bytes
* - digest: 764 bytes
*
* 该函数仅构建了 S0+S1 共1537字节
*/
static ngx_int_t ngx_rtmp_handshake_create_challenge(ngx_rtmp_session_t *s,
const u_char version[4], ngx_str_t *key)
{
ngx_buf_t *b; b = s->hs_buf;
b->last = b->pos = b->start; /* S0: 1 byte */
*b->last++ = '\x03'; /* S1 */
/* - time: 4 bytes */
b->last = ngx_rtmp_rcpymem(b->last, &s->epoch, 4);
/* - version: 4 bytes */
b->last = ngx_cpymem(b->last, version, 4);
/* 之后的缓存先填满随机数据 */
ngx_rtmp_fill_random_buffer(b);
++b->pos; // 先略过 S0
/* 写入服务器的 digest-data */
if (ngx_rtmp_write_digest(b, key, 0, s->connection->log) != NGX_OK) {
return NGX_ERROR;
}
--b->pos; // 再恢复到指向 S0 位置
return NGX_OK;
}

2.6.1 ngx_rtmp_write_digest

/* 在 Nginx-rtmp 中 S1 的数据结构使用如下:
* - time: 4 bytes
* - version: 4 bytes
* - digest: 764 bytes
* - key: 764 bytes
*
* 764 bytes digest 结构:
* - offset: 4 bytes
* - random-data: (offset) bytes
* - digest-data: 32 bytes
* - random-data: (764 - 4 - offset - 32 = 728 - offset) bytes
*/
static ngx_int_t ngx_rtmp_write_digest(ngx_buf_t *b, ngx_str_t *key, size_t base,
ngx_log_t *log)
{
size_t n, offs;
u_char *p; offs = 0;
/* 将 b->pos[8] ~ b->pos[11] 这四个值相加得到 offset */
for (n = 8; n < 12; ++n) {
offs += b->pos[base + n];
} offs = (offs % 728) + base + 12;
p = b->pos + offs; /* 生成 digest 并存放在 p 指向的位置 */
if (ngx_rtmp_make_digest(key, b, p, p, log) != NGX_OK) {
return NGX_ERROR;
} return NGX_OK;
}

2.7 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; 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;
} /* 将 wev 写事件从定时器中移除 */
if (wev->timer_set) {
ngx_del_timer(wev);
} b = s->hs_buf; /* 当 handshake 缓存中有数据时 */
while(b->pos != b->last) {
/* 调用 c->send 指向的回调函数(即 ngx_unix_send 方法)发送数据 */
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;
} /* 若 wev 写事件为活跃的,则将其从 epoll 等事件监控机制中移除 */
if (wev->active) {
ngx_del_event(wev, NGX_WRITE_EVENT, 0);
} /* 更新当前 handshake 阶段 */
++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;
}
/* 这里构建 S2 */
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;
}
/* 发送 S2 */
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;
}
}

2.8 ngx_rtmp_handshake_create_response

/*
* RTMP complex handshake: C2/S2
* S2: 前 1504 bytes数据随机生成(前 8 bytes需要注意),然后对 1504 bytes 数据进行HMACsha256
* 得到digest,将digest放到最后的32bytes。
*
* 1536 bytes C2/S2 结构
* random-data: 1504 bytes
* digest-data: 32 bytes
*/
static ngx_int_t ngx_rtmp_handshake_create_response(ngx_rtmp_session_t *s)
{
ngx_buf_t *b;
u_char *p;
ngx_str_t key; b = s->hs_buf;
b->pos = b->last = b->start + 1;
ngx_rtmp_fill_random_buffer(b);
if (s->hs_digest) {
/* p 指向最后的 32 bytes 的首地址处 */
p = b->last - NGX_RTMP_HANDSHAKE_KEYLEN;
key.data = s->hs_digest;
key.len = NGX_RTMP_HANDSHAKE_KEYLEN;
/* 将生成的 digest 放置到 s2 数据的最后 32 bytes 中 */
if (ngx_rtmp_make_digest(&key, b, p, p, s->connection->log) != NGX_OK) {
return NGX_ERROR;
}
} return NGX_OK;
}

2.9 ngx_rtmp_handshake_done

static void ngx_rtmp_handshake_done(ngx_rtmp_session_t *s)
{
/* 释放 handshake 过程使用的缓存 hs_buf,其实,主要是将其
* 插入到 ngx_rtmp_core_srv_conf_t 的 free_hs 成员所持的
* 链表表头 */
ngx_rtmp_free_handshake_buffers(s); ngx_log_debug0(NGX_LOG_DEBUG_RTMP, s->connection->log, 0,
"handshake: done"); /* 在 events 数组中查找是否有 RTMP 模块设置有 NGX_RTMP_HANDSHAKE_DONE 的回调
* 方法,有则一一调用,否则不做任何处理 */
if (ngx_rtmp_fire_event(s, NGX_RTMP_HANDSHAKE_DONE,
NULL, NULL) != NGX_OK)
{
ngx_rtmp_finalize_session(s);
return;
} /* 下面进入 rtmp 业务循环 */
ngx_rtmp_cycle(s);
}

在所有的 RTMP 模块中,仅有 ngx_rtmp_relay_module 模块设置了 NGX_RTMP_HANDSHAKE_DONE 的回调方法:

static ngx_int_t ngx_rtmp_relay_postconfiguration(ngx_conf_t *cf)
{
ngx_rtmp_core_main_conf_t *cmcf;
ngx_rtmp_handler_pt *h; ... cmcf = ngx_rtmp_conf_get_module_main_conf(cf, ngx_rtmp_core_module); h = ngx_array_push(&cmcf->events[NGX_RTMP_HANDSHAKE_DONE]);
*h = ngx_rtmp_relay_handshake_done; ...
}

2.9.1 ngx_rtmp_relay_handshake_done

static ngx_int_t ngx_rtmp_relay_handshake_done(ngx_rtmp_session_t *s, ngx_rtmp_header_t *h,
ngx_chain_t *in)
{
ngx_rtmp_relay_ctx_t *ctx; /* 获取 ngx_rtmp_relay_module 模块的上下文结构体 */
ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_relay_module);
/* 若 relay 为 0,则直接返回,这里是为 0,因为没有使用 relay 模块的相关功能 */
if (ctx == NULL || !s->relay) {
return NGX_OK;
} return ngx_rtmp_relay_send_connect(s);
}

S -> C: S0 + S1 + S2

以上就是 nginx-rtmp 握手的全过程。

Nginx-rtmp点播之complex handshake的更多相关文章

  1. 实时监控、直播流、流媒体、视频网站开发方案流媒体服务器搭建及配置详解:使用nginx搭建rtmp直播、rtmp点播、,hls直播服务配置详解

    注意:这里不会讲到nginx流媒体模块如何安装的问题,只研究rtmp,hls直播和录制相关的nginx服务器配置文件的详细用法和说明.可以对照这些命令详解配置nginx -rtmp服务 一.nginx ...

  2. Android中直播视频技术探究之---视频直播服务端环境搭建(Nginx+RTMP)

    一.前言 前面介绍了Android中视频直播中的一个重要类ByteBuffer,不了解的同学可以 点击查看 到这里开始,我们开始动手开发了,因为我们后续肯定是需要直播视频功能,然后把视频推流到服务端, ...

  3. [微信小程序直播平台开发]___(二)Nginx+rtmp在Windows中的搭建

    1.一个可以忽略的前言 Nginx (engine x) 是一个高性能的HTTP和反向代理服务,也是一个IMAP/POP3/SMTP服务.Nginx是由伊戈尔·赛索耶夫为俄罗斯访问量第二的Ramble ...

  4. nginx RTMP FFmpeg 视频直播

    /**************************************************************************** * nginx RTMP FFmpeg 视频 ...

  5. Ubuntu中使用Nginx+rtmp模块搭建流媒体视频点播服务

    1. 背景 不知不觉笔者来到流媒体部门已经一年半多了,积攒了不少的流媒体知识,但平时工作也比较忙,很少进行总结性的梳理,最近准备花几个周末时间写一个流媒体系列的实践文章,也算是给自己做总结的同时帮助有 ...

  6. Ubuntu中使用Nginx+rtmp搭建流媒体直播服务

    一.背景 本篇文章是继上一篇文章<Ubuntu中使用Nginx+rtmp模块搭建流媒体视频点播服务>文章而写,在上一篇文章中我们搭建了一个点播服务器,在此基础上我们再搭建一个直播服务器, ...

  7. Nginx RTMP 专题

    说明: 记录器 - 记录器名称 path - 记录文件路径(recorded file path) (/tmp/rec/mystream-1389499351.flv)filename - 省略目录的 ...

  8. 转:Nginx RTMP 功能研究

    看点: 1.    Nginx 配置信息与使用.  (支持 rtmp与HLS配置) 2.    有ffmpeg 编译与使用,    命令行方式来测试验证客户端使用. 转自:http://blog.cs ...

  9. Mac使用nginx+rtmp服务器

    一.安装Homebrow 已经安装了brow的可以直接跳过这一步.执行命令 ruby -e "$(curl -fsSL https://raw.githubusercontent.com/H ...

随机推荐

  1. 微信小程序点击复制功能

    wx.setClipboardData({ data: '这是要复制的文字', success: function (res) { wx.showModal({ title: '提示', conten ...

  2. 从一道Hard学习滑动窗口

    滑动窗口 滑动窗口(sliding windows algorithm)这种方法,专门用于解决区间解的问题.它在运算的时候,将解集放在窗口中,结束的时候比对是否符合预期.在运算的过程中,会对窗口的左右 ...

  3. nginx的反向代理的优势,特点于原理(一)

    说到反向代理,首先先说一下反向代理的概念 反向代理(Reverse Proxy)方式是指以代理服务器来接受客户端的连接请求,然后将请求转发给网络上的web服务器(可能是apache,nginx,tom ...

  4. 【2017-05-19】WebForm复合控件、用DropDownList实现时间日期选择。

    自动提交的属性: AutoPostBack="True" 1.RadioButtonList     单选集合 -属性:RepeatDirection:Vertical (垂直排布 ...

  5. Python3.8新特性-- 海象操作符

    “理论联系实惠,密切联系领导,表扬和自我表扬”——我就是老司机,曾经写文章教各位怎么打拼职场的老司机. 不记得没关系,只需要知道:有这么一位老司机, 穿上西装带大家打拼职场! 操起键盘带大家打磨技术! ...

  6. 22_6mybatis中的缓存

    1.mybatis中的延时加载 问题:在一对多中,当我们有一个用户,它有100个账户. 在查询用户的时候,要不要把关联的账户查出来? 在查询账户的时候,要不要把关联的用户查出来? 在查询用户时,用户下 ...

  7. 异步消息处理机制相关面试问题-handler面试问题详解

    什么是handler? 这个异常应该也就是引出handler的原因,也就是默认在非UI线程中是无法去更新UI的东东滴,那到底什么上handler呢? handler通过发送和处理Message和Run ...

  8. yii\base\InvalidCallException The cookie collection is read only.

    Invalid Call – yii\base\InvalidCallException The cookie collection is read only. 在使用Yii2进行cookie操作时会 ...

  9. MyBatis Demo

    什么是 MyBatis? MyBatis 是支持普通 SQL 查询,存储过程和高级映射的优秀持久层框架. MyBatis 消除了几乎所有的 JDBC 代码和参数的手工设置以及对结果集的检索.MyBat ...

  10. linux编译esp8266

    编译工具是xtensa-lx106-elf-gcc,一般会在~/.bashrc文件下添加 export PATH="$HOME/esp-open-sdk/xtensa-lx106-elf/b ...