nginx_rtmp_relay_module模块解读

模块功能描述

  • push转推:支持同时多个push转推,并且有失败重推机制
  • pull回源拉流:
    • 回源拉流,只支持单个回源拉流,且拉流失败没有重试机制
    • 支持static预拉流,且有重试功能,进程起来就开始拉流

以下是模块源码里面给出的功能图,很直接清晰描述了push和pull的过程

在这里插入图片描述

源码解读

本模块的源码主要理解relay模块数据怎么组织设计,和对端服务rtmp交互相关逻辑,连接关闭回收

relay的数据结构

ngx_rtmp_relay_static_t结构体主要是预拉流的一些数据

typedef struct {
    ngx_rtmp_conf_ctx_t         cctx;   // 保存了整个配置文件的信息main,srv, app
    ngx_rtmp_relay_target_t    *target; // 回源相关的地址信息等
} ngx_rtmp_relay_static_t;

rtmp交互逻辑

relay模块和对端服务器交互主要rtmp协议三次握手,connect,createStream,play/publish命令消息

ngx_rtmp_relay_pull用来创建pull 请求,ngx_rtmp_relay_push是用来创建push请求,这里只分析play-relay回源的过程,push-publish是类似的

play回源请求流程分析

以下是主要的play-relay回源函数调用堆栈,先播放器端发起play—>ngx_rtmp_relay_play—>ngx_rtmp_relay_pull—>ngx_rtmp_relay_create—>握手–>connect–>createStream–>play–>一直接受音视频消息

  • ngx_rtmp_relay_create_connection 创建rtmp 连接,以及初始化rtmp session相关的操作
  • ngx_rtmp_client_handshake 作为客户端发起rtmp 三次握手相关的信息
  • ngx_rtmp_relay_send_connect 发送connect命令请求
  • ngx_rtmp_relay_send_create_stream 发送creaStream 消息
  • ngx_rtmp_relay_send_publish 发送publish消息请求
  • ngx_rtmp_relay_send_play 发送play消息请求

从播放器play到relay回源 准备三次握手的调用堆栈
在这里插入图片描述

static ngx_int_t
ngx_rtmp_relay_play(ngx_rtmp_session_t *s, ngx_rtmp_play_t *v)
{
    ········
    ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_relay_module);
    /* 如果是回源relay 不会再进入直接next */
    if (ctx && s->relay) {
        goto next;
    }

    racf = ngx_rtmp_get_module_app_conf(s, ngx_rtmp_relay_module);
    /* 根据racf->pulls的个数来判断是否去回源,为0 的时候next*/
    if (racf == NULL || racf->pulls.nelts == 0) {
        goto next;
    }

    name.len = ngx_strlen(v->name);
    name.data = v->name;

    t = racf->pulls.elts;

    for (n = 0; n < racf->pulls.nelts; ++n, ++t) {
        target = *t;

        if (target->name.len && (name.len != target->name.len ||
            ngx_memcmp(name.data, target->name.data, name.len)))
        {
            continue;
        }
        /* 这里其实有个bug, 无论你配置多少个pull,其实都只回第一个拉流,第一个拉流失败直接,跳出这个循环不再进行下个pull,回源也就失败了 */
        if (ngx_rtmp_relay_pull(s, &name, target) == NGX_OK) {
            goto next;
        }

        ngx_log_error(NGX_LOG_ERR, s->connection->log, 0,
                "relay: pull failed name='%V' app='%V' "
                "playpath='%V' url='%V'",
                &name, &target->app, &target->play_path,
                &target->url.url);
    }

next:
    return next_play(s, v);
}

ngx_rtmp_relay_create函数当中主要是创建当前session的ctx和对端远程回源/向对端publishsession的ctx,然后用链表和数组存放所有的publish和play信息

