替换 mediasoup 的 Node.js 模块实现的尝试

关于 mediaSoup 是什么以及能用其做什么,不是本文的重点。基于某些原因考虑,希望将 mediasoup 的 Node.js 模块使用其他平台/语言来实现,比如 Java.Net CoreCSDN 上有位博主使用 C++Windows 平台实现了,不过没开源所以只好自己动手。

二、Node.js 和 mediasoup-worker 的关系

mediasoup 的核心程序是 mediasoup-worker ,其是单线程应用。在 Node.js 层一般通过 spawn 创建和 CPU 核心数相同的 mediasoup-worker 进程。

备注:严格来说 mediasoup-worker 不是单线程应用,因为其内部使用的 libuv 在进行某些操作的时候采用的是多线程。

三、Node.js 的 spawn 和 libuv uv_spawn(fork/exec)

libuvV8 是 Node.js 的基石,而 mediasoup-worker 也使用了 libuv。

在 Node.js 程序中,安装 mediasoup 的模块时会将 mediasoup-worker 会自动编译在 node_modules 里。可以直接将 mediasoup-worker 拷贝出来在 Shell 中运行——当然,一运行就会退出。

> ./mediasoup-worker
mediasoup-worker::main() | you don't seem to be my real father!

通过查看 mediasoup-worker 的源码得知其需要一个 MEDIASOUP_VERSION 环境变量——当然,加上后一运行还是会退出。

> MEDIASOUP_VERSION=3.5.5 ./mediasoup-worker
UnixStreamSocket::UnixStreamSocket() | throwing MediaSoupError: uv_pipe_open() failed: inappropriate ioctl for device
mediasoup-worker::main() | error creating the Channel: uv_pipe_open() failed: inappropriate ioctl for device

原因是 mediasoup-worker 依赖于两个目前并不存在的文件描述符 3 和 4。这里的 3 和 4 其实是一种约定。那在 Shell 中重定向到标准输出试试。

> MEDIASOUP_VERSION=3.5.5 ./mediasoup-worker 3>&1 4>&1
37:{"event":"running","targetId":"3574"},

能够获取到 mediasroup-worker 启动成功后的输出。

在 Linux 上,在 fork 子进程的时候,会将父进程的文件描述符传递到子进程中,这是进程间通信的一种方式。Node.js 程序 fork 进程之前,会创建几个 libuv 概念下而非 Linux 概念下的抽象意义上的 pipe ,在 Linux 中使用的是 Unix Domain Socket 实现。Node.js 程序或者说 libuv fork 进程后,会在子进程将要使用的文件描述符重定向。比如在父进程,期望子进程持有的文件描述符是 3 和 4 而实际上是 11 和 13,fork 之后还是 11 和 13 ,在子进程中使用 fcntl 系统调用重定向。通过合理的数量和顺序上的约定能确定重定向为 3 和 4 。最终在子进程中 exec mediasoup-worker(见:uv__process_child_init)。

// File: node_modules/mediasoup/src/Worker.ts
this._child = spawn(
    // command
    spawnBin,
    // args
    spawnArgs,
    // options
    {
        env :
        {
            MEDIASOUP_VERSION : '__MEDIASOUP_VERSION__'
        },

        detached : false,

        // fd 0 (stdin) : Just ignore it.
        // fd 1 (stdout) : Pipe it for 3rd libraries that log their own stuff.
        // fd 2 (stderr) : Same as stdout.
        // fd 3 (channel) : Producer Channel fd.
        // fd 4 (channel) : Consumer Channel fd.
        stdio : [ 'ignore', 'pipe', 'pipe', 'pipe', 'pipe' ]
    }
);

参考:Node.js 的 spawn 和 libuv 的 uv_spawn 的实现源码,以及 mediasoup 的 Node.js 模块的源码。

备注:libuv 在 Windows 上进程间通信使用的是命名管道(Named Pipe)。

三、C 实现

下面是使用 C 语言实现的一个非常粗糙的版本。

