上一篇文章中,我向你介绍了 Medooze 的 SFU 模型、录制回放模型以及推流模型,并且还向你展示了Medooze的架构,以及Medooze核心组件的基本功能等相关方面的知识。通过这些内容,你现在应该已经对Medooze有了一个初步了解了。

不过,那些知识我们是从静态的角度讲解的 Medooze ,而本文我们再从动态的角度来分析一下它。讲解的主要内容包括:WebRTC 终端是如何与 Meooze 建立连接的、数据流是如何流转的,以及Medooze是如何进行异步I/O事件处理的。

异步I/O事件模型

异步I/O事件是什么概念呢?你可以把它理解为一个引擎或动力源,这里你可以类比汽车系统来更好地理解这个概念。

汽车的动力源是发动机,发动机供油后会产生动力,然后通过传动系统带动行车系统。同时,发动机会驱动发电机产生电能给蓄电池充电。汽车启动的时候,蓄电池会驱动起动机进行点火。

这里的汽车系统就是一种事件驱动模型,发动机是事件源,驱动整车运行。实际上,Medooze的异步I/O事件模型与汽车系统的模型是很类似的。那接下来,我们就看一下 Medooze 中的异步I/O事件驱动机制:

Medooze事件驱动机制图

上面这张图就是Modooze异步 I/O 事件处理机制的整体模型图,通过这张图你可以发现 poll 就是整个系统的核心。如果说 EventLoop 是 Medooze 的发动机,那么 EventLoop 中的 poll 就是汽缸,是动力的发源地。

下面我就向你介绍一下 poll 的基本工作原理。在Linux 系统中,无论是对磁盘的操作还是对网络的操作,它们都统一使用文件描述符fd来表示,一个fd既可以是一个文件句柄,也可以是一个网络socket的句柄。换句话说,我们只要拿到这个句柄就可以对文件或socket进行操作了,比如往fd中写数据或者从fd中读数据。

对于 poll 来说,它可以对一组 fd 进行监听,监听这些 fd 是否有事件发生,实际上就是监听fd是否可写数据读数据。当然,具体是监听读事件还是写事件或其他什么事件,是需要你告诉poll的,这样poll一旦收到对应的事件时,就会通知你该事件已经来了,你可以做你想要做的事儿了。

那具体该怎么做呢?我们将要监听的 fd 设置好监听的事件后,交给 poll,同时还可以给它设置一个超时时间,poll就开始工作了。当有事件发生的时候,poll 的调用线程就会被唤醒,poll重新得到执行,并返回执行结果。此时,我们可以根据poll的返回值,做出相应的处理了。

需要说明的是,poll API 本身是一个同步系统调用,当没有要监听的事件(I/O 事件 和 timer 事件)发生的时候,poll 的调用线程就会被系统阻塞住,并让它处于休眠状态,而它处理的fd是异步调用,这两者一定不要弄混了。

poll 的功能类似 select,其API原型如下:

include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);

其各个参数及返回值含义如下表所示:

其中第一个fds ,它表示的是监听的 pollfd 类型的数组,pollfd 类型结构如下:

struct pollfd {
  int   fd;         / file descriptor /
  short events;     / requested events /
  short revents;    / returned events /
};

下表是常见事件的说明:

了解了 poll API 各参数和相关事件的含义之后,接下来我们看一下当poll睡眠时,在哪些情况下是可以被唤醒的。poll 被唤醒的两个条件:

  1. 某个文件描述符有事件发生时;
  2. 超时事件发生。

以上就是异步I/O事件处理的原理以及 poll API 的讲解与使用。接下来我们再来看看 EventLoop 模块中其他几个类的功能及其说明:

ICE 连接建立过程

了解了Medooze的异步I/O事件处理机制后,接下来我们看一下 ICE 连接的建立。实际上,传输层各个组件的基本功能,我们在上篇文章中都已一一介绍过了,不过那都是静态的,本文我们从动态的角度,进一步了解一下 DTLS 连接的建立过程。

DTLS连接建立过程图

