3、SRS4.0源代码分析之RTMP服务初始化与启动监听

目标:

    本章我们将分析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()。

下一章 4、SRS4.0源代码分析之RTMP推流处理

  • 6
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值