首頁  >  文章  >  web前端  >  深入了解Node.js和Electron是如何做進程通訊的

深入了解Node.js和Electron是如何做進程通訊的

青灯夜游
青灯夜游轉載
2021-07-26 10:11:545459瀏覽

這篇文章給大家探究一下Node.js 和 Electron 的進程通信原理,介紹一下electron 如何做進程通信、nodejs 的 child_process 和 cluster 如何做進程通信,了解進程通信的本質。

深入了解Node.js和Electron是如何做進程通訊的

為什麼前端要了解進程通訊:

前端領域已經不是單純寫在瀏覽器裡跑的頁面就可以了,還要會electron、 nodejs 等,而這兩個技術都需要掌握進程通訊。

nodejs 是js 的一個執行時,和瀏覽器不同,它擴充了許多封裝作業系統能力的api,其中就包含進程、執行緒相關api,而學習進程api 就要學習進程之間的通信機制。

electron 是基於 chromium 和 nodejs 的桌面端開發方案,它的架構是一個主進程,多個渲染進程,這兩種進程之間也需要通信,要學習 electron 的進程通信機制。 【推薦學習:《nodejs 教學》】

這篇文章我們就來深入了解一下進程通訊。

本文會說明以下知識點:

  • 程式是什麼
  • 本機處理通訊的四種方式
  • ##ipc、lpc、rpc 都是什麼
  • electron 如何做進程通訊
  • nodejs 的child_process 和cluster 如何做進程通訊
  • 進程通訊的本質

#進程

我們寫完的程式碼要在作業系統之上跑,作業系統為了更好的利用硬體資源,支援了多個程式的並發和硬體資源的分配,分配的單位就是進程,這個進程就是程式的執行過程。例如記錄程式執行到哪一步了,就申請了哪些硬體資源、佔用了什麼連接埠等。

進程包含要執行的程式碼、程式碼操作的數據,以及進程控制區塊PCB(Processing Control Block),因為程式就是程式碼在資料集上的執行過程,而執行過程的狀態和申請的資源需要記錄在一個資料結構(PCB)裡。所以進程由程式碼、資料、PCB 組成。

深入了解Node.js和Electron是如何做進程通訊的

pcb 中記錄pid、執行到的程式碼位址、進程的狀態(阻塞、運作、就緒等)以及用於通訊的信號量、管道、訊息佇列等資料結構。

深入了解Node.js和Electron是如何做進程通訊的

程式從創建到程式碼不斷的執行,到申請硬體資源(記憶體、硬碟檔案、網路等),中間還可能會阻塞,最後執行完會銷毀進程。這是一個進程的生命週期。

進程對申請來的資源是獨佔式的,每個行程都只能存取自己的資源,那進程之間怎麼溝通呢?

進程通訊

不同進程之間因為可用的記憶體不同,所以要透過一個中間媒體通訊。

信號量

如果是簡單的標記,透過一個數字來表示,放在PCB 的一個屬性裡,這叫做

信號量,例如鎖的實現就可以透過信號量。

這種訊號量的想法我們寫前端程式碼也常用,例如實現節流的時候,也要加一個標記變數。

管道

但是信號量不能傳遞具體的資料啊,傳遞具體資料還得用別的方式。例如我們可以透過讀寫文件的方式來通信,這就是

管道,如果是在記憶體中的文件,叫做匿名管道,沒有文件名,如果是真實的硬碟的文件,是有文件名的,叫做命名管道。

檔案需要先打開,然後再讀寫,之後再關閉,這也是管道的特色。管道是基於文件的思想封裝的,之所以叫管道,是因為只能一個進程讀、一個進程寫,是單向的(半雙工)。而且還需要目標進程同步的消費數據,不然就會阻塞。

這種管道的方式實現起來很簡單,就是一個文件讀寫,但是只能用在兩個進程之間通信,只能同步的通信。其實管道的同步通訊也蠻常見的,就是 stream 的 pipe 方法。

訊息佇列

管道實作簡單,但是同步的通訊比較受限制,那如果想做成非同步通訊呢?加個佇列做緩衝(buffer)不就行了,這就是

訊息佇列

訊息佇列也是兩個進程之間的通信,但是不是基於文件那一套思路,雖然也是單向的,但是有了一定的異步性,可以放很多消息,之後一次性消費。

共享記憶體#

管道、訊息佇列都是兩個行程之間的,如果多個行程之間呢?

我們可以透過申請一段多進程都可以操作的內存,叫做共享內存,用這種方式來通信。各行程都可以向該記憶體讀寫數據,效率比較高。

共享記憶體雖然效率高、也能用於多個進程的通信,但也不全是好處,因為多個進程都可以讀寫,那麼就很容易亂,要自己控制順序,比如通過進程的信號量(標記變數)來控制。

共享記憶體適用於多個進程之間的通信,不需要通過中間介質,所以效率更高,但是使用起來也更複雜。