static ngx_int_t
ngx_rtmp_relay_create(ngx_rtmp_session_t *s, ngx_str_t *name,
        ngx_rtmp_relay_target_t *target,
        ngx_rtmp_relay_create_ctx_pt create_publish_ctx,
        ngx_rtmp_relay_create_ctx_pt create_play_ctx)
{
    ngx_rtmp_relay_app_conf_t      *racf;
    ngx_rtmp_relay_ctx_t           *publish_ctx, *play_ctx, **cctx;
    ngx_uint_t                      hash;


    racf = ngx_rtmp_get_module_app_conf(s, ngx_rtmp_relay_module);
    if (racf == NULL) {
        return NGX_ERROR;
    }
    /* 创建本地的ctx,publish_ctx是创建对端session的ctx */
    play_ctx = create_play_ctx(s, name, target);
    if (play_ctx == NULL) {
        return NGX_ERROR;
    }

    /* 注意这个地方根据流名进行hash保存每路流的relay_ctx放在racf->ctx中数组中,用来保存publish和对应publish的play的 
     * racr->ctx是在配置ngx_rtmp_relay_merge_app_conf的时候进行初始化的,数组个数是配置的buckets值1024
     * 这个值主要用来记录当前服务所有流的publish和play链表值
     * publish 是指数据流入,play数据流出。push情况是指publish session的ctx,pull情况是指relay 回源session的ctx
     * /
    hash = ngx_hash_key(name->data, name->len);
    cctx = &racf->ctx[hash % racf->nbuckets];
    for (; *cctx; cctx = &(*cctx)->next) {
        if ((*cctx)->name.len == name->len
            && !ngx_memcmp(name->data, (*cctx)->name.data,
                name->len))
        {
            break;
        }
    }
    /* 同一路流,如果有多个play请求的话,之前已经请求过,就直接把当前play的ctx头插法的方式放到publish_ctx->play的链表头 ,然后返回*/
    if (*cctx) {
        play_ctx->publish = (*cctx)->publish;
        play_ctx->next = (*cctx)->play;
        (*cctx)->play = play_ctx;
        return NGX_OK;
    }
    /* 创建publish的ctx, pull的情况就是远端relay的ctx,push情况就是推流的那个session 的ctx */
    publish_ctx = create_publish_ctx(s, name, target);
    if (publish_ctx == NULL) {
        ngx_rtmp_finalize_session(play_ctx->session);
        return NGX_ERROR;
    }
    /* 给publish_ctx的publish赋值,并且将play_ctx放到publish_ctx的链表当中,并且给play_ctx的publish赋值。
     * 注意区分这个链表节点是放在哪个链表当中,play_ctx和publish_ctx都有publish和play两个指针变量。
     * 就是把paly播放的链表全放到publish_ctx的play链表当中, pull的情况publish_ctx是回源连接,push的情况,publish_ctx是推流session的ctx
     */
    publish_ctx->publish = publish_ctx;
    publish_ctx->play = play_ctx;
    play_ctx->publish = publish_ctx;
    *cctx = publish_ctx;

    return NGX_OK;
}

ngx_rtmp_relay_on_result主要用来接收对端服务端发送的amf消息包,然后按正常rtmp协议请求进行下一步交互,这个主要是当前服务端作为客户端发起远程rtmp请求流程,
这个可以是push推流请求,也可以是pull 回源play拉流请求

