【zz】替換 mediasoup 的 Node.js 模塊實現的嘗試
替換 mediasoup 的 Node.js 模塊實現的嘗試
語言: CN / TW / HK
時間 2020-03-31 22:09:50 Tubumu
主題: Node.js libuv
關於 mediaSoup 是什麼以及能用其做什麼,不是本文的重點。基於某些原因考慮,希望將 mediasoup 的 Node.js 模塊使用其他平台/語言來實現,比如 Java 或 .Net Core 。 CSDN 上有位博主使用 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)
libuv 和 V8 是 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;
}
//管道也是tcp么???
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 對命名管道命名的問題沒想到好的辦法。
參考資料
https://mediasoup.org/
https://github.com/nodejs/node
https://github.com/libuv/libuv
https://docs.microsoft.com/en-us/dotnet/standard/io/how-to-use-named-pipes-for-network-interprocess-communication
https://docs.microsoft.com/en-us/dotnet/api/system.net.sockets.unixdomainsocketendpoint?view=netstandard-2.1
https://*.com/questions/40195290/how-to-connect-to-a-unix-domain-socket-in-net-core-in-c-sharp
Unix: Why not use Unix Domain Sockets for Named Pipes?
https://silvercircle.github.io/2018/08/26/serving-net-core-kestrel-linux-unix-sockets/
https://github.com/aspnet/libuv-build
https://blog.csdn.net/gupar/article/details/83788934