上面說的這些幾乎就是本地進程通訊的全部方式了,為什麼要加個本地呢?

ipc、rpc、lpc

進程通訊就是ipc(Inter-Process Communication),兩個行程可能是一台電腦的,也可能網路上的不同電腦的進程,所以進程通訊方式分為兩種:

本地過程呼叫LPC(local procedure call)、遠端過程呼叫RPC(remote procedure call)。

本地過程呼叫就是我們上面說的信號量、管道、訊息佇列、共享記憶體的通訊方式,但是如果是網路上的,那就要透過網路協定來通訊了,這個其實我們用的比較多,例如http、websocket。

所以,當有人提到 ipc 時就是在說進程通信,可以分為本地的和遠端的兩種來討論。

遠端的都是基於網路協定封裝的,而本地的都是基於信號量、管道、訊息佇列、共享記憶體封裝出來的,例如我們接下來要探討的 electron 和 nodejs。

electron 進程通訊

electron 會先啟動主進程,然後透過 BrowserWindow 建立渲染進程,載入 html 頁面實作渲染。這兩個進程之間的通訊是透過 electron 提供的 ipc 的 api。

ipcMain、ipcRenderer

主程式裡面透過ipcMain 的on 方法監聽事件

import { ipcMain } from 'electron';

ipcMain.on('异步事件', (event, arg) => {
  event.sender.send('异步事件返回', 'yyy');
})

渲染進程裡面經過 ipcRenderer 的on 方法監聽事件,透過send 發送訊息

import { ipcRenderer } from 'electron';

ipcRender.on('异步事件返回', function (event, arg) {
  const message = `异步消息: ${arg}`
})

ipcRenderer.send('异步事件', 'xxx')

api 使用比較簡單,這是經過c 層的封裝,然後暴露給js 的事件形式的api。

我們可以想它是基於哪一種機制實現的呢?

很明顯有一定的非同步性,而且是父子程序之間的通信,所以是訊息佇列的方式實現的。

remote

除了事件形式的 api 外,electron 還提供了遠端方法呼叫 rmi (remote method invoke)形式的 api。

其實就是對訊息的進一步封裝,也就是根據傳遞的訊息,呼叫不同的方法,形式上就像呼叫本進程的方法一樣,但其實是發訊息到另一個進程來做的,和ipcMain、ipcRenderer 的形式本質上一樣。

例如在渲染流程裡面,透過 remote 來直接呼叫主程序才有的  BrowserWindow 的 api。

const { BrowserWindow } = require('electron').remote;

let win = new BrowserWindow({ width: 800, height: 600 });
win.loadURL('https://github.com');

小結一下,electron 的父子程序通訊方式是基於訊息佇列封裝的,封裝形式有兩種,一種是事件的方式,透過ipcMain、ipcRenderer 的api 使用,另一種則是進一步封裝成了不同方法的呼叫(rmi),底層也是基於訊息,執行遠端方法但是看起來像執行本地方法一樣。

nodejs

nodejs 提供了建立流程的 api,有兩個模組: child_process 和 cluster。很明顯,一個是用於父子進程的創建和通信,一個是用於多個進程。

child_process

child_process 提供了spawn、exec、execFile、fork 的api,分別用於不同的程序的建立:

spawn、exec

如果想透過shell 執行指令,那就用spawn 或exec。因為一般執行指令是需要傳回值的,這兩個 api 在傳回值的方式上有所不同。

spawn 回傳的是 stream,透過 data 事件來取,exec 進一步分裝成了 buffer,使用起來簡單一些,但是可能會超過 maxBuffer。

const { spawn } = require('child_process'); 

var app = spawn('node','main.js' {env:{}});

app.stderr.on('data',function(data) {
  console.log('Error:',data);
});

app.stdout.on('data',function(data) {
  console.log(data);
});

其實 exec 是基於 spwan 封裝出來的,簡單場景可以用,有的時候要設定下 maxBuffer。

const { exec } = require('child_process'); 

exec('find . -type f', { maxBuffer: 1024*1024 }(err, stdout, stderr) => { 
    if (err) { 
        console.error(`exec error: ${err}`); return; 
    }   
    console.log(stdout); 
});

execFile

除了執行指令外,如果要執行執行檔就用execFile 的api:

const { execFile } = require('child_process'); 

const child = execFile('node', ['--version'], (error, stdout, stderr) => { 
    if (error) { throw error; } 
    console.log(stdout); 
});

#fork

還有如果是想執行js ,那就用fork:

const { fork } = require('child_process');	

const xxxProcess = fork('./xxx.js');	
xxxProcess.send('111111');	
xxxProcess.on('message', sum => {	
    res.end('22222');	
});

小結

簡單小結一下child_process 的4 個api:

如果想要執行shell 指令,用spawn 和exec,spawn 回傳一個stream,而exec 進一步封裝成了buffer。除了 exec 有的時候需要設定下 maxBuffer,其他沒差別。

