目标:
本章我们将分析SRS4.0 RTMP服务模块的初始化和启动部分的代码,通过学习,将了解SRS软件针对TCP端口监听,客户端连接建立的一般性处理流程。以及和RTMP相关的握手、connect请求、推拉流请求等协议处理逻辑。
内容:
根据上节内容可知,SRS4.0 RTMP服务模块对应的类对象是SrsServerAdapter,SrsServerAdapter有SrsServerAdapter::initialize()和SrsServerAdapter::run()两个入口函数,RTMP服务模块的启动工作主要在SrsServerAdapter::run()接口中完成。
srs_error_t SrsServerAdapter::run() {
// SrsServerAdapter对象内部管理了SrsServer对象,实际工作由SrsServer对象完成
// SrsServer包括了RTMP/HTTP/HTTP-API等几乎所有主要功能
srs->initialize(NULL); // 此函数内部主要是调用SrsHttpServeMux和SrsHttpServer对象的初始化
// SrsHttpServeMux负责HTTP API的注册和处理
// SrsHttpServer内部包括SrsHttpStaticServer和SrsHttpStreamServer对象
// SrsHttpStaticServer提供静态文件的读取服务
// SrsHttpStreamServer提供http流式FLV/TS/MP3/AAC数据服务
srs->initialize_st(); // 此函数作用不大,可以暂不关注
srs->acquire_pid_file(); // pid文件用于防止SRS服务进程被多次重复启动,只有获得特定pid文件
//(固定路径和文件名)的写入权限(F_WRLCK)的进程才能正常启动
// 并将自身的进程PID写入该文件,其它同一程序的多余进程则自动退出
srs->initialize_signal(); // 内部调用pipe()函数创建读写管道,此管道用于传递信号(signal)
srs->listen(); // 启动监听RTMP、HTTP-API、HTTP STREAM等流服务
// 此函数需要重点分析
srs->register_signal(); // 此函数内部使用sigemptyset和sigaction注册信号处理函数sig_catcher()
// 信号处理函数sig_catcher()内部在接收到系统signal时,将signal写入前面创建的pipe管道
srs->http_handle(); // 此函数内部,将字符串形式的HTTP API和对应的处理函数注册到SrsHttpServeMux对象内,
// 后续,当SrsHttpServeMux收到某个API请求后,则调用对应的处理函数
srs->ingest(); // 启动拉取服务SrsIngester对象的内部工作协程,用于从文件、流、设备中拉取音视频流
srs->start(); // 这里的工作包括:
// 1)初始化SrsLiveSourceManager对象,SrsLiveSourceManager主要工作是通过周期性轮询检测
// 定时清理长期没有有效数据SrsLiveSource对象
// 2)启动SrsServer的内部协程,此协程的主要工作是轮询信号标志,重新加载新配置文件
// 3)调用SrsServer::setup_ticks()创建SrsServer内部周期定时器、注册定时器事件并启动定时器协程
// 在函数SrsServer::notify()内部,执行上述周期定时器的超时处理
}
根据上面的分析,接下来重点分析SrsServer::listen()函数(在srs_app_server.cpp文件里)
srs_error_t SrsServer::listen() {
listen_rtmp(); // 监听RTMP,端口信息由_srs_config->get_listens()函数从配置文件获取
// 创建一个监听对象SrsBufferListener,此对象内部还有一个SrsTcpListener对象
// 监听过程详见SrsBufferListener::listen()->SrsTcpListener::listen()
// 每个SrsTcpListener内部有个协程SrsTcpListener::cycle()负责监听
// 该协程以阻塞方式调用srs_accept(),接收到新的客户端连接后,调用
// SrsBufferListener::on_tcp_client() -> SrsServer::accept_client()
// 调用SrsServer::fd_to_resource()生成RTMP连接对象SrsRtmpConn
// 调用SrsResourceManager::add(conn)将SrsRtmpConn添加到资源管理器
// 调用SrsRtmpConn::start(),启动每个连接的协程SrsRtmpConn::do_cycle()
listen_http_api(); // 监听HTTP API,
listen_https_api(); // 监听HTTPS API
// 端口信息由_srs_config->get_http_api_listen()函数从配置文件获取
// 内部也同样创建SrsBufferListener对象和SrsTcpListener对象
// SrsTcpListener内部协程SrsTcpListener::cycle()负责监听
// 监听过程完全一致,区别在于接收到新连接后,
// 调用SrsServer::fd_to_resource()生成SrsHttp(s)Api连接对象
listen_http_stream(); // 监听HTTP
listen_https_stream(); // 监听HTTPS
// 内部创建监听对象SrsBufferListener,监听过程完全一致,区别在于接收到新连接后,
// 调用SrsServer::fd_to_resource()生成SrsResponseOnlyHttpConn连接对象
listen_stream_caster(); // 根据配置,内部创建SrsUdpCasterListener监听对象
// SrsUdpCasterListener / SrsHttpFlvListener / SrsGb28181Manger
// 这一块和RTMP本身关系不大,属于兼容多种流媒体协议,以后再单独分析
conn_manager->start(); // 启动连接资源管理器SrsResourceManager对应的协程
// 此协程内部路径比较简单,就是等待条件变量唤醒,然后清除僵尸连接
}
根据上面的分析,RTMP模块的服务端口监听过程,主要是通过SrsBufferListener和SrsTcpListener对象以及SrsTcpListener::cycle()监听协程完成的,这块代码主要是针对state-thread库进行接口封装的基础代码,和协议处理本身的关系不大,读者可以自行分析。接下来分析,SrsTcpListener::cycle()监听协程接收到一条新连接后调用SrsServer::accept_client()函数的处理过程
// RTMP模块,接收到客户端新建连接请求的处理函数
srs_error_t SrsServer::accept_client(SrsListenerType type, srs_netfd_t stfd)
{
ISrsStartableConneciton* conn = NULL;
// 调用SrsServer::fd_to_resource(),根据SrsListenerType参数,生成不同的连接对象
// SrsRtmpConn、SrsHttpApi、SrsResponseOnlyHttpConn
if ((err = fd_to_resource(type, stfd, &conn)) != srs_success) {
return srs_error_wrap(err, "fd to resource");
}
conn_manager->add(conn); // 将连接对象conn加入SrsResourceManager统一管理
// 启动连接对象conn内部的协程
// 1)SrsRtmpConn对象启动的协程是SrsRtmpConn::do_cycle()
// 2)SrsHttpApi、SrsResponseOnlyHttpConn连接对象内部又有一个SrsHttpConn对象,
// 所以最终启动的协程是SrsHttpConn::do_cycle()
if ((err = conn->start()) != srs_success) {
return srs_error_wrap(err, "start conn coroutine");
}
return srs_success;
}
在SrsServer::fd_to_resource()函数内部,根据客户端连接类型,创建不同的连接对象
srs_error_t SrsServer::fd_to_resource(SrsListenerType type, srs_netfd_t stfd, ISrsStartableConneciton** pr)
{
int fd = srs_netfd_fileno(stfd); // 获取新连接的socket描述符
string ip = srs_get_peer_ip(fd); // 获取新连接的对端IP
int port = srs_get_peer_port(fd); // 获取新连接的对端端口号
// 根据SrsListenerType,参加连接对象SrsRtmpConn、SrsHttpApi或SrsResponseOnlyHttpConn
// SrsRtmpConn处理RTMP客户端的推拉流请求
// SrsHttpApi处理用户通过http接口发送的HTTP-API请求
// SrsResponseOnlyHttpConn处理用户通过http接口发送的普通HTTP请求(请求普通文件)
if (type == SrsListenerRtmpStream) {
*pr = new SrsRtmpConn(this, stfd, ip, port);
} else if (type == SrsListenerHttpApi) {
*pr = new SrsHttpApi(false, this, stfd, http_api_mux, ip, port);
} else if (type == SrsListenerHttpsApi) {
*pr = new SrsHttpApi(true, this, stfd, http_api_mux, ip, port);
} else if (type == SrsListenerHttpStream) {
*pr = new SrsResponseOnlyHttpConn(false, this, stfd, http_server, ip, port);
} else if (type == SrsListenerHttpsStream) {
*pr = new SrsResponseOnlyHttpConn(true, this, stfd, http_server, ip, port);
}
return srs_success;
}
SrsResourceManager对象的协程,等待被条件变量唤醒,然后清除僵尸连接
srs_error_t SrsResourceManager::cycle()
{
srs_error_t err = srs_success;
while (true) {
// 检测,如果协程有错误,立刻退出
if ((err = trd->pull()) != srs_success) {
return srs_error_wrap(err, "conn manager");
}
// 清除僵尸队列中的僵尸连接
while (!zombies_.empty()) { clear(); }
srs_cond_wait(cond); // 协程阻塞等待下一次条件变量被触发
}
return err;
}
根据上面得到的几种连接对象SrsRtmpConn、SrsHttpApi、SrsResponseOnlyHttpConn可知:
1)如果要分析RTMP推拉流的处理,重点看SrsRtmpConn对象的实现。
2)如果要分析SRS对外提供HTTP API的机制,应该重点看SrsHttpApi和SrsHttpConn的实现。
3)如果要分析SRS的HTTP-Flv处理机制,应该重点看SrsResponseOnlyHttpConn和SrsHttpConn的实现。
对于初学者,我们重点分析RTMP推拉流的处理(即SrsRtmpConn::do_cycle()协程的处理逻辑),至于HTTP API的实现机制和HTTP-Flv处理机制将放在单独的章节分析。
SrsRtmpConn::do_cycle()协程,主要完成RTMP协议的(1)握手过程;(2)建立连接;(3)请求推(拉)流,具体处理如下:
srs_error_t SrsRtmpConn::cycle()
{
do_cycle();
SrsResourceManager::remove(this); // 执行到这里,表示连接线程处理结束,需要执行连接中断处理
}
srs_error_t SrsRtmpConn::do_cycle()
{
rtmp->set_recv_timeout(SRS_CONSTS_RTMP_TIMEOUT); // 设置接收、发送的超时时间
rtmp->set_send_timeout(SRS_CONSTS_RTMP_TIMEOUT);
rtmp->handshake(); // 服务端处理RTMP握手过程
rtmp->connect_app(req); // 服务端接收处理connect消息。
// 此函数用于处理客户端发送的connect消息
// 关于connect消息,可以参考:
// https://cloud.tencent.com/developer/inventory/1220/article/1630593
// 其中,比较重要的信息就是推拉流URL(rtmp://192.168.1.170/live/livestream)
service_cycle(); // 此函数内部是真正的RTMP协议处理循环
on_disconnect(); // 执行到这里,表示协议处理循环结束,执行连接中断的处理,
// 主要就是通过callback接口向外通知当前的RTMP连接结束
return err;
}
客户端与服务器之间的协议交换过程如下:
服务器接收connect消息成功后,在SrsRtmpConn::service_cycle()函数中:
1、向客户端发送Window Acknowledgement Size消息并设置本端的in_window_ack_size参数
2、向客户端发送Set Peer Bandwidth消息。
3、最终向客户端发送针对connect请求的响应。
会话开始时,双方都要向对端发送Window Acknowledgement Size。后续过程中,当一端接收的总数据超过Window Acknowledgement Size时,就要向对方发送一次Ack,否则对端就不再发新数据了,所以,这个过程有点流控的意思。
srs_error_t SrsRtmpConn::service_cycle()
{
rtmp->set_window_ack_size(); //此参数表示本端发送多少报文后需要对端回应ACK响应
rtmp->set_in_window_ack_size(); //此参数表示本端收到多少报文后发送ACK响应
rtmp->set_peer_bandwidth(); // 设置对端输出带宽
bandwidth->bandwidth_check(); // 这里根据配置文件决定是否执行带宽检测(通过发送接收测试报文失效检测)
rtmp->set_chunk_size(); // 这里将RTMP报文的chunk值调整到60000,方便后续协商
rtmp->response_connect_app(); // 对客户端发来的connect消息做出响应
rtmp->on_bw_done(); // 服务器完成带宽检测后,调用此函数,向客户端发送检测结果
while (true) {
stream_service_cycle(); // 开始处理RTMP推拉流请求
}
}
接下来在SrsRtmpConn::stream_service_cycle()函数中,根据接收到的报文,区分推流端和拉流端,完成推、拉流的创建过程
srs_error_t SrsRtmpConn::stream_service_cycle(){
rtmp->identify_client(&info->type); // 这个函数比较迷惑,它的实际工作大概是:
// 不同厂家的RTMP协议实现有区别,此函数内部根据接收到的不同请求报文,识别并适配不同的客户端类型
// 对于推流端,每个推流端通过fetch_or_create函数生成一个对应的SrsLiveSource对象
// 对于拉流端,
// 1)如果本机已经有对应推流端的SrsLiveSource对象,则根据拉流地址字符串(/live/stream)直接获取
// 2)如果本机没有对应推流端的SrsLiveSource对象,这里则会创建一个SrsLiveSource对象
// 并在后续的SrsRtmpConn::playing()函数中,根据edge模式有一个回源拉流的操作
_srs_sources->fetch_or_create(req, server, &source);
switch (info->type) {
case SrsRtmpConnPlay: // 拉流客户端类型
rtmp->start_play(info->res->stream_id); // 发送拉流响应报文
http_hooks_on_play(); // 客户端拉流开始,执行HTTP回调处理
playing(source); // 拉流处理逻辑
http_hooks_on_stop(); // 客户端拉流结束,执行HTTP回调处理
return err;
case SrsRtmpConnFMLEPublish: // 标准FFMPEG推流端类型
rtmp->start_fmle_publish(); // 发送推流响应报文
return publishing(source); // 推流处理逻辑
case SrsRtmpConnHaivisionPublish: // 海康设备推流
rtmp->start_haivision_publish(); // 发送推流响应报文
return publishing(source); // 推流处理逻辑
case SrsRtmpConnFlashPublish: // flash推流
rtmp->start_flash_publish(); // 发送推流响应报文
return publishing(source); // 推流处理逻辑
}
}
例如处理releaseStream、FCPublish、createStream这些无聊的协议交互,具体都放在SrsRtmpServer::identify_client()函数中处理:
srs_error_t SrsRtmpServer::identify_client(int stream_id, SrsRtmpConnType& type, string& stream_name, srs_utime_t& duration)
{
type = SrsRtmpConnUnknown;
srs_error_t err = srs_success;
while (true) {
SrsCommonMessage* msg = NULL;
// 此函数内部识别并组装chunk报文,得到RTMP Message数据包
if ((err = protocol->recv_message(&msg)) != srs_success) {
return srs_error_wrap(err, "recv identify message");
}
SrsMessageHeader& h = msg->header;
if (h.is_ackledgement() || h.is_set_chunk_size() || h.is_window_ackledgement_size() || h.is_user_control_message()) {
continue; // 如果是chunk层控制命令,则不处理,继续在本函数内循环接收新报文
}
if (!h.is_amf0_command() && !h.is_amf3_command()) {
srs_trace("ignore message type=%#x", h.message_type);
continue; // 如果不是17或20类型的AMF编码格式的RTMP层协议命令,则不处理,继续循环接收新报文
}
//只针对下面三种报文执行响应后函数返回,继续向后处理
// 1)如果收到SrsCreateStreamPacket报文,则回送一个SrsCreateStreamResPacket响应报文,
// 并继续等待接收对端消息,确定客户端是推流或拉流类型
// 2)如果收到SrsFMLEStartPacket报文,则设置连接类型为SrsRtmpConnFMLEPublish推流类型
// 3)如果收到SrsPlayPacket报文,则设置连接类型为SrsRtmpConnPlay拉流类型
// 另外,从SrsProtocol::do_decode_message()函数中可知:
// 只有"releaseStream"/"FCPublish"/"FCUnpublish"请求被封装为SrsFMLEStartPacket
// "createStream"请求被封装为SrsCreateStreamPacket
// "play"请求被封装为SrsPlayPacket
if (dynamic_cast<SrsCreateStreamPacket*>(pkt)) {
return identify_create_stream_client(dynamic_cast<SrsCreateStreamPacket*>(pkt), stream_id, 3, type, stream_name, duration);
}
if (dynamic_cast<SrsFMLEStartPacket*>(pkt)) {
return identify_fmle_publish_client(dynamic_cast<SrsFMLEStartPacket*>(pkt), type, stream_name);
}
if (dynamic_cast<SrsPlayPacket*>(pkt)) {
return identify_play_client(dynamic_cast<SrsPlayPacket*>(pkt), type, stream_name, duration);
}
// 针对call命令,只做出响应,仍在当前函数循环接收新的请求报文
SrsCallPacket* call = dynamic_cast<SrsCallPacket*>(pkt);
if (call) {
SrsCallResPacket* res = new SrsCallResPacket(call->transaction_id);
protocol->send_and_free_packet(res, 0);
continue;
}
}
}
最终,回到SrsRtmpConn::stream_service_cycle()函数中,根据识别的不同客户端类型,执行不同的推拉流响应,最后进入SrsRtmpConn::publishing()或SrsRtmpConn::playing()函数开始正式的推拉音视频数据流。
srs_error_t SrsRtmpConn::stream_service_cycle(){
rtmp->identify_client(&info->type);
// 对于推流端,每个推流端通过fetch_or_create函数生成一个对应的SrsLiveSource对象
// 对于拉流端,
// 1)如果本机已经有对应推流端的SrsLiveSource对象,则根据拉流地址字符串(/live/stream)直接获取
// 2)如果本机没有对应推流端的SrsLiveSource对象,这里则会创建一个SrsLiveSource对象
// 并在后续的SrsRtmpConn::playing()函数中,根据edge模式有一个回源拉流的操作
_srs_sources->fetch_or_create(req, server, &source);
switch (info->type) {
case SrsRtmpConnPlay: // 拉流客户端类型
rtmp->start_play(info->res->stream_id); // 发送拉流响应报文
http_hooks_on_play(); // 客户端拉流开始,执行HTTP回调处理
playing(source); // 拉流处理逻辑
http_hooks_on_stop(); // 客户端拉流结束,执行HTTP回调处理
return err;
case SrsRtmpConnFMLEPublish: // 标准FFMPEG推流端类型
rtmp->start_fmle_publish(); // 发送推流响应报文
return publishing(source); // 推流处理逻辑
case SrsRtmpConnHaivisionPublish: // 海康设备推流
rtmp->start_haivision_publish(); // 发送推流响应报文
return publishing(source); // 推流处理逻辑
case SrsRtmpConnFlashPublish: // flash推流
rtmp->start_flash_publish(); // 发送推流响应报文
return publishing(source); // 推流处理逻辑
}
}
总结:
本章分析了RTMP服务模块的启动、监听服务端口的处理流程,过程中涉及的关键类和关键函数如下图所示。
接下来,将深入学习RTMP服务模块的推流处理流程SrsRtmpConn::publishing()和拉流处理流程SrsRtmpConn::playing()。