流媒体学习之路(mediasoup)——Worker(c++)libuv(番外)
提示:
当mediasoup进程启动时,会检测Node.js服务器是否启动。随后初始化需要用到的各个模块。
分别启动:libUV、建立管道通信模块、读取config、OpenSSL模块、libSRTP模块、UsrSCTP模块、libWebRTC模块、Crypto加密模块、DtlsTransport传输模块、SrtpSession会议模块、两个业务channel模块。
下面对整体的Worker用到的开源模块进行分析对比:
文章目录
- 流媒体学习之路(mediasoup)——Worker(c++)libuv(番外)
- 一、libUV基础
-
- uv_loop_t结构体
- uv_loop_init
- 二、事件循环
- 三、mediasoup中的调用简介
- 四、总结
一、libUV基础
??下面给出阿里云评论几个常用的io库的对比:感兴趣请参考https://developer.aliyun.com/article/611321。我主要描述libUV的优点。
libuv是目前影响力最大的io模型库。架构图如下:
??在linux上,libuv是对epoll的封装;在windows上,是对完成端口的封装;在macOS/FreeBSD上,是对kqueue的封装。
uv_loop_t结构体
??uv_loop_t是libuv中最重要的结构体,存储了libuv整个生命周期的数据。下图引用来自知乎——the gc 作者的解析:https://zhuanlan.zhihu.com/p/139127919
uv_loop_init
??uv_loop_init是用于初始化uv_loop_t结构体的。
??下面给出mediasoup代码中的libuv源码:
int uv_loop_init(uv_loop_t* loop) {
struct heap* timer_heap;int err;/* Initialize libuv itself first *//* 对自身进行初始化 */uv__once_init();/* Create an I/O completion port *//* 创建一个io 完成端口 */ loop->iocp = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 1);if (loop->iocp == NULL)return uv_translate_sys_error(GetLastError());/* To prevent uninitialized memory access, loop->time must be initialized* to zero before calling uv_update_time for the first time.*//* 在uv_update_time访问前初始化loop->time来防止对未初始化的空间进行访问 */ loop->time = 0;uv_update_time(loop);QUEUE_INIT(&loop->wq);QUEUE_INIT(&loop->handle_queue);loop->active_reqs.count = 0;loop->active_handles = 0;loop->pending_reqs_tail = NULL;loop->endgame_handles = NULL;loop->timer_heap = timer_heap = uv__malloc(sizeof(*timer_heap));if (timer_heap == NULL) {
err = UV_ENOMEM;goto fail_timers_alloc;}heap_init(timer_heap);// 初始化结构体成员loop->check_handles = NULL;loop->prepare_handles = NULL;loop->idle_handles = NULL;loop->next_prepare_handle = NULL;loop->next_check_handle = NULL;loop->next_idle_handle = NULL;memset(&loop->poll_peer_sockets, 0, sizeof loop->poll_peer_sockets);loop->active_tcp_streams = 0;loop->active_udp_streams = 0;loop->timer_counter = 0;loop->stop_flag = 0;// 初始化各类锁err = uv_mutex_init(&loop->wq_mutex);if (err)goto fail_mutex_init;err = uv_async_init(loop, &loop->wq_async, uv__work_done);if (err)goto fail_async_init;uv__handle_unref(&loop->wq_async);loop->wq_async.flags |= UV_HANDLE_INTERNAL;err = uv__loops_add(loop);if (err)goto fail_async_init;return 0;fail_async_init:uv_mutex_destroy(&loop->wq_mutex);fail_mutex_init:uv__free(timer_heap);loop->timer_heap = NULL;fail_timers_alloc:CloseHandle(loop->iocp);loop->iocp = INVALID_HANDLE_VALUE;return err;
}
??当上述初始化完成后,整个libuv将进入一个事件循环。
二、事件循环
??I/O loop 也就是事件循环(Event Loop)是 libuv 的核心组成部分。它为所有 I/O 操作建立内容,实际上这意味着事件循环被绑定到一个单一的线程。可以运行多个不同的事件循环只要它们在不同的线程中。除非另有说明,事件循环(任何涉及事件循环和 handle 的 API)并不是线程安全的。
??所有的异步操作的结果最终在事件循环中被处理,也就是通常所说的回调函数,在事件循环中被调用。因为事件循环线程是单线程模式,所以所有用户代码都在一个线程中运行,libuv 更像是一个调度器,在合适的时候调度用户代码运行。此时,如果用户代码 CPU 密集型的耗时运算,将会阻塞事件循环。
??事件循环是非常常见的单线程异步 I/O 的处理方法:所有(网络)I/O 都在非阻塞的套接字上执行,这些套接字使用给定平台上可用的最佳机制进行轮询:Linux 上使用 epoll,OSX 和其他 BSDs 系统上使用 kqueue,SunOS 使用 event ports,Windows 上使用 IOCP。以上 I/O 轮询作为事件循环迭代的一部分,事件循环将会被阻塞在 I/O 轮询(例如:linux 上的 epoll_pwait 调用),直到被添加到轮询器中的套接字有 IO 活动(事件),事件循环线程将会在有 IO 事件时被唤醒,关联的回调函数将会被调用表明套接字有新的连接,然后便可以在 handles 上进行读、写或其他想要进行的操作 requests。
??图中主要有七个阶段,
??其中 idle、prepare、check 的实现完全相同,调用时间不同,类似于生命周期勾子,这几个阶段目的是允许开发者在事件循环的特定阶段执行代码,在 Node.js 主要用于性能信息收集。这三个阶段的实现代码比较简单,很容易理解。因源代码几乎完全使用宏实现,所以编辑器无法跳转到对应实现,搜索关键字也无法匹配,这里给出源文件路径:src/unix/loop-watcher.c,便于读者找到源文件。
??其余剩下的阶段就主要有 Call pending callbacks Poll for I/O Call close callbacks,这三个阶段主要用于处理 IO 操作等异步操作结果,阅读源码也主要是围绕着这三个阶段的代码展开的。
??各阶段用途描述:
- Run due timers:处理定时任务;
- Call pending callbacks:处理上一轮事件循环中因出现错误或者逻辑需要等原因挂起的任务;
- Run idle handles;
- Run prepare handles;
- Poll for I/O:事件循环 则有可能 因为 epoll_wait 而阻塞在这里,这取决于 timeout 参数是否为 0,但是通常情况下,会阻塞到有关注的 IO 事件发送时回,这也直接避免了时间循环一直工作导致占用 CPU 的问题。这个阶段是整个 libuv 事件循环最重要的阶段。libuv 的大部分 handle (都是 I/O 相关的)都依赖该阶段实现。
- Run check handles:
- Call close callbacks:清理被关闭的 handles。
三、mediasoup中的调用简介
??mediasoup对libuv的底层调用是通过文件描述符传递,来对io实现读写。
??下面介绍一下流媒体包接收的大致函数,对mediasoup的转发部分管中窥豹。
??下面是传输模块接到rtp包时做的处理。
// UnixStreamSocket 继承了 ConsumerSocket::Listener
// 而 ConsumerSocket::Listener 则是一个socket的监听者,当接到libuv的回调数据时进行处理。// 首先循环事件检测到数据,并执行了回调。此时,UnixStreamSocket 类则进行了读操作。// 括号后代表调用类
执行 OnUvRecv(UDPSocket)|
执行 UserOnUdpDatagramReceived(UDPSocket)|
执行 OnUdpSocketPacketReceived(UDPSocket)|
执行 OnUdpSocketPacketReceived(WebRtcTransport)|
执行 OnPacketReceived(WebRtcTransport)|
执行 OnRtpDataReceived(WebRtcTransport)|
执行 ReceiveRtpPacket(RTC)回调后进行处理转发。
四、总结
??本文主要通过查阅相关博客对libuv进行基本的了解,为mediasoup源码的阅读进行必要的准备。内容可能较于粗浅,也可能存在错误和偏差。但是这样的准备是必不可少的。
引用:
作者:阿里云社区——sunsky303 链接:https://developer.aliyun.com/article/611321
作者:知乎——the gc 链接https://zhuanlan.zhihu.com/p/139127919
查阅:https://libuv-docs-chinese.readthedocs.io/zh/latest/loop.html