如果想執行執行文件,用  execFile。

如果想執行 js 文件,用 fork。

child_process 的进程通信

说完了 api 我们来说下 child_process 创建的子进程怎么和父进程通信,也就是怎么做 ipc。

pipe

首先,支持了 pipe,很明显是通过管道的机制封装出来的,能同步的传输流的数据。

const { spawn } = require('child_process'); 

const find = spawn('cat', ['./aaa.js']);
const wc = spawn('wc', ['-l']);  find.stdout.pipe(wc.stdin);

比如上面通过管道把一个进程的输出流传输到了另一个进程的输入流,和下面的 shell 命令效果一样:

cat ./aaa.js | wc -l

message

spawn 支持 stdio 参数,可以设置和父进程的 stdin、stdout、stderr 的关系,比如指定 pipe 或者 null。还有第四个参数,可以设置 ipc,这时候就是通过事件的方式传递消息了,很明显,是基于消息队列实现的。

const { spawn } = require('child_process');

const child = spawn('node', ['./child.js'], {
    stdio: ['pipe', 'pipe', 'pipe', 'ipc'] 
}); 
child.on('message', (m) => { 
    console.log(m); 
}); 
child.send('xxxx');

而 fork 的 api 创建的子进程自带了 ipc 的传递消息机制,可以直接用。

const { fork } = require('child_process');	

const xxxProcess = fork('./xxx.js');	
xxxProcess.send('111111');	
xxxProcess.on('message', sum => {	
    res.end('22222');	
});

cluster

cluster 不再是父子进程了,而是更多进程,也提供了 fork 的 api。

比如 http server 会根据 cpu 数启动多个进程来处理请求。

import cluster from 'cluster';
import http from 'http';
import { cpus } from 'os';
import process from 'process';

const numCPUs = cpus().length;

if (cluster.isPrimary) {
  for (let i = 0; i < numCPUs; i++) {
    cluster.fork();
  }
} else {
  const server = http.createServer((req, res) => {
    res.writeHead(200);
    res.end(&#39;hello world\n&#39;);
  })
  
  server.listen(8000);
  
  process.on(&#39;message&#39;, (msg) => {
    if (msg === &#39;shutdown&#39;) {
       server.close();
    }
  });
}

它同样支持了事件形式的 api,用于多个进程之间的消息传递,因为多个进程其实也只是多个父子进程的通信,子进程之间不能直接通信,所以还是基于消息队列实现的。

共享内存

子进程之间通信还得通过父进程中转一次,要多次读写消息队列,效率太低了,就不能直接共享内存么?

现在 nodejs 还是不支持的,可以通过第三方的包 shm-typed-array 来实现,感兴趣可以看一下。

https://www.npmjs.com/package/shm-typed-array

总结

进程包括代码、数据和 PCB,是程序的一次执行的过程,PCB 记录着各种执行过程中的信息,比如分配的资源、执行到的地址、用于通信的数据结构等。

进程之间需要通信,可以通过信号量、管道、消息队列、共享内存的方式。

  • 信号量就是一个简单的数字的标记,不能传递具体数据。

  • 管道是基于文件的思想,一个进程写另一个进程读,是同步的,适用于两个进程。

  • 消息队列有一定的 buffer,可以异步处理消息,适用于两个进程。

  • 共享内存是多个进程直接操作同一段内存,适用于多个进程,但是需要控制访问顺序。

这四种是本地进程的通信方式,而网络进程则基于网络协议的方式也可以做进程通信。

进程通信叫做 ipc,本地的叫做 lpc,远程的叫 rpc。

其中,如果把消息再封装一层成具体的方法调用,叫做 rmi,效果就像在本进程执行执行另一个进程的方法一样。

electron 和 nodejs 都是基于上面的操作系统机制的封装:

  • elctron 支持 ipcMain 和 ipcRenderer 的消息传递的方式,还支持了 remote 的 rmi 的方式。

  • nodejs 有 child_process 和 cluster 两个模块和进程有关,child_process 是父子进程之间,cluster 是多个进程:

    • child_process 提供了用于执行 shell 命令的 spawn、exec,用于执行可执行文件的 execFile,用于执行 js 的 fork。提供了 pipe 和 message 两种 ipc 方式。

    • cluster 也提供了 fork,提供了 message 的方式的通信。

当然,不管封装形式是什么,都离不开操作系统提供的信号量、管道、消息队列、共享内存这四种机制。

ipc 是开发中频繁遇到的需求,希望这篇文章能够帮大家梳理清楚从操作系统层到不同语言和运行时的封装层次的脉络。

原文地址:https://juejin.cn/post/6988484297485189127

作者:zxg_神说要有光

更多编程相关知识,请访问:编程视频!!

以上是深入了解Node.js和Electron是如何做進程通訊的的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文轉載於:juejin.cn。如有侵權,請聯絡admin@php.cn刪除