Node.js child_process模塊解讀

在引見child_process模塊之前,先來看一個下面的代碼。

const http = require('http');
const longComputation = () => {
  let sum = 0;
  for (let i = 0; i < 1e10; i++) {
    sum += i;
  };
  return sum;
};
const server = http.createServer();
server.on('request', (req, res) => {
  if (req.url === '/compute') {
    const sum = longComputation();
    return res.end(`Sum is ${sum}`);
  } else {
    res.end('Ok')
  }
});

server.listen(3000);

能夠試一下運用上面的代碼啟動Node.js效勞,然後翻開兩個瀏覽器選項卡離別接見/compute和/,能夠發明node效勞接收到/compute要求時會舉行大批的數值盤算,致使沒法相應其他的要求(/)。

在Java語言中能夠經由歷程多線程的體式格局來處理上述的題目,然則Node.js在代碼實行的時刻是單線程的,那末Node.js應該怎樣處理上面的題目呢?實在Node.js能夠豎立一個子歷程實行麋集的cpu盤算使命(比方上面例子中的longComputation)來處理題目,而child_process模塊恰是用來豎立子歷程的。

豎立子歷程的體式格局

child_process供應了幾種豎立子歷程的體式格局

  • 異步體式格局:spawn、exec、execFile、fork
  • 同步體式格局:spawnSync、execSync、execFileSync

起首引見一下spawn要領

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

command: 要實行的指令
args:    通報參數
options: 設置項
const { spawn } = require('child_process');
const child = spawn('pwd');

pwd是shell的敕令,用於獵取當前的目次,上面的代碼實行完控制台並沒有任何的信息輸出,這是為何呢?

控制台之所以不能看到輸出信息的原因是由於子歷程有自身的stdio流(stdin、stdout、stderr),控制台的輸出是與當前歷程的stdio綁定的,因而假如願望看到輸出信息,能夠經由歷程在子歷程的stdout 與當前歷程的stdout之間豎立管道完成

child.stdout.pipe(process.stdout);

也能夠監聽事宜的體式格局(子歷程的stdio流都是完成了EventEmitter API的,所以能夠增加事宜監聽)

child.stdout.on('data', function(data) {
  process.stdout.write(data);
});

在Node.js代碼里運用的console.log實在底層依靠的就是process.stdout

除了豎立管道以外,還能夠經由歷程子歷程和當前歷程共用stdio的體式格局來完成

const { spawn } = require('child_process');
const child = spawn('pwd', {
  stdio: 'inherit'
});

stdio選項用於設置父歷程和子歷程之間豎立的管道,由於stdio管道有三個(stdin, stdout, stderr)因而stdio的三個能夠的值實際上是數組的一種簡寫

  • pipe 相當於[‘pipe’, ‘pipe’, ‘pipe’](默許值)
  • ignore 相當於[‘ignore’, ‘ignore’, ‘ignore’]
  • inherit 相當於[process.stdin, process.stdout, process.stderr]

由於inherit體式格局使得子歷程直接運用父歷程的stdio,因而能夠看到輸出

ignore用於疏忽子歷程的輸出(將/dev/null指定為子歷程的文件描述符了),因而當ignore時child.stdout是null。

spawn默許狀況下並不會豎立子shell來實行敕令,因而下面的代碼會報錯

const { spawn } = require('child_process');
const child = spawn('ls -l');
child.stdout.pipe(process.stdout);

// 報錯
events.js:167
      throw er; // Unhandled 'error' event
      ^

Error: spawn ls -l ENOENT
    at Process.ChildProcess._handle.onexit (internal/child_process.js:229:19)
    at onErrorNT (internal/child_process.js:406:16)
    at process._tickCallback (internal/process/next_tick.js:63:19)
    at Function.Module.runMain (internal/modules/cjs/loader.js:746:11)
    at startup (internal/bootstrap/node.js:238:19)
    at bootstrapNodeJSCore (internal/bootstrap/node.js:572:3)
Emitted 'error' event at:
    at Process.ChildProcess._handle.onexit (internal/child_process.js:235:12)
    at onErrorNT (internal/child_process.js:406:16)
    [... lines matching original stack trace ...]
    at bootstrapNodeJSCore (internal/bootstrap/node.js:572:3)