static ngx_int_t
ngx_rtmp_relay_on_result(ngx_rtmp_session_t *s, ngx_rtmp_header_t *h,
        ngx_chain_t *in)
{
    ngx_rtmp_relay_ctx_t       *ctx;
    
   ......

    ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_relay_module);
    if (ctx == NULL || !s->relay) {
        return NGX_OK;
    }
    /* 接受对端发送amf包,解析amf包消息 */
    ngx_memzero(&v, sizeof(v));
    if (ngx_rtmp_receive_amf(s, in, in_elts,
                sizeof(in_elts) / sizeof(in_elts[0])))
    {
        return NGX_ERROR;
    }

    /* 解析得到消息类型后,按不同消息发送下一步的rtmp交互信息 */
    switch ((ngx_int_t)v.trans) {
        case NGX_RTMP_RELAY_CONNECT_TRANS:
            /* 发送connect 成功, 发送createStream命令 */
            return ngx_rtmp_relay_send_create_stream(s);

        case NGX_RTMP_RELAY_CREATE_STREAM_TRANS:
            /* 如果是push的情况,发送createStream命令成功, 发送publish消息请求 */
            if (ctx->publish != ctx && !s->static_relay) {
                if (ngx_rtmp_relay_send_publish(s) != NGX_OK) {
                    return NGX_ERROR;
                }
                return ngx_rtmp_relay_play_local(s);
             /* 如果是pull的情况, 发送createStream命令成功, 发送play消息请求 */
            } else {
                if (ngx_rtmp_relay_send_play(s) != NGX_OK) {
                    return NGX_ERROR;
                }
                return ngx_rtmp_relay_publish_local(s);
            }

        default:
            return NGX_OK;
    }
}

发起完握手请求之后,然后是根据接受来自服务端的请求信息,依次发送connect–>play->createStream等消息,下面只列举play的请求堆栈

注意理解connect是怎么发送的,connect事件在握手完成之后开始发送的位于.ngx_rtmp_relay_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;
    ngx_rtmp_amf_handler_t             *ch;

    cmcf = ngx_rtmp_conf_get_module_main_conf(cf, ngx_rtmp_core_module);

    /* 在解析配置文件的时候,relay模块注册握手完成的handler */
    h = ngx_array_push(&cmcf->events[NGX_RTMP_HANDSHAKE_DONE]);
    *h = ngx_rtmp_relay_handshake_done;

    ·········
}

ngx_rtmp_relay_handshake_done主要是发送connect请求,ngx_rtmp_relay_on_result根据

在这里插入图片描述
下面是发送play的函数堆栈,ngx_rtmp_receive_message会根据接受到消息类型进入不同的事件函数当中
在这里插入图片描述

static预拉流

static静态模式回源拉流是指worker进程启动的时候开始拉流。

application live {
        live on;;
       pull rtmp://test.com:4935/live name=test static;
}

1)配置文件中pull后面参数当中加上static 并且配置app和name参数,app和name参数可以放到url,支持配置多个pull

支持这种方式    pull rtmp://test.com:4935/live/test static;

2)程序启动解析配置文件,ngx_rtmp_relay_push_pull函数会根据配置解析static,注册static预拉流事件并放到一个动态数组当中,

动态数组是存在本模块的app配置文件里面。
static char *
ngx_rtmp_relay_push_pull(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
{
    ······
    /* 根据配置文件中的pull 后面带的变量static来区分是否需要预拉流, is_static表示加了static */ 

    if (is_static) {
        /* 只有pull的情况, push不允许 */
        if (!is_pull) {
            ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
                               "static push is not allowed");
            return NGX_CONF_ERROR;
        }
        /* 配置static需要app,name参数配合使用,name表示流名,预拉流需要知道app/name*/
        if (target->name.len == 0) {
            ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
                               "stream name missing in static pull "
                               "declaration");
            return NGX_CONF_ERROR;
        }
        /* 讲当前需要预拉流的流注册事件,并且放到racf->static_events 数组当中 */
        ee = ngx_array_push(&racf->static_events);
        if (ee == NULL) {
            return NGX_CONF_ERROR;
        }

        /* 给预拉流事件和预拉流结构体数据进行分配内存和初始化 */
        e = ngx_pcalloc(cf->pool, sizeof(ngx_event_t));
        if (e == NULL) {
            return NGX_CONF_ERROR;
        }

        *ee = e;

        rs = ngx_pcalloc(cf->pool, sizeof(ngx_rtmp_relay_static_t));
        if (rs == NULL) {
            return NGX_CONF_ERROR;
        }
        /* target 是回源相关的url地址相关信息 */
        rs->target = target;

        /* 预拉流事件的回调handler,预拉流失败的情况下会加定时器一直重试 */
        e->data = rs;
        e->log = &cf->cycle->new_log;
        e->handler = ngx_rtmp_relay_static_pull_reconnect;

        t = ngx_array_push(&racf->static_pulls);

    } 
    ······

}

