node child_process模塊


NodeJs是一個單進程的語言,不能像Java那樣可以創建多線程來並發執行。當然在大部分情況下,NodeJs是不需要並發執行的,因為它是事件驅動性永不阻塞。但單進程也有個問題就是不能充分利用CPU的多核機制,根據前人的經驗,可以通過創建多個進程來充分利用CPU多核,並且Node通過了child_process模塊來創建完成多進程的操作。

child_process模塊給予node任意創建子進程的能力,node官方文檔對於child_proces模塊給出了四種方法,映射到操作系統其實都是創建子進程。但對於開發者而已,這幾種方法的api有點不同

child_process.exec(command[, options][, callback]) 啟動
子進程來執行shell命令,可以通過回調參數來獲取腳本shell執行結果

const { exec } = require('child_process');
exec('cat *.js bad_file | wc -l', (error, stdout, stderr) => {
  if (error) {
    console.error(`exec error: ${error}`);
    return;
  }
  console.log(`stdout: ${stdout}`);
  console.log(`stderr: ${stderr}`);
});


child_process.execfile(file[, args][, options][, callback])
與exec類型不同的是,不衍生一個 shell,而是,指定的可執行的 file 被直接衍生為一個新進程,這使得它比 child_process.exec() 更高效。 由於沒有衍生 shell,因此不支持像 I/O 重定向和文件查找這樣的行為。

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

 

與另外另個命令不同的是接受一個函數,如果提供了一個 callback 函數,則它被調用時會帶上參數 (error, stdout, stderr)。 當成功時,error 會是 null。 當失敗時,error 會是一個 Error實例。 error.code 屬性會是子進程的退出碼,error.signal 會被設為終止進程的信號。 除 0 以外的任何退出碼都被認為是一個錯誤。 

exec()與execfile()在創建的時候可以指定timeout屬性設置超時時間,一旦超時會被殺死 
如果使用execfile()執行可執行文件,那么頭部一定是#!/usr/bin/env node

 

child_process.spawn(command[, args][, options])

僅僅執行一個shell命令,不需要獲取執行結果

const { spawn } = require('child_process');
const ls = spawn('ls', ['-lh', '/usr']);

ls.stdout.on('data', (data) => {
  console.log(`stdout: ${data}`);
});

ls.stderr.on('data', (data) => {
  console.log(`stderr: ${data}`);
});

ls.on('close', (code) => {
  console.log(`子進程退出碼:${code}`);
});

// 例子,一種執行 'ps ax | grep ssh' 的方法:
const { spawn } = require('child_process');
const ps = spawn('ps', ['ax']);
const grep = spawn('grep', ['ssh']);

ps.stdout.on('data', (data) => {
  grep.stdin.write(data);
});

ps.stderr.on('data', (data) => {
  console.log(`ps stderr: ${data}`);
});

ps.on('close', (code) => {
  if (code !== 0) {
    console.log(`ps 進程退出碼:${code}`);
  }
  grep.stdin.end();
});

grep.stdout.on('data', (data) => {
  console.log(data.toString());
});

grep.stderr.on('data', (data) => {
  console.log(`grep stderr: ${data}`);
});

grep.on('close', (code) => {
  if (code !== 0) {
    console.log(`grep 進程退出碼:${code}`);
  }
});


child_process.fork(modulePath[, args][, options])   

與另外三個不同的是它開啟的是一個node進程,執行的只能是js文件。並通過建立 IPC 通訊通道來調用指定的模塊,該通道允許父進程與子進程之間相互發送信息。

進程間通信

node 與 子進程之間的通信是使用IPC管道機制完成。如果子進程 
也是node進程(使用fork),則可以使用監聽message事件和使用send()來通信。

main.js

var cp = require('child_process');
//只有使用fork才可以使用message事件和send()方法
var n = cp.fork('./child.js');
n.on('message',function(m){
  console.log(m);
})

n.send({"message":"hello"});

child.js

var cp = require('child_process');
process.on('message',function(m){
 console.log(m);
})
process.send({"message":"hello I am child"})

父子進程之間會創建IPC通道,message事件和send()便利用IPC通道通信.

句柄傳遞

學會如何創建子進程后,我們創建一個HTTP服務並啟動多個進程來共同 
做到充分利用CPU多核。 
worker.js

var http = require('http');
http.createServer(function(req,res){
  res.end('Hello,World');
  //監聽隨機端口
}).listen(Math.round((1+Math.random())*1000),'127.0.0.1');

main.js

var fork = require('child_process').fork;
var cpus = require('os').cpus();
for(var i=0;i<cpus.length;i++){
  fork('./worker.js');
}

上述代碼會根據你的cpu核數來創建對應數量的fork進程,每個進程監聽一個隨機端口來提供HTTP服務。

