<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
我們現在已經知道了Node
是單執行緒執行的,這表示潛在的錯誤有可能導致執行緒崩潰,然後程序也會隨著退出,無法做到企業追求的穩定性;另一方面,單程序也無法充分多核CPU,這是對硬體本身的浪費。Node
社群本身也意識到了這一問題,於是從0.1版本就提供了child_process
模組,用來提供多程序的支援。
child_process
模組中包括了很多建立子程序的方法,包括fork
、spawn
、exec
、execFile
等等。它們的定義如下:
child_process.exec(command[, options][, callback])
child_process.spawn(command[, args][, options])
child_process.fork(modulePath[, args][, options])
child_process.execFile(file[, args][, options][, callback])
在這4個API中以spawn
最為基礎,因為其他三個API或多或少都是藉助spawn
實現的。
spawn
方法的宣告格式如下:
child_process.spawn(command[, args][, options])
spawn
方法會使用指定的command
來生成一個新程序,執行完對應的command
後子程序會自動退出。
該命令返回一個child_process
物件,這代表開發者可以通過監聽事件來獲得命令執行的結果。
下面我們使用spwan
來執行ls
命令:
const spawn = require('child_process').spawn; const ls = spawn('ls', ['-1h', '/usr']); ls.stdout.on('data', (data) => { console.log('stdout: ', daata.toString()); }); ls.stderr.on('data', (data) => { console.log('stderr: ', daata.toString()); }); ls.on('close', (code) => { console.log('child process exited with code', code); });
其中spawn
的第一個引數雖然是command
,但實際接收的卻是一個file
,可以在Linux或者Mac OSX上執行,這是由於ls
命令也是以可執行檔案形式存在的。
類似的,在Windows系統下我們可以試著使用dir
命令來實現功能類似的程式碼:
const spawn = require('child_process').spawn; const ls = spawn('dir'); ls.stdout.on('data', (data) => { console.log('stdout: ', daata.toString()); });
然而在Windows下執行上面程式碼會出現形如Error:spawn dir ENOENT
的錯誤。
原因就在於spawn
實際接收的是一個檔名而非命令,正確的程式碼如下:
const spawn = require('child_process').spawn; const ls = spawn('powershell', ['dir']); ls.stdout.on('data', (data) => { console.log('stdout: ', daata.toString()); });
這個問題的原因與作業系統本身有關,在Linux中,一般都是檔案,命令列的命令也不例外,例如ls
命令是一個名為ls
的可執行檔案;而在Windows中並沒有名為dir
的可執行檔案,需要通過cmd
或者powershell
之類的工具提供執行環境。
在Linux環境下,建立一個新程序的本質是複製一個當前的程序,當用戶呼叫 fork
後,作業系統會先為這個新程序分配空間,然後將父程序的資料原樣複製一份過去,父程序和子程序只有少數值不同,例如程序識別符號(PD)。
對於 Node 來說,父程序和子程序都有獨立的記憶體空間和獨立的 V8 範例,它們和父程序唯一的聯絡是用來程序間通訊的 IPC Channel。
此外,Node中fork
和 POSIX 系統呼叫的不同之處在於Node中的fork
並不會複製父程序。
Node中的fork
是上面提到的spawn
的一種特例,前面也提到了Node中的fork
並不會複製當前程序。多數情況下,fork
接收的第一個引數是一個檔名,使用fork("xx.js")
相當於在命令列下呼叫node xx.js
,並且父程序和子程序之間可以通過process.send
方法來進行通訊。
下面我們來看一個簡單的栗子:
// master.js 呼叫 fork 來建立一個子程序 const child_process = require('child_process'); const worker = child_process.fork('worker.js', ['args1']); worker.on('exit', () => { console.log('child process exit'); }); worker.send({ msg: 'hello child' }); worker.on('message', msg => { console.log('from child: ', msg); }); // worker.js const begin = process.argv[2]; console.log('I am worker ' + begin); process.on('message', msg => { console.log('from parent ', msg); process.exit(); }); process.send({ msg: 'hello parent' });
fork
內部會通過spawn
呼叫process.executePath
,即Node
的可執行檔案地址來生成一個Node
範例,然後再用這個範例來執行fork
方法的modulePath
引數。
輸出結果為:
I am worker args1
from parent { msg: 'hello child' }
from child: { msg: 'hello parent' }
child process exit
如果我們開發一種系統,那麼對於不同的模組可能會用到不同的技術來實現,例如 Web伺服器使用 Node ,然後再使用 Java 的訊息佇列提供釋出訂閱服務,這種情況下通常使用程序間通訊的方式來實現。
但有時開發者不希望使用這麼複雜的方式,或者要呼叫的乾脆是一個黑盒系統,即無法通過修改原始碼來進行來實現程序間通訊,這時候往往採用折中的方式,例如通過 shell 來呼叫目標服務,然後再拿到對應的輸出。
child_process
提供了一個execFile
方法,它的宣告如下:
child_process.execFile(file, args, options, callback)
說明:
file {String}
要執行的程式的檔名args {Array}
字串參數列options {Object}
cwd {String}
子程序的當前工作目錄env {Object}
環境變數鍵值對encoding {String}
編碼(預設為 'utf8'
)timeout {Number}
超時(預設為 0)maxBuffer {Number}
緩衝區大小(預設為 200*1024)killSignal {String}
結束訊號(預設為'SIGTERM'
)callback {Function}
程序結束時回撥並帶上輸出error {Error}
stdout {Buffer}
stderr {Buffer}
ChildProcess
物件可以看出,execfile
和spawn
在形式上的主要區別在於execfile
提供了一個回撥函數,通過這個回撥函數可以獲得子程序的標準輸出/錯誤流。
使用 shell 進行跨程序呼叫長久以來被認為是不穩定的,這大概源於人們對控制檯不友好的互動體驗的恐懼(輸入命令後,很可能長時間看不到一個輸出,儘管後臺可能在一直運算,但在使用者看來和宕機無異)。
在 Linux下執行exec
命令後,原有程序會被替換成新的程序,進而失去對新程序的控制,這代表著新程序的狀態也沒辦法獲取了,此外還有 shell 本身執行出現錯誤,或者因為各種原因出現長時間卡頓甚至失去響應等情況。
Node.js 提供了比較好的解決方案,timeout
解決了長時間卡頓的問題,stdout
和stderr
則提供了標準輸出和錯誤輸出,使得子程序的狀態可以被獲取。
為了更好地說明,我們先寫一段簡單的 C 語言程式碼,並將其命名為 example.c
:
#include<stdio.h> int main() { printf("%s", "Hello World!"); return 5; }
使用 gcc
編譯該檔案:
gcc example.c -o example
生成名為example
的可執行檔案,然後將這個可執行檔案放到系統環境變數中,然後開啟控制檯,輸入example
,看到最後輸出"Hello World"
。
確保這個可執行檔案在任意路徑下都能存取。
我們分別用spawn
和execfile
來呼叫example
檔案。
首先是spawn
。
const spawn = require('child_process').spawn; const ls = spawn('example'); ls.stdout.on('data', (data) => { console.log('stdout: ', daata.toString()); }); ls.stderr.on('data', (data) => { console.log('stderr: ', daata.toString()); }); ls.on('close', (code) => { console.log('child process exited with code', code); });
程式輸出:
stdout: Hello World!
child process exited with code 5
程式正確列印出了Hello World
,此外還可以看到example
最後的return 5
會被作為子程序結束的code
被返回。
然後是execFile
。
const exec = require('child_process').exec; const child = exec('example', (error, stdout, stderr) => { if (error) { throw error; } console.log(stdout); });
同樣列印出Hello World
,可見除了呼叫形式不同,二者相差不大。
在子程序的資訊互動方面,spawn
使用了流式處理的方式,當子程序產生資料時,主程序可以通過監聽事件來獲取訊息;而exec
是將所有返回的資訊放在stdout
裡面一次性返回的,也就是該方法的maxBuffer
引數,當子程序的輸出超過這個大小時,會產生一個錯誤。
此外,spawn
有一個名為shell
的引數:
其型別為一個布林值或者字串,如果這個值被設定為true
,,就會啟動一個 shell 來執行命令,這個 shell 在 UNIX上是 bin/sh,,在Windows上則是cmd.exe。
exec
在內部也是通過呼叫execFile
來實現的,我們可以從原始碼中驗證這一點,在早期的Node原始碼中,exec
命令會根據當前環境來初始化一個 shell,,例如 cmd.exe 或者 bin/sh,然後在shell中呼叫作為引數的命令。
通常execFile
的效率要高於exec
,這是因為execFile
沒有啟動一個 shell,而是直接呼叫 spawn
來實現的。
前面介紹的幾個用於建立程序的方法,都是屬於child_process
的類方法,此外childProcess
類繼承了EventEmitter
,在childProcess
中引入事件給程序間通訊帶來很大的便利。
childProcess
中定義瞭如下事件。
Event:'close'
:程序的輸入輸出流關閉時會觸發該事件。Event:'disconnect'
:通常childProcess.disconnect
呼叫後會觸發這一事件。Event:'exit'
:程序退出時觸發。Event:'message'
:呼叫child_process.send
會觸發這一事件Event:'error'
:該事件的觸發分為幾種情況:kill
方法關閉。Event:'error'
事件無法保證一定會被觸發,因為可能會遇到一些極端情況,例如伺服器斷電等。
上面也提到,childProcess
模組定義了send
方法,用於程序間通訊,該方法的宣告如下:
child.send(message[, sendHandle[, options]][, callback])
通過send
方法傳送的訊息,可以通過監聽message
事件來獲取。
// master.js 父程序向子程序傳送訊息 const child_process = require('child_process'); const worker = child_process.fork('worker.js', ['args1']); worker.on('exit', () => { console.log('child process exit'); }); worker.send({ msg: 'hello child' }); worker.on('message', msg => { console.log('from child: ', msg); }); // worker.js 子程序接收父程序訊息 const begin = process.argv[2]; console.log('I am worker ' + begin); process.on('message', msg => { console.log('from parent ', msg); process.exit(); }); process.send({ msg: 'hello parent' });
send
方法的第一個引數型別通常為一個json
物件或者原始型別,第二個引數是一個控制程式碼,該控制程式碼可以是一個net.Socket
或者net.Server
物件。下面是一個例子:
//master.js 父程序傳送一個 Socket 物件 const child = require('child_process').fork('worker.js'); // Open up the server object and send the handle. const server = require('net').createServer(); server.on('connection', socket => { socket.end('handled by parent'); }); server.listen(1337, () => { child.send('server', server); }); //worker.js 子程序接收 Socket 物件 process.on('message', (m, server) => { if (m === 'server') { server.on('connection', socket => { socket.end('handled by child'); }); } });
前面已經介紹了child_process
的使用,child_process
的一個重要使用場景是建立多程序服務來保證服務穩定執行。
為了統一 Node 建立多程序服務的方式,Node 在之後的版本中增加了Cluster
模組,Cluster
可以看作是做了封裝的child_Process
模組。
Cluster
模組的一個顯著優點是可以共用同一個socket
連線,這代表可以使用Cluster
模組實現簡單的負載均衡。
下面是Cluster
的簡單栗子:
const cluster = require('cluster'); const http = require('http'); const numCPUs = require('os').cpus().length; if (cluster.isMaster) { console.log('Master process id is', process.pid); // Fork workers. for (let i = 0; i < numCPUs; i++) { cluster.fork(); } cluster.on('exit', (worker, code, signal) => { console.log('worker process died, id ', worker.process.pid); }); } else { // Worker 可以共用同一個 TCP 連線 // 這裡的例子是一個 http 伺服器 http.createServer((req, res) => { res.writeHead(200); res.end('hello worldn'); }).listen(8000); console.log('Worker started, process id', process.pid); }
上面是使用Cluster
模組的一個簡單的例子,為了充分利用多核CPU,先呼叫OS
模組的cpus()
方法來獲得CPU的核心數,假設主機裝有兩個 CPU,每個CPU有4個核,那麼總核數就是8。
在上面的程式碼中,Cluster
模組呼叫fork
方法來建立子程序,該方法和child_process
中的fork
是同一個方法。
Cluster
模組採用的是經典的主從模型,由master
程序來管理所有的子程序,可以使用cluster.isMaster
屬性判斷當前程序是master
還是worker
,其中主程序不負責具體的任務處理,其主要工作是負責排程和管理,上面的程式碼中,所有的子程序都監聽8000埠。
通常情況下,如果多個 Node 程序監聽同一個埠時會出現Error: listen EADDRINUS
的錯誤,而Cluster
模組能夠讓多個子程序監聽同一個埠的原因是master
程序內部啟動了一個 TCP 伺服器,而真正監聽埠的只有這個伺服器,當來自前端的請求觸發伺服器的connection
事件後,master
會將對應的socket
控制程式碼傳送給子程序。
到此這篇關於深入瞭解 Node的多程序服務實現的文章就介紹到這了,更多相關Node 多程序服務內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45