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);
}

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

猜你喜欢

转载自www.cnblogs.com/jimodetiantang/p/8973724.html