当某个参与人(Participator )加入房间后,需要建立一个 DTLS 连接使客户端与Medooze可以进行通信,上图描述了各个对象实例之间的先后调用关系。其具体连接建立过程说明如下(步骤较多,建议对照上图来理解和学习):

  1. 当有参与人(Participator)加入时,如果此时房间内只有一个人,则该 Participator 对象会调用 MediaServer::createEndpoint 接口,来创建一个接入点。
  2. MediaServer 对象会创建一个 Endpoint 实例。
  3. Endpoint 对象会创建一个 Native RTPBundleTransport 实例。
  4. Native RTPBundleTransport 实例创建好后,Endpoint 再调用 RTPBundleTransport 实例的init()方法对 RTPBundleTransport 实例进行初始化。
  5. 在 RTPBundleTransport 初始化过程中,会创建 UDP socket,动态绑定一个端口。这个动态端口的范围,用户是可以指定的。
  6. RTPBundleTransport 中包含了一个 EventLoop 实例,所以会创建事件循环实例,初始化 pipe。
  7. EvenLoop 会创建一个事件循环线程。
  8. 事件循环线程中会执行 poll,进入事件循环逻辑。此时,Endpoint 的初始过程算是完成了
  9. Participator 需要创建一个 DTLS 连接,DTLS 连接是由 Endpoint::createTransport 函数来创建的。
  10. Endpoint 创建一个 Transport 实例,此实例是 Native 的一个 wrapper。
  11. Transport 调用 RTPBundleTransport::AddICETransport 接口,准备创建 Native DTLS 实例。
  12. RTPBundleTransport 创建一个 DTLSICETransport 实例。
  13. DTLSICETransport 聚合了 DTLSConnection 实例,对其进行初始化。主要工作就是证书的生成、证书的应用、ssl 连接实例的创建,以及DTLS 的握手过程。
  14. DTLSICETransport 的初始化工作,包括 SRTPSession 的初始化和 DataChannel 的初始化。

经历这么一个过程,ICE 连接就建立好了,剩下就是媒体流的转发了。

SFU 数据包收发过程

当终端和 Medooze 建立好 DTLSICETransport 连接以后,对于需要共享媒体的终端来说,Medooze 会在服务端为它建立一个与之对应的 IncomingStream 实例,这样终端发送来的音视频流就进入到 IncomingStream中。

如果其他终端想观看共享终端的媒体流时,Medooze 会为观看终端创建一个 OutgoingStream 实例,并且将此实例与共享者的 IncomingStream 实例绑定到一起,这样从IncomingStream中收到的音视频流就被源源不断输送给了OutgoingStream。然后OutgoingStream又会将音视频流通过DTLSICETransport对象发送给观看终端,这样就实现了媒体流的转发。

RTP 数据包在 Medooze 中的详细流转过程大致如下图所示:

SFU 数据转发图

图中带有 in 标签的绿色线条表示共享者数据包流入的方向,带有 out 标签的深红色线条表示经过 Medooze 分发以后转发给观看者的数据流出方向,带有 protect_rtp/unprotect_rtp 标签的红色线条表示 RTP 数据包的加密、解码过程,带有 dtls 标签的紫色线条表示 DTLS 协议的协商过程以及DTLS 消息的相关处理逻辑。

上面的内容有以下四点需要你注意一下:

接下来,我们就对数据包的流转过程做一个简要的说明(步骤较多,建议对照上图来理解和学习):

小结

本文我们重点介绍了事件驱动机制,它像人的心脏一样,在现代服务器中起着至关重要的作用。 Medooze 服务器中的具体实现模块就是 EventLoop 模块。通过本文的学习,我相信你对事件驱动机制有了一定的了解。接下来,就需要结合代码做进一步的学习,最好能进行相应的项目实践,这样对你继续研读 Medooze 源码会有很大的帮助。

后半部分,我们还分别介绍了 DTLS 连接建立过程和数据包的流转过程。通过本文以及上一篇文章的学习,我相信你对 Medooze 的基础架构以及 SFU 的实现细节已经有了深刻的理解。

思考时间

今天你的思考题是:异步I/O事件中,什么进候会触发写事件?

欢迎在留言区与我分享你的想法,也欢迎你在留言区记录你的思考过程。感谢阅读,如果你觉得这篇文章对你有帮助的话,也欢迎把它分享给更多的朋友。

评论