• Nginx-rtmp点播之complex handshake


    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

    image

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

  • 相关阅读:
    python os.path模块常用方法详解
    PHP脚本执行效率性能检测之WebGrind的使用
    Laravel操作上传文件的方法
    Nginx获取自定义头部header的值
    Laravel Nginx 除 `/` 外所有路由 404
    laravel查看执行的sql语句
    laravel 安装excel扩展
    mysql 按值排序
    处理laravel表单提交默认将空值转为null的问题
    设置虚拟机里的Centos7的IP
  • 原文地址:https://www.cnblogs.com/jimodetiantang/p/8973724.html
Copyright © 2020-2023  润新知