目錄
前言
上篇文章對MediaSoup源碼的調試方法 以及運行時分析、調試、查看核心信息 【流媒體服務器Mediasoup】 源碼中重要類基本概念 、上層代碼作用詳解、底層C++類關係詳解(四),本章節主要對MediaSoup的源碼中源碼中NodeJs與C++信令通信詳解,以及講解在Linux下管道通信的使用
在下一篇文章中將繼續對MediaSoup的源碼進行分析和架構的講解。
匿名管道進程間通信
Linux下常見的進程間通訊方式(IPC)
1:管道:匿名管道,有名管道
匿名管道需要有關聯如父子進程時間的聯繫
2:Socket:unixsocket,普通的socket
3:共享內存
4:信號 (kill命令)
管道的原理: 管道實爲內核使用環形隊列機制,藉助內核緩衝區(4k)實現。
對管道的具體概念不進行詳解,如需可以參考網上資料。
進程間管道 的創建與圖解
前三個是固定的特殊的管道,標準輸入,標準輸出,標準錯誤,最後兩個由兩個文件描述符引用,一個表示讀端,一個表示 寫端。
對於fd[0]=3 或者4來說 既可以讀也可以寫爲全雙工通信。fd[0]=4 寫入的數據需要 fd[0]=3 來讀取,反之fd[0]=3 寫入的數據需要fd[0]= 4 來讀取 ,這是隻有一個進程的情況下創建的socketpair。但fork了子進程後,子進程的fd[0]=3和fd[0]=4 一樣指向了 讀和寫管道,這樣很容易造成混亂。
那麼父進程fork子進程後,顯然 如果同時寫和讀會造成混亂,下圖改進方案爲
父進程發送信息給子進程,則就父寫子讀,就形成半雙工的父進程到子進程的通信
Mediasoup底層使用的是Unixsocket全雙工進程間通訊.那麼要想實現全雙工, 修改後的方案如下
最後實現的原理:
父進程關掉3,子進程關閉掉4,父進程用4去接收或者發送管道,子進程用3去接收或者發送管道
代碼實戰
代碼中給出了明顯的註釋,閱讀後來看下最後運行的結果
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <sys/socket.h>
const char* str = "SOCKET PAIR TEST.";
int main(int argc, char* argv[]){
int socket_pair[2];
pid_t id;
//創建socketpair
if(socketpair(AF_UNIX, SOCK_STREAM, 0, socket_pair) == -1){
printf("Error, socketpair create failed, errno(%d): %s\n", errno, strerror(errno));
return EXIT_FAILURE;
}
//創建子進程
id =fork();
if(id == 0){
//子進程
char buffer[512]={0, };
//子進程關閉管道4
close(socket_pair[1]);
while(1){
//發送數據給父進程
printf("childer send \n");
write(socket_pair[0], str , strlen(str));
sleep(1);
//接收父進程數據
ssize_t len = read(socket_pair[0], buffer, sizeof(buffer));
if(len > 0 ){
buffer[len] = '\0';
printf("childer: recv from parent : %s \n",buffer);
}
}
}else if(id > 0){
//父進程
char buffer[512]={0, };
//父進程關閉管道3
close(socket_pair[0]);
while(1){
//接收子進程數據
ssize_t len = read(socket_pair[1], buffer, sizeof(buffer));
if(len > 0 ){
buffer[len] = '\0';
printf("father: recv from childer : %s \n",buffer);
}
sleep(1);
//發送數據給父進程
printf("father send \n");
write(socket_pair[1], str , strlen(str));
}
}else{
printf("Error, fork failed, errno(%d): %s\n", errno, strerror(errno));
return EXIT_FAILURE;
}
return 0;
}
root@ubuntu:/work/guo# g++ hello.cpp -o hello //編譯
root@ubuntu:/work/guo# ./hello //運行
最後運行結果查看:
從結果中可以看到父進程發子進程收,子進程發父進程收,這樣就達到了父子進程的通訊效果。
在MediaSoup中在NodeJs層 根據CPU核心數量 用spawn 創建了這麼多數量的創建了Woker(子進程),那麼NodeJs層和C++層的子進程這個是通過這種管道通信方式進行信令的傳輸。
MediaSoup中的管道創建
管道在源碼中 哪裏創建,怎麼創建?
前面瞭解那麼多我們知道Worker 在底層實際上是一個進程或者說子進程,那麼管道是用來進程間通訊的,可以猜測到創建
管道的地方在和woker有關的類中,那麼根據源碼一層層剖析下去。
首先定位 mediasoup-demo/server/server.js
/**
* Launch as many mediasoup Workers as given in the configuration file.
*/
async function runMediasoupWorkers()
{
const { numWorkers } = config.mediasoup;
logger.info('running %d mediasoup Workers...', numWorkers);
for (let i = 0; i < numWorkers; ++i)
{ //服務啓動後開始創建worker
const worker = await mediasoup.createWorker(
{
logLevel : config.mediasoup.workerSettings.logLevel,
logTags : config.mediasoup.workerSettings.logTags,
rtcMinPort : Number(config.mediasoup.workerSettings.rtcMinPort),
rtcMaxPort : Number(config.mediasoup.workerSettings.rtcMaxPort)
});
....省略
}
}
const worker = await mediasoup.createWorker 最後實際調用到MediaSoup庫中的Woker.js
繼續定位到 mediasoup-demo\server\node_modules\mediasoup\lib\Worker.js
class Worker extends EnhancedEventEmitter_1.EnhancedEventEmitter {
/**
* @private
* @emits died - (error: Error)
* @emits @success
* @emits @failure - (error: Error)
*/
constructor({ logLevel, logTags, rtcMinPort, rtcMaxPort, dtlsCertificateFile, dtlsPrivateKeyFile, appData }) {
super();
...省略
//啓動核心文件 實際上新建woker即線程
this._child = child_process_1.spawn(
// command
spawnBin,
// args
spawnArgs,
// options
{
env: {
MEDIASOUP_VERSION: '3.4.11'
},
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']
});
this._pid = this._child.pid;
// 創建的管道交給channel處理
this._channel = new Channel_1.Channel({
producerSocket: this._child.stdio[3],
consumerSocket: this._child.stdio[4],
pid: this._pid
});
this._appData = appData;
let spawnDone = false;
// Listen for 'ready' notification.
this._channel.once(String(this._pid), (event) => {
...省略
});
this._child.on('exit', (code, signal) => {
...省略
});
this._child.on('error', (error) => {
...省略
});
// Be ready for 3rd party worker libraries logging to stdout.
this._child.stdout.on('data', (buffer) => {
...省略
});
// In case of a worker bug, mediasoup will log to stderr.
this._child.stderr.on('data', (buffer) => {
...省略
});
}
首先對node child_process模塊 spawn 的使用不了解的話可以搜索網上的資料,這裏簡單的介紹下
child_process 模塊Api : https://nodejs.org/api/child_process.html
//spawn 實際上是執行Linux系統下的命令
const ls = spawn('ls', ['-lh', '/usr']);
ls.stdout.on('data', (data) => { console.log(`輸出:${data}`); });
ls.stderr.on('data', (data) => { console.log(`錯誤:${data}`); });
ls.on('close', (code) => { console.log(`子進程退出碼:${code}`); });
最後實際執行的結果會回調在 stdout這個文件描述符 data的內容就是/usr 下的所有目錄和文件打印。
- spawn : 子進程中執行的是非node程序,提供一組參數後,執行的結果以流的形式返回。
默認情況下,Node.js 的父進程與衍生的子進程之間會建立 stdin、stdout 和 stderr 的管道
在源碼使用spawn中,來詳解一些參數,其中
command
爲 spawnBin 實際上是一個MediaSoup編譯完的一個可執行庫的路徑
args 是一些執行時的參數,主要是一些 config.js中mediasoup節點下的值拼接成字符串作爲參數 來啓動核心文件。
option 可以有很多選項,這邊只列舉了上述代碼中使用到的,具體參數意義如下圖所示
child_process.spawn(command[, args][, options])
command
<string> The command to run. //要運行的命令args
<string[]> List of string arguments. //字符串參數列表
options
<Object>
env
<Object> Environment key-value pairs. Default:process.env
. //環境鍵值對。默認值:process.env。stdio
<Array> | <string> Child's stdio configuration (seeoptions.stdio
). //options.stdio選項用於配置在父進程和子進程之間建立的管道。默認情況下,子進程的stdin、stdout和stderr被重定向到相應的subprocess.stddetached
<boolean> Prepare child to run independently of its parent process. Specific behavior depends on the platform, seeoptions.detached
). //準備子進程獨立於其父進程運行。具體行爲取決於平臺- Returns: <ChildProcess>
所以根據根據中 可知
producerSocket -> stdio[3] consumerSocket -> stdio[4]
stdio[3] 用來發送數據給子進程,stdio[4] 用來接收數據。
最後管道處理都交給了Channel 類來處理了。
this._channel = new Channel_1.Channel({
producerSocket: this._child.stdio[3],
consumerSocket: this._child.stdio[4],
pid: this._pid
});
到這裏我們就知道具體管道建立的過程。
MediaSoup Channel的創建
JS首先組成JSON格式的命令最後將它轉成字符串 通過channel通道傳給C++端,C++有個接收管道接收到數據之後,再轉成JSON,最後在解析成Request(c++類) 中的一些字段,根據Methodid去處理相對應的信令。
NodeJs和 C++ 管道通信的過程
上述已經對管道的創建過程大致的說明,當通過spwn創建子進程後管道其實就已經建立成功,這些管道最後交給Channel管理,
繼續定位到 mediasoup-demo\server\node_modules\mediasoup\lib\Channel.js
根據上述管道瞭解知道,producerSocket stdio[3] 用來發送數據給子進程,consumerSocket stdio[4] 用來接收數據。
那麼具體是如何發送或者接受呢?
那麼先來看發送數據:
將數據寫入管道 這裏真正的是進行管道寫入
this._producerSocket.write(ns);
用異步方式去監聽底層是否接受並處理信息,這裏的確認結果和接收中的邏輯相匹配
會通過this._sents.set(id, sent); sent的裏的resolve 或者 pReject 返回
發送之後會保存在一個Map對象裏,等待後續消息確認回來根據對應的id進行處理。
request(method, internal, data) {
return __awaiter(this, void 0, void 0, function* () {
...省略
//將數據寫入管道 這裏真正的是進行管道寫入
this._producerSocket.write(ns);
//用異步方式去監聽底層是否接受並處理信息,這裏的確認結果和接收中的邏輯相匹配
//會通過this._sents.set(id, sent); sent的裏的resolve 或者 pReject 返回
return new Promise((pResolve, pReject) => {
const timeout = 1000 * (15 + (0.1 * this._sents.size));
const sent = {
id: id,
method: method,
resolve: (data2) => {
if (!this._sents.delete(id))
return;
clearTimeout(sent.timer);
pResolve(data2);
},
reject: (error) => {
if (!this._sents.delete(id))
return;
clearTimeout(sent.timer);
pReject(error);
},
timer: setTimeout(() => {
if (!this._sents.delete(id))
return;
pReject(new Error('Channel request timeout'));
}, timeout),
close: () => {
clearTimeout(sent.timer);
pReject(new errors_1.InvalidStateError('Channel closed'));
}
};
// Add sent stuff to the map.
this._sents.set(id, sent);
});
});
}
在下面哪段代碼中的Channel構造函數中,
this._consumerSocket.on('data', (buffer) => 回調函數裏監聽或者接受數據,真正的處理有效數據其實在 this._processMessage(JSON.parse(nsPayload)); 函數中。
class Channel extends EnhancedEventEmitter_1.EnhancedEventEmitter {
constructor({ producerSocket, consumerSocket, pid }) {
...省略
this._producerSocket = producerSocket;
this._consumerSocket = consumerSocket;
// Read Channel responses/notifications from the worker.
//用於接受底層C++發來的信令數據
this._consumerSocket.on('data', (buffer) => {
...省略
try {
// We can receive JSON messages (Channel messages) or log strings.
switch (nsPayload[0]) {
// 123 = '{' (a Channel JSON messsage).
case 123:
//真正的處理有效的信令數據
this._processMessage(JSON.parse(nsPayload));
break;
// 68 = 'D' (a debug log).
case 68:
logger.debug(`[pid:${pid}] ${nsPayload.toString('utf8', 1)}`);
break;
// 87 = 'W' (a warn log).
case 87:
logger.warn(`[pid:${pid}] ${nsPayload.toString('utf8', 1)}`);
break;
// 69 = 'E' (an error log).
case 69:
logger.error(`[pid:${pid} ${nsPayload.toString('utf8', 1)}`);
break;
// 88 = 'X' (a dump log).
case 88:
// eslint-disable-next-line no-console
console.log(nsPayload.toString('utf8', 1));
break;
default:
....省略
}
定位到 _processMessage(msg)
從下面哪段代碼中可以看出,其中處理信令又種方式一種msg帶id 一種不帶
其原因是一種是 消息確認 和 事件通知 區別。
其中上層發送信令給底層會暫時保存起來消息確認需要攜帶id,上層才能通過id來確定是哪條信令完成。
如果是不帶id,那麼屬於事件通知,最終會調用 this.emit(msg.targetId, msg.event, msg.data); 發送出去。
_processMessage(msg) {
if (msg.id) {
...省略
if (msg.accepted) {
logger.debug('request succeeded [method:%s, id:%s]', sent.method, sent.id);
//確定消息處理完成,並在信令 Map表裏確認並回調
sent.resolve(msg.data);
}
else if (msg.error) {
switch (msg.error) {
case 'TypeError':
sent.reject(new TypeError(msg.reason));
break;
default:
sent.reject(new Error(msg.reason));
}
}
else {
logger.error('received response is not accepted nor rejected [method:%s, id:%s]', sent.method, sent.id);
}
}
// If a notification emit it to the corresponding entity.
else if (msg.targetId && msg.event) {
this.emit(msg.targetId, msg.event, msg.data);
}
// Otherwise unexpected message.
else {
logger.error('received message is not a response nor a notification');
}
}
MediaSoup 消息確認與事件通知
消息的確認是指上層給mediasoup底層發送消息時,底層處理完要發送消息確認給上層處理結果。
事件通知是底層的一些操作導致狀態變化要通知到到上層進行操作同步。簡單初步的看下C++是如何執行消息確認與事件通知的。
返回信令確認消息給上層
...
Request->Accept(data); & Request->Accept();
...
給上層發送通知 Notifier
在main函數裏初始化
....
Channel::Notifier::ClassInit(channel);
...
Channel::Notifier::Emit(this->id, "icestatechange", data);
...
無論是事件通知上層或者返回消息,兩者都是通過管道傳給上層
最終都調用channel->send()
小結
JS首先組成JSON格式的命令最後將它轉成字符串 通過channel通道傳給C++端,C++有個接收管道接收到數據之後,再轉成JSON,最後在解析成Request (C++中)中的一些字段,根據Methodid去處理相對應的信令。處理完消息後再生成字符串的發送給上層去確認。 通知事件是由底層主動發起的通知。
因此整個通信架構基本清楚,通過管道進行進程間通訊,在Linux下也是非常高效常見的一種方式。