3)ngx_rtmp_relay_init_process主要是从本模块app配置文件中,取出之前解析数组里面的tatic pull事件,将事件放到post事件队列里面。

注意:

1)明白这个加入的事件队列ngx_rtmp_init_queue的这些事件是什么时候执行? ngx_rtmp_init_queue是一个post事件队列,会专门写篇文章介绍

2)ngx_rtmp_relay_init_process 是worker进程启动初始化本模块相关的一些操作,是在解析配置文件之后启动worker的时候会给worker初始化

有关nginx事件介绍可以看这篇文章:https://blog.csdn.net/wu5215080/article/details/90451792

static ngx_int_t
ngx_rtmp_relay_init_process(ngx_cycle_t *cycle)
{
#if !(NGX_WIN32)
   ·····

    if (cmcf == NULL || cmcf->servers.nelts == 0) {
        return NGX_OK;
    }

    /* only first worker does static pulling */

    if (ngx_process_slot) {
        return NGX_OK;
    }

    pcscf = cmcf->servers.elts;
    for (n = 0; n < cmcf->servers.nelts; ++n, ++pcscf) {

        cscf = *pcscf;
        pcacf = cscf->applications.elts;

        for (m = 0; m < cscf->applications.nelts; ++m, ++pcacf) {

            cacf = *pcacf;
           /* 找到本模块app的relay 配置,将放在配置文件动态数组当中static pull事件取出来 */
            racf = cacf->app_conf[ngx_rtmp_relay_module.ctx_index];
            pevent = racf->static_events.elts;

            for (k = 0; k < racf->static_events.nelts; ++k, ++pevent) {
                event = *pevent;

                rs = event->data;
                rs->cctx = *cscf->ctx;
                rs->cctx.app_conf = cacf->app_conf;
                /* 将事件取出来之后,放到ngx_rtmp_init_queue事件队列中 */
                ngx_post_event(event, &ngx_rtmp_init_queue);
            }
        }
    }
#endif
    return NGX_OK;
}

static void
ngx_rtmp_relay_static_pull_reconnect(ngx_event_t *ev)
{
    ngx_rtmp_relay_static_t    *rs = ev->data;

    ngx_rtmp_relay_ctx_t       *ctx;
    ngx_rtmp_relay_app_conf_t  *racf;

    racf = ngx_rtmp_get_module_app_conf(&rs->cctx, ngx_rtmp_relay_module);

    /* 创建 rtmp请求连接和session 信息,以及发送rtmp请求 */
    ctx = ngx_rtmp_relay_create_connection(&rs->cctx, &rs->target->name,
                                           rs->target);
    if (ctx) {
        /* static_relay表示当前是预拉流的session */
        ctx->session->static_relay = 1;
        ctx->static_evt = ev;
        return;
    }
    /* pull_reconnect 默认3s, 3s后重新*/
    ngx_add_timer(ev, racf->pull_reconnect);
}

以下是预拉流的一些堆栈调用
在这里插入图片描述

如果没有拉到流,默认是3s重试
在这里插入图片描述

relay模块相关资源回收

relay模块资源的回收主要集中在ngx_rtmp_relay_close函数当中,本模块的资源回收主要包过以下几点:

  • 定时器事件删除:
  • 预拉流事件加定时重连
  • 连接的关闭回收:这个是难点也容易出错,重点区分ctx->publish 和ctx->play这俩指针变量以及当前ctx的关系
    • publish推流session,ctx和ctx->publish是一致
    • 回源relay session的ctx和ctx->publish是一致
    • 其他push和play的session的ctx和ctx->publish是不一致
    • 所有的play和push的连接通过ctx->publish链表头插法串联起来的
