nginx-rtmp连接建立过程

连接建立整体过程:

  • 解析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三次握手

猜你喜欢

转载自blog.csdn.net/wu5215080/article/details/91817203