//
//  main.c
//  TestMedaisoup
//
//  Created by Alby on 2020/3/31.
//  Copyright © 2020 alby. All rights reserved.
//
#include <stdio.h>
#include <uv.h>
#define ASSERT(expr)                                      \
do {                                                     \
 if (!(expr)) {                                          \
   fprintf(stderr,                                       \
           "Assertion failed in %s on line %d: %s\n",    \
           __FILE__,                                     \
           __LINE__,                                     \
           #expr);                                       \
   abort();                                              \
 }                                                       \
} while (0)
static int close_cb_called;
static int exit_cb_called;
static uv_process_t process;
static uv_process_options_t options;
static char* args[5];
#define OUTPUT_SIZE 1024
static char output[OUTPUT_SIZE];
static int output_used;
static void init_process_options(char* test, uv_exit_cb exit_cb) {
  char *exepath = "/Users/XXXX/Developer/OpenSource/Meeting/Lab/worker/mediasoup-worker";
  args[0] = exepath;
  args[1] = NULL;
  args[2] = NULL;
  args[3] = NULL;
  args[4] = NULL;
  options.file = exepath;
  options.args = args;
  options.exit_cb = exit_cb;
  options.flags = 0;
}
static void close_cb(uv_handle_t* handle) {
  printf("close_cb\n");
  close_cb_called++;
}
static void exit_cb(uv_process_t* process,
                    int64_t exit_status,
                    int term_signal) {
  printf("exit_cb\n");
  exit_cb_called++;
  ASSERT(exit_status == 1);
  ASSERT(term_signal == 0);
  uv_close((uv_handle_t*)process, close_cb);
}
static void on_alloc(uv_handle_t* handle,
                     size_t suggested_size,
                     uv_buf_t* buf) {
  buf->base = output + output_used;
  buf->len = OUTPUT_SIZE - output_used;
}
static void on_read(uv_stream_t* tcp, ssize_t nread, const uv_buf_t* buf) {
  if (nread > 0) {
    output_used += nread;
    printf(buf->base);
  } else if (nread < 0) {
    ASSERT(nread == UV_EOF);
    uv_close((uv_handle_t*)tcp, close_cb);
  }
}
int main() {

    const int stdio_count = 5;
    int r;
    uv_pipe_t pipes[4];
    uv_stdio_container_t stdio[5];
    init_process_options("spawn_helper5", exit_cb);
    for(int i = 1; i < stdio_count; i++) {
        uv_pipe_init(uv_default_loop(), &pipes[i-1], 0);
    }
    stdio[0].flags = UV_IGNORE;
    for(int i = 1; i < stdio_count; i++) {
        stdio[i].flags = UV_CREATE_PIPE | UV_READABLE_PIPE | UV_WRITABLE_PIPE;
        stdio[i].data.stream = (uv_stream_t*)&pipes[i-1];
    }

    char* quoted_path_env[1];
    quoted_path_env[0] = "MEDIASOUP_VERSION=3.5.5";
    options.env = quoted_path_env;
    options.stdio = stdio;
    options.stdio_count = stdio_count;
    r = uv_spawn(uv_default_loop(), &process, &options);
    ASSERT(r == 0);
    for(int i = 1; i < stdio_count; i++) {
        r = uv_read_start((uv_stream_t*) &pipes[i - 1], on_alloc, on_read);
        ASSERT(r == 0);
    }
    r = uv_run(uv_default_loop(), UV_RUN_DEFAULT);
    ASSERT(r == 0);
    ASSERT(exit_cb_called == 1);
    ASSERT(close_cb_called == 5); /* Once for process once for the pipe. */

    return 0;
}

四、问题

我尝试在 Linux 中用 .Net Core 实现,但是没有想到完善的方案;而如果在 Windows 中实现,也还有一个 libuv 对命名管道命名的问题没想到好的办法。

参考资料

我来评几句
登录后评论

已发表评论数()

相关站点

+订阅
热门文章