连接建立整体过程:
- 解析listen配置-ngx_rtmp_core_listen
- 根据ip地址端口创建ngx_listening_t监听套接字-ngx_rtmp_optimize_servers
- 创建socket,打开监听–ngx_open_listening_sockets
- accpet监听事件初始化–ngx_event_process_init
- rtmp连接创建 ngx_rtmp_init_connection
- rtmp三次握手
监听端口相关数据结构
listen监听端口相关的数据结构主要有以下数据结构,简单来说,nginx配置文件中可以有多个listen端口,每个端口对应有多个ip地址。
- ngx_rtmp_conf_addr_t
- ngx_rtmp_conf_port_t
- ngx_rtmp_listen_t
- ngx_rtmp_core_main_conf_t
服务器很多都是有多网卡的,一个端口也可以对应有多个地址,监听端口的时候可以配置监听某个指定地址的端口,也可以只配置端口那么就是所有ip地址都监听。
ngx_rtmp_conf_addr_t是用来表示监听的某个地址信息。
typedef struct {
struct sockaddr *sockaddr;
socklen_t socklen;
ngx_rtmp_conf_ctx_t *ctx;
unsigned bind:1;
unsigned wildcard:1; //如果赋值为1表示监听当前端口的所有地址
#if (NGX_HAVE_INET6 && defined IPV6_V6ONLY)
unsigned ipv6only:2;
#endif
unsigned so_keepalive:2;
unsigned proxy_protocol:1;
#if (NGX_HAVE_KEEPALIVE_TUNABLE)
int tcp_keepidle;
int tcp_keepintvl;
int tcp_keepcnt;
#endif
} ngx_rtmp_conf_addr_t;
ngx_rtmp_conf_port_t是用来表示某个端口对应的地址ngx_rtmp_conf_addr_t。如果填写的是1935端口,且机器是多ip的情况,监听的是多个ip的端口。
ngx_rtmp_conf_port_t 里面有端口信息,addrs存储的其实是 ngx_rtmp_conf_addr_t 地址信息,多个地址以数组形式存储的
typedef struct {
int family; /* socket 地址家族 */
in_port_t port; /* 监听端口 */
/* 监听的端口下对应着的所有 ngx_rtmp_conf_addr_t 地址 */
ngx_array_t addrs; /* array of ngx_rtmp_conf_addr_t */
} ngx_rtmp_conf_port_t;
在ngx_rtmp_core_listen解析listen配置的时候会根据每个端口port创建ngx_rtmp_listen_t。
typedef struct {
u_char sockaddr[NGX_SOCKADDRLEN];
socklen_t socklen;
/* server ctx */
ngx_rtmp_conf_ctx_t *ctx;
unsigned bind:1;
unsigned wildcard:1;
#if (NGX_HAVE_INET6 && defined IPV6_V6ONLY)
unsigned ipv6only:2;
#endif
unsigned so_keepalive:2;
unsigned proxy_protocol:1;
#if (NGX_HAVE_KEEPALIVE_TUNABLE)
int tcp_keepidle;
int tcp_keepintvl;
int tcp_keepcnt;
#endif
} ngx_rtmp_listen_t;
端口地址信息是以动态数组的形式全部存放在ngx_rtmp_core_main_conf_t当中,每个listen端口对应一个或多个地址
typedef struct {
…
ngx_array_t listen; /* ngx_rtmp_listen_t */
…
} ngx_rtmp_core_main_conf_t;
总得来说,nginx-rtmp 是使用 ngx_rtmp_conf_addr_t 结构体来表示一个对应着具体地址的监听端口信息,而一个端口又有多个地址。因此,一个 ngx_rtmp_conf_port_t 将会对应多个 ngx_rtmp_conf_addr_t,而 ngx_rtmp_conf_addr_t 是以动态数组的形式保存在 ngx_rtmp_conf_port_t 中的 addrs 成员中的。
listen端口配置解析
nginx-rtmp的监听端口配置解析过程是在ngx_rtmp_core_listen函数当中实现的,以下是ngx_rtmp_core_listen 主要源码逻辑:
static char *
ngx_rtmp_core_listen(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
{
......
value = cf->args->elts;
ngx_memzero(&u, sizeof(ngx_url_t));
u.url = value[1];
/* 表示当前监听端口生效 */
u.listen = 1;
/* 从listen 配置后的第一个端口参数进行解析端口和ip地址信息, 有可能是1935,也有可能是127.0.0.1:1935
* 注意 u->wildcard = 1的时候是表示监听所有的ip地址,后面ls->wildcard也是从这里来的
* /
if (ngx_parse_url(cf->pool, &u) != NGX_OK) {
if (u.err) {
ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
"%s in \"%V\" of the \"listen\" directive",
u.err, &u.url);
}
return NGX_CONF_ERROR;
}
cmcf = ngx_rtmp_conf_get_module_main_conf(cf, ngx_rtmp_core_module);
ls = cmcf->listen.elts;
/* 从当前动态数组当中匹配是否有重复的listen配置,主要比较端口对应地址 */
for (i = 0; i < cmcf->listen.nelts; i++) {
sa = (struct sockaddr *) ls[i].sockaddr;
if (sa->sa_family != u.family) {
continue;
}
.....
}
/* 从动态数组当中新创建一个ngx_rtmp_listen_t 结构体,并且根据listen后面参数初始化对应成员变量值 */
ls = ngx_array_push(&cmcf->listen);
if (ls == NULL) {
return NGX_CONF_ERROR;
}
ngx_memzero(ls, sizeof(ngx_rtmp_listen_t));
ngx_memcpy(ls->sockaddr, (u_char *) &u.sockaddr, u.socklen);
ls->socklen = u.socklen;
ls->wildcard = u.wildcard;
ls->ctx = cf->ctx;
.....
for (i = 2; i < cf->args->nelts; i++) {
/* 设置bind参数 */
if (ngx_strcmp(value[i].data, "bind") == 0) {
ls->bind = 1;
continue;
}
if (ngx_strncmp(value[i].data, "ipv6only=o", 10) == 0) {
#if (NGX_HAVE_INET6 && defined IPV6_V6ONLY)
struct sockaddr *sa;
u_char buf[NGX_SOCKADDR_STRLEN];
sa = (struct sockaddr *) ls->sockaddr;
if (sa->sa_family == AF_INET6) {
if (ngx_strcmp(&value[i].data[10], "n") == 0) {
ls->ipv6only = 1;
} else if (ngx_strcmp(&value[i].data[10], "ff") == 0) {
ls->ipv6only = 0;
} else {
ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
"invalid ipv6only flags \"%s\"",
&value[i].data[9]);
return NGX_CONF_ERROR;
}
ls->bind = 1;
} else {
len = ngx_sock_ntop(sa,
#if (nginx_version >= 1005003)
ls->socklen,
#endif
buf, NGX_SOCKADDR_STRLEN, 1);
}
continue;
......
}
/* 设置so_keepalive 参数 */
if (ngx_strncmp(value[i].data, "so_keepalive=", 13) == 0) {
if (ngx_strcmp(&value[i].data[13], "on") == 0) {
ls->so_keepalive = 1;
} else if (ngx_strcmp(&value[i].data[13], "off") == 0) {
ls->so_keepalive = 2;
} else {
......
if (ls->tcp_keepidle == 0 && ls->tcp_keepintvl == 0
&& ls->tcp_keepcnt == 0)
{
goto invalid_so_keepalive;
}
ls->so_keepalive = 1;
......
}
ls->bind = 1;
continue;
/* 设置proxy_protocol参数 */
if (ngx_strcmp(value[i].data, "proxy_protocol") == 0) {
ls->proxy_protocol = 1;
continue;
}
ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
"the invalid \"%V\" parameter", &value[i]);
return NGX_CONF_ERROR;
}
return NGX_CONF_OK;
}
1)先从cmcf->listen中判断当前端口以及对应的地址是否已经存在,如果存在说明listen配置重复
2)如果不存在,根据listen配置创建一个 ngx_rtmp_listen_t,并设置listen对应参数。
3)解析完后将当前ngx_rtmp_core_listen放到cmcf->listen动态数组当中
解析完listen端口之后,将 ngx_rtmp_listen_t和 ngx_rtmp_conf_port_t映射,每个端口都有一个ngx_rtmp_conf_port_t。
这个过程主要是在ngx_rtmp_add_ports函数当中完成。在 ngx_rtmp_block 函数中调用。
static char *
ngx_rtmp_block(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
{
ngx_rtmp_listen_t *listen;
......
/* 从之前ngx_rtmp_core_listen解析cmcf->listen数组里面取listen信息 */
listen = cmcf->listen.elts;
for (i = 0; i < cmcf->listen.nelts; i++) {
if (ngx_rtmp_add_ports(cf, &ports, &listen[i]) != NGX_OK) {
return NGX_CONF_ERROR;
}
}
return ngx_rtmp_optimize_servers(cf, &ports);
}
从cmcf->listen数组当中,根据每个listen创建port信息。
static ngx_int_t
ngx_rtmp_add_ports(ngx_conf_t *cf, ngx_array_t *ports,
ngx_rtmp_listen_t *listen)
{
in_port_t p;
ngx_uint_t i;
struct sockaddr *sa;
struct sockaddr_in *sin;
ngx_rtmp_conf_port_t *port;
ngx_rtmp_conf_addr_t *addr;
.....
sa = (struct sockaddr *) &listen->sockaddr;
......
/* 先判断之前ports数组里面port是否已经存在,每个端口是有可能有多个ip地址 */
port = ports->elts;
for (i = 0; i < ports->nelts; i++) {
if (p == port[i].port && sa->sa_family == port[i].family) {
/* a port is already in the port list */
port = &port[i];
goto found;
}
}
/* add a port to the port list */
port = ngx_array_push(ports);
if (port == NULL) {
return NGX_ERROR;
}
port->family = sa->sa_family;
port->port = p;
if (ngx_array_init(&port->addrs, cf->temp_pool, 2,
sizeof(ngx_rtmp_conf_addr_t))
!= NGX_OK)
{
return NGX_ERROR;
}
found:
addr = ngx_array_push(&port->addrs);
if (addr == NULL) {
return NGX_ERROR;
}
/* 初始化端口对应ngx_rtmp_conf_addr_t地址信息 */
addr->sockaddr = (struct sockaddr *) &listen->sockaddr;
addr->socklen = listen->socklen;
addr->ctx = listen->ctx;
addr->bind = listen->bind;
addr->wildcard = listen->wildcard;
addr->so_keepalive = listen->so_keepalive;
addr->proxy_protocol = listen->proxy_protocol;
#if (NGX_HAVE_KEEPALIVE_TUNABLE)
addr->tcp_keepidle = listen->tcp_keepidle;
addr->tcp_keepintvl = listen->tcp_keepintvl;
addr->tcp_keepcnt = listen->tcp_keepcnt;
#endif
#if (NGX_HAVE_INET6 && defined IPV6_V6ONLY)
addr->ipv6only = listen->ipv6only;
#endif
return NGX_OK;
}
ngx_rtmp_optimize_servers 函数主要是根据每个ngx_rtmp_conf_port_t端口对应的每个地址创建socket以及监听需要的初始化信息ngx_listening_t。
static char *
ngx_rtmp_optimize_servers(ngx_conf_t *cf, ngx_array_t *ports)
{
ngx_uint_t i, p, last, bind_wildcard;
ngx_listening_t *ls;
ngx_rtmp_port_t *mport;
ngx_rtmp_conf_port_t *port;
ngx_rtmp_conf_addr_t *addr;
port = ports->elts;
/* 遍历ports端口数组,给每个端口每个地址创建socket监听信息的ngx_listenning_t数据 */
for (p = 0; p < ports->nelts; p++) {
ngx_sort(port[p].addrs.elts, (size_t) port[p].addrs.nelts,
sizeof(ngx_rtmp_conf_addr_t), ngx_rtmp_cmp_conf_addrs);
addr = port[p].addrs.elts;
/* 表示一个端口有多少个地址 */
last = port[p].addrs.nelts;
/*
* if there is the binding to the "*:port" then we need to bind()
* to the "*:port" only and ignore the other bindings
*/
if (addr[last - 1].wildcard) {
addr[last - 1].bind = 1;
bind_wildcard = 1;
} else {
bind_wildcard = 0;
}
i = 0;
/* 给每个地址创建listenning信息 */
while (i < last) {
if (bind_wildcard && !addr[i].bind) {
i++;
continue;
}
ls = ngx_create_listening(cf, addr[i].sockaddr, addr[i].socklen);
if (ls == NULL) {
return NGX_CONF_ERROR;
}
ls->addr_ntop = 1;
/* 监听事件的处理函数,后面accept接受到监听事件之后,会调用此handler函数 */
ls->handler = ngx_rtmp_init_connection;
ls->pool_size = 4096;
/* TODO: error_log directive */
ls->logp = &cf->cycle->new_log;
ls->log.data = &ls->addr_text;
ls->log.handler = ngx_accept_log_error;
ls->keepalive = addr[i].so_keepalive;
#if (NGX_HAVE_KEEPALIVE_TUNABLE)
ls->keepidle = addr[i].tcp_keepidle;
ls->keepintvl = addr[i].tcp_keepintvl;
ls->keepcnt = addr[i].tcp_keepcnt;
#endif
#if (NGX_HAVE_INET6 && defined IPV6_V6ONLY)
ls->ipv6only = addr[i].ipv6only;
#endif
mport = ngx_palloc(cf->pool, sizeof(ngx_rtmp_port_t));
if (mport == NULL) {
return NGX_CONF_ERROR;
}
ls->servers = mport;
if (i == last - 1) {
mport->naddrs = last;
} else {
mport->naddrs = 1;
i = 0;
}
/* 给每个ngx_rtmp_conf_addr_t地址信息赋值 */
switch (ls->sockaddr->sa_family) {
#if (NGX_HAVE_INET6)
case AF_INET6:
if (ngx_rtmp_add_addrs6(cf, mport, addr) != NGX_OK) {
return NGX_CONF_ERROR;
}
break;
#endif
default: /* AF_INET */
if (ngx_rtmp_add_addrs(cf, mport, addr) != NGX_OK) {
return NGX_CONF_ERROR;
}
break;
}
addr++;
last--;
}
}
return NGX_CONF_OK;
}
解析完listen的配置,并给每个端口的每个地址对应ngx_listenning_t结构初始化后,接下来就是打开配置每个监听的端口。
ngx_open_listening_sockets 这个函数就是打开socket,和一般socket编程一样,就是调用socket、bind和listen
ngx_configure_listening_sockets 这个函数就是根据配置调用setsockopt函数设置socket选项,比如接收缓冲区、发送缓冲区等
以上过程都是在 ngx_init_cycyle()函数当中完成的。
整体的listen配置解析过程就是 ngx_init_cycyle–>ngx_rtmp_block–>ngx_rtmp_core_listen–>ngx_rtmp_add_port—>ngx_rtmp_optimize_servers。
accpet事件处理
接下来就是在每个worker进程启动的时候,会将监听accept事件放入到响应的事件驱动中。下面以epoll事件驱动为例子
ngx_event_process_init函数(ngx_event_core_moduel的process_init回调函数,在创建完worker进程后调用)中将这些监听socket添加到事件循环中。
static ngx_int_t
ngx_event_process_init(ngx_cycle_t *cycle)
{
ls = cycle->listening.elts;
for (i = 0; i < cycle->listening.nelts; i++) {
/* 对每个监听的端口,从连接池获取一个空闲的连接 */
c = ngx_get_connection(ls[i].fd, cycle->log);
...
ls[i].connection = c;
rev = c->read;
...
#if (NGX_WIN32)
if (ngx_event_flags & NGX_USE_IOCP_EVENT) {
ngx_iocp_conf_t *iocpcf;
...
} else {
// 将连接事件的处理函数挂载到 ngx_event_accept 函数上
rev->handler = ngx_event_accept;
if (ngx_use_accept_mutex) {
continue;
}
// 将当前事件加到epoll事件当中
if (ngx_add_event(rev, NGX_READ_EVENT, 0) == NGX_ERROR) {
return NGX_ERROR;
}
}
#else
rev->handler = (c->type == SOCK_STREAM) ? ngx_event_accept
: ngx_event_recvmsg;
#if (NGX_HAVE_REUSEPORT)
if (ls[i].reuseport) {
if (ngx_add_event(rev, NGX_READ_EVENT, 0) == NGX_ERROR) {
return NGX_ERROR;
}
continue;
}
}
}
这里需要注意下单进程和多进程监听事件加入epoll当中的过程,多进程需要先拿到ngx_accept_mutex_held锁,然后再通过ngx_enable_accept_events将accpet事件放入epoll当中。
注册完accpet监听事件handler后,就是等待网络事件到来。以下是从accpet外部链接到握手的阶段过程:
epoll接受到外界客户端发来的accpet连接请求,首先会进入到ngx_event_accept函数当中,该函数是处理accpet连接事件的handler。
void
ngx_event_accept(ngx_event_t *ev)
{
do {
socklen = sizeof(ngx_sockaddr_t);
......
s = accept(lc->fd, &sa.sockaddr, &socklen);
.....
/* 获取一个空闲的 connection 关联到新建立的连接上,并进行一些初始化工作 */
c = ngx_get_connection(s, ev->log);
if (c == NULL) {
if (ngx_close_socket(s) == -1) {
ngx_log_error(NGX_LOG_ALERT, ev->log, ngx_socket_errno,
ngx_close_socket_n " failed");
}
return;
}
.....
c->recv = ngx_recv;
c->send = ngx_send;
c->recv_chain = ngx_recv_chain;
c->send_chain = ngx_send_chain;
c->log = log;
c->pool->log = log;
c->socklen = socklen;
c->listening = ls;
c->local_sockaddr = ls->sockaddr;
c->local_socklen = ls->socklen;
c->unexpected_eof = 1;
......
if (ngx_add_conn && (ngx_event_flags & NGX_USE_EPOLL_EVENT) == 0) {
if (ngx_add_conn(c) == NGX_ERROR) {
ngx_close_accepted_connection(c);
return;
}
}
log->data = NULL;
log->handler = NULL;
/* 调用连接处理句柄进行处理ngx_rtmp_init_connection, 这个函数就是在前面ngx_rtmp_optimize_servers中注册的 */
ls->handler(c);
if (ngx_event_flags & NGX_USE_KQUEUE_EVENT) {
ev->available--;
}
} while (ev->available);
}
从以上函数当中明白,主要是:
- 接受accpet外部连接请求
- 获取一个空闲的 connection 关联到新建立的连接上,并进行一些初始化工作
- 调用连接处理handler句柄进行处理ngx_rtmp_init_connection
此处的 handler 根据不同的模块挂载不同,挂载点一般为配置初始化时,对服务端口进行监听时挂载的:
如 HTTP 模块是在 ngx_http_block->ngx_http_optimize_servers->ngx_http_init_listening->ngx_http_add_listening 时挂载的,
挂载处理函数为 ngx_http_init_connection
如 MAIL 模块是在 ngx_mail_block->ngx_mail_optimize_servers 时挂载的,挂载处理函数为 ngx_mail_init_connection
最后就是进入ngx_rtmp_init_connection中,该函数主要建立rtmp session初始化工作以及准备ngx_rtmp_handshake进入rtmp的三次握手阶段:
void
ngx_rtmp_handshake(ngx_rtmp_session_t *s)
{
ngx_connection_t *c;
ngx_time_t *tp;
c = s->connection;
/* 给rtmp链接的挂载读写事件的handler */
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;
tp = ngx_timeofday();
s->start_sec = tp->sec;
s->start_msec = tp->msec;
ngx_rtmp_handshake_recv(c->read);
}
ngx_rtmp_handshake_recv rtmp链接的读事件handler,主要接受来自客户端的c0, c1 c2的握手请求
ngx_rtmp_handshake_send rtmp链接的写事件handler,主要给客户端响应握手请求s0,s1, s2请求信息
三次握手请求后,结束握手ngx_rtmp_handshake_done进入rtmp的窗口确认,connect,createStream请求里面。这些操作会重新挂载读写事件的handler。
重新注册rtmp连接的读写事件处理handler是在ngx_rtmp_cycle当中完成的。
static void
ngx_rtmp_handshake_done(ngx_rtmp_session_t *s)
{
/* 释放三次握手申请的内存 */
ngx_rtmp_free_handshake_buffers(s);
ngx_log_debug0(NGX_LOG_DEBUG_RTMP, s->connection->log, 0,
"handshake: done");
/* 进入NGX_RTMP_HANDSHAKE_DONE 回调函数中, 这个地方的rtmp handler只有relay模块注册了就是ngx_rtmp_relay_handshake_done
* 但是不会进入,因为当前连接不是relay 连接,ngx_rtmp_relay_handshake_done只处理relay的情况
* /
if (ngx_rtmp_fire_event(s, NGX_RTMP_HANDSHAKE_DONE,
NULL, NULL) != NGX_OK)
{
ngx_rtmp_finalize_session(s);
return;
}
/* 进入*/
ngx_rtmp_cycle(s);
}
void
ngx_rtmp_cycle(ngx_rtmp_session_t *s)
{
ngx_connection_t *c;
c = s->connection;
/* 挂载rtmp三次握手后的读写事件handler */
c->read->handler = ngx_rtmp_recv;
c->write->handler = ngx_rtmp_send;
s->ping_evt.data = c;
s->ping_evt.log = c->log;
s->ping_evt.handler = ngx_rtmp_ping;
ngx_rtmp_reset_ping(s);
ngx_rtmp_recv(c->read);
}
ngx_rtmp_recv 主要是接受来自客户端的rtmp消息请求一般connect,creatStream,play,publish等请求,如果推流的话,后面基本一直接受音视频数据
ngx_rtmp_send 主要是给客户端发送rtmp响应消息窗口确认,以及后续play请求的话,就是给播放端一直发送音视频消息
以上就是rtmp连接建立到握手过程, 整体大概就是从监听配置解析–创建监听socket相关地址信息–accpet连接处理–rtmp连接创建–rtmp三次握手