static void
ngx_rtmp_relay_close(ngx_rtmp_session_t *s)
{

   ······
    /* 预拉流的连接关闭会加定时器重连 */
    if (s->static_relay) {
        ngx_add_timer(ctx->static_evt, racf->pull_reconnect);
    }
    /* relay_close 可能会多次调用,ctx->publish 为null 说明可能已经释放了不需要再继续释放 */
    if (ctx->publish == NULL) {
        return;
    }
    
    /* 这里要细分一下场景,
    *  配置pull情况的时候,回收播放器端play会进入这里
    * 
    *  配置push情况,回收push的连接会进入这个逻辑
    * /
    /* play end disconnect? */
    if (ctx->publish != ctx) {
        /* 如果是play播放连接断开了,将ctx从链表当中去除,和之前ngx_rtmp_relay_create加入链表要互相有,有加入就有删除 */
        for (cctx = &ctx->publish->play; *cctx; cctx = &(*cctx)->next) {
            if (*cctx == ctx) {
                *cctx = ctx->next;
                break;
            }
        }

        ······

        /* push reconnect  这个是push的情况,如果push转推的连接断开了,定时重连 */
        if (s->relay && ctx->tag == &ngx_rtmp_relay_module &&
            !ctx->publish->push_evt.timer_set)
        {
            ngx_add_timer(&ctx->publish->push_evt, racf->push_reconnect);
        }
        ······

        /* 这个是分析pull的情况,如果回源relay没人观看了,关闭正在回源的连接 */
        if (ctx->publish->play == NULL && ctx->publish->session->relay) {
            ngx_log_debug2(NGX_LOG_DEBUG_RTMP,
                 ctx->publish->session->connection->log, 0,
                "relay: publish disconnect empty app='%V' name='%V'",
                &ctx->app, &ctx->name);
            ngx_rtmp_finalize_session(ctx->publish->session);
        }
        /* 最后记住这个ctx->publish 也要置null防止野指针释放 */
        ctx->publish = NULL;

        return;
    }

    /* 以下这些逻辑主要是回收推流的session和relya回源的session。此时的ctx和ctx->publish是同一个值
     * 推流断了的情况下下,push转推不需要再继续重连了,删除转推重连的定时器事件
     * /
    if (ctx->push_evt.timer_set) {
        ngx_del_timer(&ctx->push_evt);
    }

    /* 如果是relay 回源session断了,将播放客户端play连接全关掉,并且将每个play的oublish全置位null,这样不会重复释放*/
    for (cctx = &ctx->play; *cctx; cctx = &(*cctx)->next) {
        (*cctx)->publish = NULL;
        ngx_log_debug2(NGX_LOG_DEBUG_RTMP, (*cctx)->session->connection->log,
            0, "relay: play disconnect orphan app='%V' name='%V'",
            &(*cctx)->app, &(*cctx)->name);
        ngx_rtmp_finalize_session((*cctx)->session);
    }
    /* 当前本次的publish指针也置位null,连接都释放了,防止野指针的情况 */
    ctx->publish = NULL;

    /* 主要讲当前流名的relay的ctx从配置文件当中数组里面移除 */
    hash = ngx_hash_key(ctx->name.data, ctx->name.len);
    cctx = &racf->ctx[hash % racf->nbuckets];
    for (; *cctx && *cctx != ctx; cctx = &(*cctx)->next);
    if (*cctx) {
        *cctx = ctx->next;
    }
}

模块改进:

从直播CDN系统整体可用上分析:改模块有改进的空间

1)回源失败无法切主备

2)没有回源切换重试机制,可以根据配置的pull回源个数进行切主备重试机制,以及relay超时断开机制

猜你喜欢

转载自blog.csdn.net/wu5215080/article/details/90671369
今日推荐