上述就完成了一個典型的Master-Worker主從復制模式。在分布式應用中用於並行處理業務,具備良好的收縮性和穩定性。這里需要注意,fork一個進程代價是昂貴的,node單進程事件驅動具有很好的性能。此例的多個fork進程是為了充分利用CPU的核,並非解決並發問題.
上述示例有個不太好的地方就是占有了太多端口,那么能不能對於多個子進程全部使用同一個端口從而對外提供http服務也只是使用這一個端口。嘗試將上述的端口隨機數改為8080,啟動會發現拋出如下異常。

events.js:72
        throw er;//Unhandled 'error' event
Error:listen EADDRINUSE
XXXX

拋出端口被占有的異常,這意味着只有一個worker.js才能監聽8080端口,而其余的會拋出異常。
如果要解決對外提供一個端口的問題,可以參考nginx反向代理的做法。對於Master進程使用80端口對外提供服務,而對於fork的進程則使用隨機端口,Master進程接受到請求就將其轉發到fork進程中

對於剛剛所說的代理模式,由於進程每收到一個連接會使用掉一個文件描述符,因此代理模式中客戶端連接到代理進程,代理進程再去連接fork進程會使用掉兩個文件描述符,OS中文件描述符是有限的,為了解決這個問題,node引入進程間發送句柄的功能。
在node的IPC進程通訊API中,send(message,[sendHandle])的第二個參數就是句柄。
句柄就是一種標識資源的引用,它的內部包含了指向對象的文件描述符。句柄可以用來描述一個socket對象,一個UDP套接子,一個管道
主進程向工作進程發送句柄意味着當主進程接收到客戶端的socket請求后則直接將這個socket發送給工作進程,而不需要再與工作進程建立socket連接,則文件描述符的浪費即可解決。我們來看示例代碼:
main.js

var cp = require('child_process');
var child = cp.fork('./child.js');
var server = require('net').createServer();
//監聽客戶端的連接
server.on('connection',function(socket){
  socket.end('handled by parent');
});
//啟動監聽8080端口
server.listen(8080,function(){
//給子進程發送TCP服務器(句柄)
  child.send('server',server);
});

child.js

process.on('message',function(m,server){
  if(m==='server'){
    server.on('connection',function(socket){
      socket.end('handle by child');
    });
  }
});

使用telnet或curl都可以測試:

1 wang@wang ~/code/nodeStudy $ curl 192.168.10.104:8080
2 handled by parent
3 wang@wang ~/code/nodeStudy $ curl 192.168.10.104:8080
4 handle by child
5 wang@wang ~/code/nodeStudy $ curl 192.168.10.104:8080
6 handled by parent
7 wang@wang ~/code/nodeStudy $ curl 192.168.10.104:8080
8 handled by parent

測試結果是每次對於客戶端的連接,有可能父進程處理也有可能被子進程處理。現在我們嘗試僅提供http服務,並且為了讓父進程更加輕量,僅讓父進程傳遞句柄給子進程而不做請求處理:

main.js

var cp = require('child_process');
var child1 = cp.fork('./child.js');
var child2 = cp.fork('./child.js');
var child3 = cp.fork('./child.js');
var child4 = cp.fork('./child.js');
var server = require('net').createServer();
//父進程將接收到的請求分發給子進程
server.listen(8080,function(){
  child1.send('server',server);
  child2.send('server',server);
  child3.send('server',server);
  child4.send('server',server);
  //發送完句柄后關閉監聽
  server.close();
});

child.js

var http = require('http');
var serverInChild = http.createServer(function(req,res){
 res.end('I am child.Id:'+process.pid);
});
//子進程收到父進程傳遞的句柄(即客戶端與服務器的socket連接對象)
process.on('message',function(m,serverInParent){
  if(m==='server'){
    //處理與客戶端的連接
    serverInParent.on('connection',function(socket){
      //交給http服務來處理
      serverInChild.emit('connection',socket);
    });
  }
});

當運行上述代碼,此時查看8080端口占有會有如下結果:

wang@wang ~/code/nodeStudy $ lsof -i:8080
COMMAND  PID USER   FD   TYPE DEVICE SIZE/OFF NODE NAME
node    5120 wang   11u  IPv6  44561      0t0  TCP *:http-alt (LISTEN)
node    5126 wang   11u  IPv6  44561      0t0  TCP *:http-alt (LISTEN)
node    5127 wang   11u  IPv6  44561      0t0  TCP *:http-alt (LISTEN)
node    5133 wang   11u  IPv6  44561      0t0  TCP *:http-alt (LISTEN)

運行curl查看結果:

wang@wang ~/code/nodeStudy $ curl 192.168.10.104:8080
I am child.Id:5127
wang@wang ~/code/nodeStudy $ curl 192.168.10.104:8080
I am child.Id:5133
wang@wang ~/code/nodeStudy $ curl 192.168.10.104:8080
I am child.Id:5120
wang@wang ~/code/nodeStudy $ curl 192.168.10.104:8080
I am child.Id:5126
wang@wang ~/code/nodeStudy $ curl 192.168.10.104:8080
I am child.Id:5133
wang@wang ~/code/nodeStudy $ curl 192.168.10.104:8080
I am child.Id:5126

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM