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

renke發表於2021-09-11
本篇文章給大家探究一下 和 Electron 的程式通訊原理,介紹一下electron 如何做程式通訊、nodejs 的 child_process 和 cluster 如何做程式通訊,瞭解程式通訊的本質。

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

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

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

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

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

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

本文會講解以下知識點:

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

程式

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

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

1.png

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

2.png

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

程式對申請來的資源是獨佔式的,每個程式都只能訪問自己的資源,那程式之間怎麼通訊呢?

程式通訊

不同程式之間因為可用的記憶體不同,所以要透過一箇中間介質通訊。

訊號量

如果是簡單的標記,透過一個數字來表示,放在 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('');

小結一下,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('hello worldn');
  })
  
  server.listen(8000);
  
  process.on('message', (msg) => {
    if (msg === 'shutdown') {
       server.close();
    }
  });
}

它同樣支援了事件形式的 api,用於多個程式之間的訊息傳遞,因為多個程式其實也只是多個父子程式的通訊,子程式之間不能直接通訊,所以還是基於訊息佇列實現的。

共享記憶體

子程式之間通訊還得透過父程式中轉一次,要多次讀寫訊息佇列,效率太低了,就不能直接共享記憶體麼?

現在 nodejs 還是不支援的,可以透過第三方的包 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 是開發中頻繁遇到的需求,希望這篇文章能夠幫大家梳理清楚從作業系統層到不同語言和執行時的封裝層次的脈絡。

原文地址:

作者:zxg_神說要有光

更多程式設計相關知識,請訪問:!!

以上就是深入瞭解Node.js和Electron是如何做程式通訊的的詳細內容,更多請關注php中文網其它相關文章!

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/430/viewspace-2827769/,如需轉載,請註明出處,否則將追究法律責任。

相關文章