模块功能描述
- 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超时断开机制