概述

在这个部分我希望能够用一张函数调用的流程图说明程序运行原理。同时我会将流程图中涉及到的一些设计方法和技术点拿出来说。庞大的派生类构成,复杂的日志系统,惊叹的解耦设计和安全设计,都让6.0版本的源码阅读成了一件难事。与之相比4.0版本的源码就显得亲切的多了。

基础类关系

  • ISrsConnection: 表明它是一个可被管理的连接资源,拥有ID (get_id())和IP地址(remote_ip())等基本属性。

  • ISrsStartable: 拥有 start() 方法,意味着它可以被启动(通常是启动其内部的协程)。

  • ISrsReloadHandler: 使其能够响应配置文件的热加载事件。

  • ISrsCoroutineHandler: 表明其核心逻辑 cycle() 是在协程中执行的。

  • ISrsExpire: 表明它有过期机制,可以被服务器主动清理。

  • SrsServer* server: 持有对顶层 Server 对象的引用,用于访问全局配置和资源。

  • SrsTcpConnection* skt: 持有底层的 TCP 套接字对象,负责网络数据的收发。

  • SrsCoroutine* trd: 每个连接都由一个协程驱动,trd 就是这个协程的实例。这是 SRS 高并发能力的关键。

  • SrsClientInfo* info: 包含了解析后的客户端基本信息。

  • SrsLiveSource (交互): 这是 SrsRtmpConn 最重要的交互对象。

    • 当 SrsRtmpConn 是一个发布者 (Publisher) 时,它会查找或创建一个 SrsLiveSource 实例,并将接收到的音视频消息 (SrsCommonMessage) 转发给 SrsLiveSource。

    • 当 SrsRtmpConn 是一个播放者 (Player) 时,它会查找一个已经存在的 SrsLiveSource,并从中获取音视频数据。

  • SrsLiveConsumer (创建与使用): 对于播放者连接,SrsRtmpConn 会创建一个 SrsLiveConsumer 并将其挂载到 SrsLiveSource 上,通过这个 consumer 来安全地消费数据。

  • SrsRequest* req / SrsResponse* res: 这两个对象(包含在 SrsClientInfo 中)用于处理 RTMP connect 命令中的详细请求信息和生成响应。

  • SrsPublishRecvThread* / SrsQueueRecvThread*: 这两个是用于在后台异步接收和处理消息的线程/协程封装,以优化性能和处理超时。

RTMP连接流程

  • SrsServerAdapter::run() 被调用。
  • rtmp_listener_->listen() 执行,调用至SrsTcpListener::listen(),接下来调用SrsTcpListener::cycle()。生成一个监听协程阻塞在 srs_accept(rtmp_port)。
  • srs->start() 执行,SrsLiveSourceManager 初始化完毕。
  • 此时,一个OBS客户端向SRS的1935端口发起连接。
  • srs_accept() 不再阻塞,返回一个新的客户端连接 fd。
  • handler->on_tcp_client(this, fd)监听协程被唤醒,它调用SrsMultipleTcpListeners::on_tcp_client(ISrsListener* listener, srs_netfd_t stfd),再到SrsServer::on_tcp_client,最后到::SrsServer::do_on_tcp_client,通过此创建真正的连接处理器。
  • accept_client() 内部创建一个 SrsRtmpConn 对象,专门用于处理这个新的OBS连接。
  • SrsRtmpConn::start() 被调用,它会为这个单独的连接再创建一个新的工作协程,这个协程执行 SrsRtmpConn::do_cycle()。
    在 do_cycle() 内部,开始进行RTMP握手、处理C1/S1/C2/S2消息、处理 connect、publish、play 等命令,并与 SrsLiveSourceManager 交互。srs中对于握手的处理采用的是rtmp complex handshake,具体要看文章
    接下来具体的RTMP流程推荐看这篇文章,写的已经是非常详细了,既然难以望其项背,就不班门弄斧了。下面只记录基本流程和我画的框图
  • SrsRtmpConn 的接收线程从 TCP socket 读取数据,并将其组装成一个 SrsCommonMessage。
  • 消息被送入 handle_publish_message 函数。
  • 如果是 unpublish 等控制信令,handle_publish_message 直接处理,并返回一个特定错误码 ERROR_CONTROL_REPUBLISH,导致推流循环结束,连接断开。
  • 如果是音视频或 Metadata 数据,handle_publish_message 将其“放行”,调用 process_publish_message。
  • process_publish_message 判断服务器角色:
    • 如果是边缘节点,将消息转发给源站。
    • 如果是源站,则根据消息类型(Audio/Video/Metadata),调用 SrsLiveSource 中对应的 on_audio, on_video 或 on_meta_data 方法。
  • SrsLiveSource 收到数据后,进行缓存(GOP Cache),并将其拷贝分发给所有正在等待拉流的 SrsConsumer(每个播放者都对应一个 Consumer)。
  • SrsConsumer 将消息放入自己的队列,最终由对应的 SrsRtmpConn 的发送线程发送给播放客户端。

多态程序调试技巧

我们以SRS中的trd->start()为例,几乎在各处都可以看到类似trd的成员函数调用,但是应该如何去找到它的实现呢?首先要确定它的静态类型,找到它的上下文中的所属类,转到这个类的声明,一般在private中就可以看到它的静态类型了,但是到这里我们还是没有办法确定它的函数定义的,原因在于实际定义可能在子类中(可以观察静态类型中的函数声明是否是纯虚函数)。接下来应该确定start究竟是哪个子类的函数,可以到所属类中的构造函数去找,观察trd的创建类型。再转到子类中的具体实现即可。在SRS中start函数一般都是用来创建协程,业务循环代码在cycle中。

自管道技术

在SrsServerAdapter::run()中定义了两个函数:srs->initialize_signal()与srs->register_signal();这是为了解决,当操作系统将一个信号发给进程时操作系统会中断进程的正常执行,去调用你预先注册的信号处理函数(Signal Handler)。而处理函数的上下文非常受限,只有异步安全信号才可以被调用。因此SRS会创建一个pipe,在其中有两个文件描述符(fd[0]\fd[1])前者用于读,后者用于写,然后注册一个信号处理器,像fd[1]中将捕获的信号写入。接下来创建新的阻塞监听fd[0]线程

PIMPL设计模式

1
2
3
4
SrsSTCoroutine::SrsSTCoroutine(string n, ISrsCoroutineHandler* h, SrsContextId cid)
{
impl_ = new SrsFastCoroutine(n, h, cid);//Pointer to Implementation PIMPL设计模式
}

SrsSTCoroutine是一个句柄类,SrsFastCoroutine是隐藏在内部的实现类(Implementation Class),包含了所有真正的成员变量和复杂的逻辑。impl_是SrsSTCoroutine内部的一个指针,它知道如何与SrsFastCoroutine通信。在构造函数中这样做可以实现完美的接口与实现分离。SrsSTCoroutine 的用户完全不需要知道 SrsFastCoroutine 的存在,他们只需要和简洁的公共接口打交道


本站由 Edison.Chen 创建。
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。

undefined