假如須要通報參數的話,應該採納數組的體式格局傳入

const { spawn } = require('child_process');
const child = spawn('ls', ['-l']);
child.stdout.pipe(process.stdout);

假如要實行ls -l | wc -l敕令的話能夠採納豎立兩個spawn敕令的體式格局

const { spawn } = require('child_process');
const child = spawn('ls', ['-l']);
const child2 = spawn('wc', ['-l']);
child.stdout.pipe(child2.stdin);
child2.stdout.pipe(process.stdout);

也能夠運用exec

const { exec } = require('child_process');
exec('ls -l | wc -l', function(err, stdout, stderr) {
  console.log(stdout);
});

由於exec會豎立子shell,所以能夠直接實行shell管道敕令。spawn採納流的體式格局來輸出敕令的實行效果,而exec也是將敕令的實行效果緩存起來一致放在回調函數的參數內里,因而exec只適用於敕令實行效果數據小的狀況。

實在spawn也能夠經由歷程設置shell option的體式格局來豎立子shell進而支撐管道敕令,以下所示

const { spawn, execFile } = require('child_process');
const child = spawn('ls -l | wc -l', {
  shell: true
});
child.stdout.pipe(process.stdout);

設置項除了stdio、shell以外另有cwd、env、detached等經常使用的選項

cwd用於修正敕令的實行目次

const { spawn, execFile, fork } = require('child_process');
const child = spawn('ls -l | wc -l', {
  shell: true,
  cwd: '/usr'
});
child.stdout.pipe(process.stdout);

env用於指定子歷程的環境變量(假如不指定的話,默許獵取當前歷程的環境變量)

const { spawn, execFile, fork } = require('child_process');
const child = spawn('echo $NODE_ENV', {
  shell: true,
  cwd: '/usr'
});
child.stdout.pipe(process.stdout);
NODE_ENV=randal node b.js

// 輸出效果
randal

假如指定env的話就會覆蓋掉默許的環境變量,以下

const { spawn, execFile, fork } = require('child_process');
spawn('echo $NODE_TEST $NODE_ENV', {
  shell: true,
  stdio: 'inherit',
  cwd: '/usr',
  env: {
    NODE_TEST: 'randal-env'
  }
});

NODE_ENV=randal node b.js

// 輸出效果
randal

detached用於將子歷程與父歷程斷開銜接

比方假定存在一個長時間運轉的子歷程

// timer.js
while(true) {

}

然則主歷程並不須要長時間運轉的話就能夠用detached來斷開二者之間的銜接

const { spawn, execFile, fork } = require('child_process');
const child = spawn('node', ['timer.js'], {
  detached: true,
  stdio: 'ignore'
});
child.unref();

當挪用子歷程的unref要領時,同時設置子歷程的stdio為ignore時,父歷程就能夠自力退出了

execFile與exec差別,execFile通經常使用於實行文件,而且並不會豎立子shell環境

fork要領是spawn要領的一個慣例,fork用於實行js文件豎立Node.js子歷程。而且fork體式格局豎立的子歷程與父歷程之間豎立了IPC通訊管道,因而子歷程和父歷程之間能夠經由歷程send的體式格局發送音訊。

注重:fork體式格局豎立的子歷程與父歷程是完整自力的,它具有零丁的內存,零丁的V8實例,因而並不引薦豎立許多的Node.js子歷程

fork體式格局的父子歷程之間的通訊參照下面的例子

parent.js

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

const forked = fork('child.js');

forked.on('message', (msg) => {
  console.log('Message from child', msg);
});

forked.send({ hello: 'world' });

child.js

process.on('message', (msg) => {
  console.log('Message from parent:', msg);
});

let counter = 0;

setInterval(() => {
  process.send({ counter: counter++ });
}, 1000);
node parent.js

// 輸出效果
Message from parent: { hello: 'world' }
Message from child { counter: 0 }
Message from child { counter: 1 }
Message from child { counter: 2 }
Message from child { counter: 3 }
Message from child { counter: 4 }
Message from child { counter: 5 }
Message from child { counter: 6 }

