在nodejs中建立child process
簡介
nodejs的main event loop是單執行緒的,nodejs本身也維護著Worker Pool用來處理一些耗時的操作,我們還可以通過使用nodejs提供的worker_threads來手動建立新的執行緒來執行自己的任務。
本文將會介紹一種新的執行nodejs任務的方式,child process。
child process
lib/child_process.js提供了child_process模組,通過child_process我們可以建立子程式。
注意,worker_threads建立的是子執行緒,而child_process建立的是子程式。
在child_process模組中,可以同步建立程式也可以非同步建立程式。同步建立方式只是在非同步建立的方法後面加上Sync。
建立出來的程式用ChildProcess類來表示。
我們看下ChildProcess的定義:
interface ChildProcess extends events.EventEmitter {
stdin: Writable | null;
stdout: Readable | null;
stderr: Readable | null;
readonly channel?: Pipe | null;
readonly stdio: [
Writable | null, // stdin
Readable | null, // stdout
Readable | null, // stderr
Readable | Writable | null | undefined, // extra
Readable | Writable | null | undefined // extra
];
readonly killed: boolean;
readonly pid: number;
readonly connected: boolean;
readonly exitCode: number | null;
readonly signalCode: NodeJS.Signals | null;
readonly spawnargs: string[];
readonly spawnfile: string;
kill(signal?: NodeJS.Signals | number): boolean;
send(message: Serializable, callback?: (error: Error | null) => void): boolean;
send(message: Serializable, sendHandle?: SendHandle, callback?: (error: Error | null) => void): boolean;
send(message: Serializable, sendHandle?: SendHandle, options?: MessageOptions, callback?: (error: Error | null) => void): boolean;
disconnect(): void;
unref(): void;
ref(): void;
/**
* events.EventEmitter
* 1. close
* 2. disconnect
* 3. error
* 4. exit
* 5. message
*/
...
}
可以看到ChildProcess也是一個EventEmitter,所以它可以傳送和接受event。
ChildProcess可以接收到event有5種,分別是close,disconnect,error,exit和message。
當呼叫父程式中的 subprocess.disconnect() 或子程式中的 process.disconnect() 後會觸發 disconnect 事件。
當出現無法建立程式,無法kill程式和向子程式傳送訊息失敗的時候都會觸發error事件。
當子程式結束後時會觸發exit事件。
當子程式的 stdio 流被關閉時會觸發 close 事件。 注意,close事件和exit事件是不同的,因為多個程式可能共享同一個stdio,所以傳送exit事件並不一定會觸發close事件。
看一個close和exit的例子:
const { spawn } = require('child_process');
const ls = spawn('ls', ['-lh', '/usr']);
ls.stdout.on('data', (data) => {
console.log(`stdout: ${data}`);
});
ls.on('close', (code) => {
console.log(`子程式使用程式碼 ${code} 關閉所有 stdio`);
});
ls.on('exit', (code) => {
console.log(`子程式使用程式碼 ${code} 退出`);
});
最後是message事件,當子程式使用process.send() 傳送訊息的時候就會被觸發。
ChildProcess中有幾個標準流屬性,分別是stderr,stdout,stdin和stdio。
stderr,stdout,stdin很好理解,分別是標準錯誤,標準輸出和標準輸入。
我們看一個stdout的使用:
const { spawn } = require('child_process');
const subprocess = spawn('ls');
subprocess.stdout.on('data', (data) => {
console.log(`接收到資料塊 ${data}`);
});
stdio實際上是stderr,stdout,stdin的集合:
readonly stdio: [
Writable | null, // stdin
Readable | null, // stdout
Readable | null, // stderr
Readable | Writable | null | undefined, // extra
Readable | Writable | null | undefined // extra
];
其中stdio[0]表示的是stdin,stdio[1]表示的是stdout,stdio[2]表示的是stderr。
如果在通過stdio建立子程式的時候,這三個標準流被設定為除pipe之外的其他值,那麼stdin,stdout和stderr將為null。
我們看一個使用stdio的例子:
const assert = require('assert');
const fs = require('fs');
const child_process = require('child_process');
const subprocess = child_process.spawn('ls', {
stdio: [
0, // 使用父程式的 stdin 用於子程式。
'pipe', // 把子程式的 stdout 通過管道傳到父程式 。
fs.openSync('err.out', 'w') // 把子程式的 stderr 定向到一個檔案。
]
});
assert.strictEqual(subprocess.stdio[0], null);
assert.strictEqual(subprocess.stdio[0], subprocess.stdin);
assert(subprocess.stdout);
assert.strictEqual(subprocess.stdio[1], subprocess.stdout);
assert.strictEqual(subprocess.stdio[2], null);
assert.strictEqual(subprocess.stdio[2], subprocess.stderr);
通常情況下父程式中維護了一個對子程式的引用計數,只有在當子程式退出之後父程式才會退出。
這個引用就是ref,如果呼叫了unref方法,則允許父程式獨立於子程式退出。
const { spawn } = require('child_process');
const subprocess = spawn(process.argv[0], ['child_program.js'], {
detached: true,
stdio: 'ignore'
});
subprocess.unref();
最後,我們看一下如何通過ChildProcess來傳送訊息:
subprocess.send(message[, sendHandle[, options]][, callback])
其中message就是要傳送的訊息,callback是傳送訊息之後的回撥。
sendHandle比較特殊,它可以是一個TCP伺服器或socket物件,通過將這些handle傳遞給子程式。子程式將會在message事件中,將該handle傳遞給Callback函式,從而可以在子程式中進行處理。
我們看一個傳遞TCP server的例子,首先看主程式:
const subprocess = require('child_process').fork('subprocess.js');
// 開啟 server 物件,併傳送該控制程式碼。
const server = require('net').createServer();
server.on('connection', (socket) => {
socket.end('由父程式處理');
});
server.listen(1337, () => {
subprocess.send('server', server);
});
再看子程式:
process.on('message', (m, server) => {
if (m === 'server') {
server.on('connection', (socket) => {
socket.end('由子程式處理');
});
}
});
可以看到子程式接收到了server handle,並且在子程式中監聽connection事件。
下面我們看一個傳遞socket物件的例子:
onst { fork } = require('child_process');
const normal = fork('subprocess.js', ['normal']);
const special = fork('subprocess.js', ['special']);
// 開啟 server,併傳送 socket 給子程式。
// 使用 `pauseOnConnect` 防止 socket 在被髮送到子程式之前被讀取。
const server = require('net').createServer({ pauseOnConnect: true });
server.on('connection', (socket) => {
// 特殊優先順序。
if (socket.remoteAddress === '74.125.127.100') {
special.send('socket', socket);
return;
}
// 普通優先順序。
normal.send('socket', socket);
});
server.listen(1337);
subprocess.js的內容:
process.on('message', (m, socket) => {
if (m === 'socket') {
if (socket) {
// 檢查客戶端 socket 是否存在。
// socket 在被髮送與被子程式接收這段時間內可被關閉。
socket.end(`請求使用 ${process.argv[2]} 優先順序處理`);
}
}
});
主程式建立了兩個subprocess,一個處理特殊的優先順序, 一個處理普通的優先順序。
非同步建立程式
child_process模組有4種方式可以非同步建立程式,分別是child_process.spawn()、child_process.fork()、child_process.exec() 和 child_process.execFile()。
先看一個各個方法的定義:
child_process.spawn(command[, args][, options])
child_process.fork(modulePath[, args][, options])
child_process.exec(command[, options][, callback])
child_process.execFile(file[, args][, options][, callback])
其中child_process.spawn是基礎,他會非同步的生成一個新的程式,其他的fork,exec和execFile都是基於spawn來生成的。
fork會生成新的Node.js 程式。
exec和execFile是以新的程式執行新的命令,並且帶有callback。他們的區別就在於在windows的環境中,如果要執行.bat或者.cmd檔案,沒有shell終端是執行不了的。這個時候就只能以exec來啟動。execFile是無法執行的。
或者也可以使用spawn。
我們看一個在windows中使用spawn和exec的例子:
// 僅在 Windows 上。
const { spawn } = require('child_process');
const bat = spawn('cmd.exe', ['/c', 'my.bat']);
bat.stdout.on('data', (data) => {
console.log(data.toString());
});
bat.stderr.on('data', (data) => {
console.error(data.toString());
});
bat.on('exit', (code) => {
console.log(`子程式退出,退出碼 ${code}`);
});
const { exec, spawn } = require('child_process');
exec('my.bat', (err, stdout, stderr) => {
if (err) {
console.error(err);
return;
}
console.log(stdout);
});
// 檔名中包含空格的指令碼:
const bat = spawn('"my script.cmd"', ['a', 'b'], { shell: true });
// 或:
exec('"my script.cmd" a b', (err, stdout, stderr) => {
// ...
});
同步建立程式
同步建立程式可以使用child_process.spawnSync()、child_process.execSync() 和 child_process.execFileSync() ,同步的方法會阻塞 Node.js 事件迴圈、暫停任何其他程式碼的執行,直到子程式退出。
通常對於一些指令碼任務來說,使用同步建立程式會比較常用。
本文作者:flydean程式那些事
本文連結:http://www.flydean.com/nodejs-childprocess/
本文來源:flydean的部落格
歡迎關注我的公眾號:「程式那些事」最通俗的解讀,最深刻的乾貨,最簡潔的教程,眾多你不知道的小技巧等你來發現!