流媒体学习之路(mediasoup)——Worker(c++)libuv(番外)

流媒体学习之路(mediasoup)——Worker(c++)libuv(番外)

提示:
当mediasoup进程启动时,会检测Node.js服务器是否启动。随后初始化需要用到的各个模块。
分别启动:libUV、建立管道通信模块、读取config、OpenSSL模块、libSRTP模块、UsrSCTP模块、libWebRTC模块、Crypto加密模块、DtlsTransport传输模块、SrtpSession会议模块、两个业务channel模块。
下面对整体的Worker用到的开源模块进行分析对比:



一、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 操作等异步操作结果,阅读源码也主要是围绕着这三个阶段的代码展开的。
  各阶段用途描述:

  1. Run due timers:处理定时任务;
  2. Call pending callbacks:处理上一轮事件循环中因出现错误或者逻辑需要等原因挂起的任务;
  3. Run idle handles;
  4. Run prepare handles;
  5. Poll for I/O:事件循环 则有可能 因为 epoll_wait 而阻塞在这里,这取决于 timeout 参数是否为 0,但是通常情况下,会阻塞到有关注的 IO 事件发送时回,这也直接避免了时间循环一直工作导致占用 CPU 的问题。这个阶段是整个 libuv 事件循环最重要的阶段。libuv 的大部分 handle (都是 I/O 相关的)都依赖该阶段实现。
  6. Run check handles:
  7. 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

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值