回到本文初的那個題目,我們就能夠將麋集盤算的邏輯放到零丁的js文件中,然後再經由歷程fork的體式格局來盤算,等盤算完成時再關照主歷程盤算效果,如許防止主歷程忙碌的狀況了。

compute.js

const longComputation = () => {
  let sum = 0;
  for (let i = 0; i < 1e10; i++) {
    sum += i;
  };
  return sum;
};

process.on('message', (msg) => {
  const sum = longComputation();
  process.send(sum);
});

index.js

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

const server = http.createServer();

server.on('request', (req, res) => {
  if (req.url === '/compute') {
    const compute = fork('compute.js');
    compute.send('start');
    compute.on('message', sum => {
      res.end(`Sum is ${sum}`);
    });
  } else {
    res.end('Ok')
  }
});

server.listen(3000);

監聽歷程事宜

經由歷程前述幾種體式格局豎立的子歷程都完成了EventEmitter,因而能夠針對歷程舉行事宜監聽

經常使用的事宜包括幾種:close、exit、error、message

close事宜當子歷程的stdio流封閉的時刻才會觸發,並非子歷程exit的時刻close事宜就一定會觸發,由於多個子歷程能夠共用雷同的stdio。

close與exit事宜的回調函數有兩個參數code和signal,code代碼子歷程終究的退出碼,假如子歷程是由於接收到signal信號停止的話,signal會紀錄子歷程接收的signal值。

先看一個一般退出的例子

const { spawn, exec, execFile, fork } = require('child_process');
const child = exec('ls -l', {
  timeout: 300
});
child.on('exit', function(code, signal) {
  console.log(code);
  console.log(signal);
});

// 輸出效果
0
null

再看一個由於接收到signal而停止的例子,運用之前的timer文件,運用exec實行的時刻並指定timeout

const { spawn, exec, execFile, fork } = require('child_process');
const child = exec('node timer.js', {
  timeout: 300
});
child.on('exit', function(code, signal) {
  console.log(code);
  console.log(signal);
});
// 輸出效果
null
SIGTERM

注重:由於timeout超時的時刻error事宜並不會觸發,而且當error事宜觸發時exit事宜並不一定會被觸發

error事宜的觸發前提有以下幾種:

  • 沒法豎立歷程
  • 沒法完畢歷程
  • 給歷程發送音訊失利

注重當代碼實行失足的時刻,error事宜並不會觸發,exit事宜會觸發,code為非0的非常退出碼

const { spawn, exec, execFile, fork } = require('child_process');
const child = exec('ls -l /usrs');
child.on('error', function(code, signal) {
  console.log(code);
  console.log(signal);
});
child.on('exit', function(code, signal) {
  console.log('exit');
  console.log(code);
  console.log(signal);
});

// 輸出效果
exit
1
null

message事宜適用於父子歷程之間豎立IPC通訊管道的時刻的信息通報,通報的歷程中會閱歷序列化與反序列化的步驟,因而終究接收到的並不一定與發送的數據相一致。

sub.js

process.send({ foo: 'bar', baz: NaN });
const cp = require('child_process');
const n = cp.fork(`${__dirname}/sub.js`);

n.on('message', (m) => {
  console.log('got message:', m);   // got message: { foo: 'bar', baz: null }
});

關於message有一種特殊狀況要注重,下面的message並不會被子歷程接收到

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

const forked = fork('child.js');

forked.send({
  cmd: "NODE_foo",
  hello: 'world'
});

當發送的音訊內里包括cmd屬性,而且屬性的值是以NODE_開首的話,如許的音訊是供應給Node.js自身保存運用的,因而並不會發出message事宜,而是會發出internalMessage事宜,開發者應該防止這類範例的音訊,而且應該防止監聽internalMessage事宜。

message除了發送字符串、object以外還支撐發送server對象和socket對象,正由於支撐socket對象才能夠做到多個Node.js歷程監聽雷同的端口號。

未完待續……

參考資料

https://medium.freecodecamp.o…

https://nodejs.org/dist/lates…

    原文作者:Randal
    原文地址: https://segmentfault.com/a/1190000015123556
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