詳解從Node.js的child_process模塊來學(xué)習(xí)父子進程之間的通信
child_process模塊提供了和popen(3)一樣的方式來產(chǎn)生自進程,這個功能主要是通過child_process.spawn函數(shù)來提供的:
const spawn = require('child_process').spawn;
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(`child process exited with code $[code]`);
});
默認(rèn)情況下,Node.js進程和子進程之間的stdin,stdout,stderr管道是已經(jīng)存在的。通常情況下這個方法可以以一種非阻塞的方式來傳遞數(shù)據(jù)。(注意,有些程序在內(nèi)部使用line-buffered I/O。因為這也不會影響到Node.js,這意味著傳遞給子進程的數(shù)據(jù)可能不會馬上消費)。
chid-process的spawn方法是通過一種異步的方式來產(chǎn)生自進程的,因此不會阻塞Node.js的事件循環(huán),然而child-process.spawnSync方法是同步的,他會阻塞事件循環(huán)只到產(chǎn)生的進程退出或者終止。
child_process.exec:產(chǎn)生一個shell客戶端,然后使用shell來執(zhí)行程序,當(dāng)完成的時候傳遞給回調(diào)函數(shù)一個stdout和stderr
child_process.execFile:和exec相似,但是他不會馬上產(chǎn)生一個shell
child_process.fork:產(chǎn)生一個新的Node.js進程,同時執(zhí)行一個特定的模塊來產(chǎn)生IPC通道,進而在父進程和子進程之間傳輸數(shù)據(jù)
child_process.execSync:和exec不同之處在于會阻塞Node.js的事件循環(huán),然而child-process
child_process.execFileSync:和execFile不同之處在于會阻塞Node.js的事件循環(huán),然而child-process在一些特殊情況下,例如自動化shell腳本,同步的方法可能更加有用。多數(shù)情況下,同步的方法對性能產(chǎn)生重要的影響,因為他會阻塞事件循環(huán)
child_process.spawn(), child_process.fork(), child_process.exec(), and child_process.execFile()都是異步的API。每一個方法都會產(chǎn)生一個ChildProcess實例,而且這個對象實現(xiàn)了Node.js的EventEmitter這個API,于是父進程可以注冊函數(shù),在子進程的特定事件觸發(fā)的時候被調(diào)用。 child_process.exec() 和 child_process.execFile()可以指定一個可選的callback函數(shù),這個函數(shù)在子進程終止的時候被調(diào)用。
在windows平臺上執(zhí)行.bat和.cmd:
child_process.exec和child_process.execFile的不同之處可能隨著平臺不同而有差異。在Unit/Linux/OSX平臺上execFile更加高效,因為他不會產(chǎn)生shell。在windows上,.bat/.cmd在沒有終端的情況下是無法執(zhí)行的,因此就無法使用execFile(child_process.spawn也無法使用)。在window上,.bat/.cmd可以使用spawn方法,同時指定一個shell選項;或者使用child_process.exec或者通過產(chǎn)生一個cmd.exe同時把.bat/.cmd文件傳遞給它作為參數(shù)(child_process.exec就是這么做的)。
const spawn = require('child_process').spawn;
const bat = spawn('cmd.exe', ['/c', 'my.bat']);//使用shell方法指定一個shell選項
bat.stdout.on('data', (data) => {
console.log(data);
});
bat.stderr.on('data', (data) => {
console.log(data);
});
bat.on('exit', (code) => {
console.log(`Child exited with code $[code]`);
}); 或者也可以使用如下的方式:
const exec = require('child_process').exec;//產(chǎn)生exec,同時傳入.bat文件
exec('my.bat', (err, stdout, stderr) => {
if (err) {
console.error(err);
return;
}
console.log(stdout);
}); child_process.exec(command[, options][, callback])
其中options中的maxBuffer參數(shù)表示stdout/stderr允許的最大的數(shù)據(jù)量,如果超過了數(shù)據(jù)量那么子進程就會被終止,默認(rèn)是200*1024比特;killSignal默認(rèn)是'SIGTERM'。其中回調(diào)函數(shù)當(dāng)進程結(jié)束時候調(diào)用,參數(shù)分別為error,stdout,stderr。這個方法返回的是一個ChildProcess對象。
const exec = require('child_process').exec;
const child = exec('cat *.js bad_file | wc -l',
(error, stdout, stderr) => {
console.log(`stdout: ${stdout}`);
console.log(`stderr: ${stderr}`);
if (error !== null) {
console.log(`exec error: ${error}`);
}
});
上面的代碼產(chǎn)生一個shell,然后使用這個shell執(zhí)行命令,同時對產(chǎn)生的結(jié)果進行緩存。其中回調(diào)函數(shù)中的error.code屬性表示子進程的exit code,error.signal表示結(jié)束這個進程的信號,任何非0的代碼表示出現(xiàn)了錯誤。默認(rèn)的options參數(shù)值如下:
{
encoding: 'utf8',
timeout: 0,
maxBuffer: 200*1024,//stdout和stderr允許的最大的比特數(shù)據(jù),超過她子進程就會被終止
cwd: null,
env: null
} 如果timeout非0,那么父進程就會發(fā)送信號,這個信號通過killSignal指定,默認(rèn)是"SIGTERM"來終結(jié)子進程,如果子進程超過了timeout指定的時間。注意:和POSIX系統(tǒng)上調(diào)用exec方法不一樣的是,child_process.exec不會替換當(dāng)前線程,而是使用一個shell去執(zhí)行命令
child_process.execFile(file[, args][, options][, callback])
其中file表示需要執(zhí)行的文件。 child_process.execFile()和exec很相似,但是這個方法不會產(chǎn)生一個shell。指定的可執(zhí)行文件會馬上產(chǎn)生一個新的線程,因此其效率比child_process.exec高。
const execFile = require('child_process').execFile;
const child = execFile('node', ['--version'], (error, stdout, stderr) => {
if (error) {
throw error;
}
console.log(stdout);
});
因為不會產(chǎn)生shell,一些I/O redirection和file globbing這些行為不支持
child_process.fork(modulePath[, args][, options])
最后,我們來看看子進程和父進程之間是如何通信的:
服務(wù)器端的代碼:
var http = require('http');
var cp = require('child_process');
var server = http.createServer(function(req, res) {
var child = cp.fork(__dirname + '/cal.js');
//每個請求都單獨生成一個新的子進程
child.on('message', function(m) {
res.end(m.result + '\n');
});
//為其指定message事件
var input = parseInt(req.url.substring(1));
//和postMessage很類似,不過這里是通過send方法而不是postMessage方法來完成的
child.send({input : input});
});
server.listen(8000);
子進程的代碼:
function fib(n) {
if (n < 2) {
return 1;
} else {
return fib(n - 2) + fib(n - 1);
}
}
//接受到send傳遞過來的參數(shù)
process.on('message', function(m) {
//console.log(m);
//打印{ input: 9 }
process.send({result: fib(m.input)});
}); child_process.spawn(command[, args][, options])
{
cwd: undefined, //產(chǎn)生這個進程的工作目錄,默認(rèn)繼承當(dāng)前的工作目錄
env: process.env//這個參數(shù)用于指定對于新的進程可見的環(huán)境變量,默認(rèn)是process.env
}其中cwd用于指定子進程產(chǎn)生的工作目錄,如果沒有指定表示的就是當(dāng)前工作目錄。env用于指定新進程的環(huán)境變量,默認(rèn)為process.env。下面的例子展示了使用ls -lh/usr來獲取stdout,stderr以及exit code:
const spawn = require('child_process').spawn;
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(`child process exited with code $[code]`);
});
下面是一個很詳細(xì)的運行"ps ax|grep ssh"的例子:
const spawn = require('child_process').spawn;
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 process exited with code $[code]`);
}
grep.stdin.end();
});
grep.stdout.on('data', (data) => {
console.log(`${data}`);
});
grep.stderr.on('data', (data) => {
console.log(`grep stderr: ${data}`);
});
grep.on('close', (code) => {
if (code !== 0) {
console.log(`grep process exited with code $[code]`);
}
});
用下面的例子來檢查錯誤的執(zhí)行程序:
const spawn = require('child_process').spawn;
const child = spawn('bad_command');
child.on('error', (err) => {
console.log('Failed to start child process.');
}); options.detached:
在windows上,把這個參數(shù)設(shè)置為true的話,這時候如果父進程退出了那么子進程還會繼續(xù)運行,而且子進程有自己的console window。如果把子進程設(shè)置了這個為true,那么就不能設(shè)置為false了。在非window平臺上,如果把這個設(shè)置為true,子進程就會成為進程組合和session的leader,這時候子進程在父進程退出以后會繼續(xù)執(zhí)行,不管子進程是否detached。可以參見setsid(2)。
const fs = require('fs');
const spawn = require('child_process').spawn;
const out = fs.openSync('./out.log', 'a');
const err = fs.openSync('./out.log', 'a');
const child = spawn('prg', [], {
detached: true,//依賴于父進程
stdio: [ 'ignore', out, err ]
});
child.unref();//允許父進程單獨退出,不用等待子進程
當(dāng)使用了detached選項去產(chǎn)生一個長期執(zhí)行的進程,這時候如果父進程退出了那么子進程就不會繼續(xù)執(zhí)行了,除非指定了一個stdio配置(不和父進程之間有聯(lián)系)。如果父進程的stdio是繼承的,那么子進程依然會和控制終端之間保持關(guān)系。
options.stdio
這個選項用于配置父進程和子進程之間的管道。默認(rèn)情況下,子進程的stdin,stdout,stderr導(dǎo)向了ChildProcess這個對象的child.stdin,child.stdout,child.stderr流,這和設(shè)置stdio為['pipe','pipe','pipe']是一樣的。stdio可以是下面的任何一個字符串:
'pipe':相當(dāng)于['pipe','pipe','pipe'],為默認(rèn)選項
'ignore':相當(dāng)于['ignore','ignore','ignore']
'inherit':相當(dāng)于[process.stdin,process.stdout,process.stderr]或者[0,1,2]
一般情況下,stdio是一個數(shù)組,每一個選項對應(yīng)于子進程的fd。其中0,1,2分別對應(yīng)于stdin,stdout,stderr。如果還設(shè)置了多于的fds那么就會用于創(chuàng)建父進程和子進程之間的額外的管道,可以是下面的任何一個值:
'pipe':為子進程和父進程之間創(chuàng)建一個管道。父進程管道的末端會作為child_process對象的ChildProcess.stdio[fd]而存在。fds0-2創(chuàng)建的管道在ChildProcess.stdin,ChildProcess.stdout,ChildProcess.stderr也是存在的
'ipc':用于創(chuàng)建IPC通道用于在父進程和子進程之間傳輸消息或者文件描述符。ChildProcess對象最多有一個IPC stdio文件描述符,使用這個配置可以啟用ChildProcess的send方法,如果父進程在文件描述符里面寫入了JSON對象,那么ChildProcess.on("message")事件就會在父進程上觸發(fā)。如果子進程是Node.js進程,那么ipc配置就會啟用子進程的process.send(), process.disconnect(), process.on('disconnect'), and process.on('message')方法。
'ignore':讓Node.js子進程忽視文件描述符。因為Node.js總是會為子進程開啟fds0-2,設(shè)置為ignore就會導(dǎo)致Node.js去開啟/dev/null,同時把這個值設(shè)置到子進程的fd上面。
'strem':和子進程之間共享一個可讀或者可寫流,比如file,socket,pipe。這個stream的文件描述符和子進程的文件描述符fd是重復(fù)的。注意:流必須有自己的文件描述符
正整數(shù):表示父進程的打開的文件描述符。和stream對象可以共享一樣,這個文件描述符在父子進程之間也是共享的
null/undefined:使用默認(rèn)值。stdio的fds0,1,2管道被創(chuàng)建(stdin,stdout,stderr)。對于fd3或者fdn,默認(rèn)為'ignore'
const spawn = require('child_process').spawn;
// Child will use parent's stdios
//使用父進程的stdios
spawn('prg', [], { stdio: 'inherit' });
//產(chǎn)生一個共享process.stderr的子進程
// Spawn child sharing only stderr
spawn('prg', [], { stdio: ['pipe', 'pipe', process.stderr] });
// Open an extra fd=4, to interact with programs presenting a
// startd-style interface.
spawn('prg', [], { stdio: ['pipe', null, null, null, 'pipe'] });
注意:當(dāng)子進程和父進程之間建立了IPC通道,同時子進程為Node.js進程,這時候開啟的具有IPC通道的子進程(使用unref)直到子進程注冊了一個disconnect的事件處理句柄process.on('disconnect'),這樣就會允許子進程正常退出而不會由于IPC通道的打開而持續(xù)運行。
Class: ChildProcess
這個類的實例是一個EventEmitters,用于代表產(chǎn)生的子進程。這個類的實例不能直接創(chuàng)建,必須使用 child_process.spawn(), child_process.exec(), child_process.execFile(), or child_process.fork()來完成
'close'事件:
其中code表示子進程退出的時候的退出碼;signal表示終止子進程發(fā)出的信號;這個事件當(dāng)子進程的stdio stream被關(guān)閉的時候觸發(fā),和exit事件的區(qū)別是多個進程可能共享同一個stdio streams!(所以一個進程退出了也就是exit被觸發(fā)了,這時候close可能不會觸發(fā))
'exit'事件:
其中code表示子進程退出的時候的退出碼;signal表示終止子進程發(fā)出的信號。這個事件當(dāng)子進程結(jié)束的時候觸發(fā),如果進程退出了那么code表示進程退出的exit code,否則沒有退出就是null。如果進程是由于收到一個信號而終止的,那么signal就是這個信號,是一個string,默認(rèn)為null。
注意:如果exit事件被觸發(fā)了,子進程的stdio stream可能還是打開的;Node.js為SUGUBT,SIGTERM創(chuàng)建信號處理器,而且Node.js進程在收到信號的時候不會馬上停止。Node.js會進行一系列的清理工作,然后才re-raise handled signal。見waitpid(2)
'disconnect'事件:
在子進程或者父進程中調(diào)用ChildProcess.disconnect()方法的時候會觸發(fā)。這時候就不能再發(fā)送和接受信息了,這是ChildProcess.connected就是false了
'error'事件:
當(dāng)進程無法產(chǎn)生的時候,進程無法終止的時候,為子進程發(fā)送消息失敗的時候就會觸發(fā)。注意:當(dāng)產(chǎn)生錯誤的時候exit事件可能會也可能不會觸發(fā)。如果你同時exit和error事件,那么就要注意是否會無意中多次調(diào)用事件處理函數(shù)
'message'事件:
message參數(shù)表示一個解析后的JSON對象或者初始值;sendHandle可以是一個net.Socket或者net.Server對象或者undefined。當(dāng)子進程調(diào)用process.send時候觸發(fā)
child.connected:
調(diào)用了disconnect方法后就會是false。表示是否可以在父進程和子進程之間發(fā)送和接受數(shù)據(jù),當(dāng)值為false就不能發(fā)送數(shù)據(jù)了
child.disconnect()
關(guān)閉子進程和父進程之間的IPC通道,這時候子進程可以正常退出如果沒有其他的連接使得他保持活動。這時候父進程的child.connected和子進程的process.connected就會設(shè)置為false,這時候不能傳輸數(shù)據(jù)了。disconnect事件當(dāng)進程沒有消息接收到的時候被觸發(fā),當(dāng)調(diào)用child.disconnect時候會立即觸發(fā)。注意:當(dāng)子進程為Node.js實例的時候如child_process.fork,這時候process.disconnect方法就會在子進程中調(diào)用然后關(guān)閉IPC通道。
child.kill([signal])
為子進程傳入消息,如果沒有指定參數(shù)那么就會發(fā)送SIGTERM信號,可以參見signal(7)來查看一系列信號
const spawn = require('child_process').spawn;
const grep = spawn('grep', ['ssh']);
grep.on('close', (code, signal) => {
console.log(
`child process terminated due to receipt of signal ${signal}`);
});
// Send SIGHUP to process
grep.kill('SIGHUP');
ChildProcess對象在無法傳輸信號的時候會觸發(fā)error事件。為一個已經(jīng)退出的子進程發(fā)送信號雖然無法報錯但是可能導(dǎo)致無法預(yù)料的結(jié)果。特別的,如果這個PID已經(jīng)被分配給另外一個進程那么這時候也會導(dǎo)致無法預(yù)料的結(jié)果。
child.pid:
返回進程的PID值
const spawn = require('child_process').spawn;
const grep = spawn('grep', ['ssh']);
console.log(`Spawned child pid: ${grep.pid}`);
grep.stdin.end();//通過grep.stdin.end結(jié)束
child.send(message[, sendHandle][, callback])
當(dāng)父子進程之間有了IPC通道,child.send就會為子進程發(fā)送消息,當(dāng)子進程為Node.js實例,那么可以用process.on('message')事件接收
父進程為:
const cp = require('child_process');
const n = cp.fork(`${__dirname}/sub.js`);
n.on('message', (m) => {
console.log('PARENT got message:', m);
});
n.send({ hello: 'world' }); 子進程為:
process.on('message', (m) => {
console.log('CHILD got message:', m);
});
process.send({ foo: 'bar' }); 子進程使用process.send方法為父進程發(fā)送消息。有一個特例,發(fā)送{cmd: 'NODE_foo'}。當(dāng)一個消息在他的cmd屬性中包含一個NODE_前綴會被看做使用Node.js核心(被Node.js保留)。這時候不會觸發(fā)子進程的process.on('message')。而是使用process.on('internalMessage')事件,同時會被Node.js內(nèi)部消費,一般不要使用這個方法。sendHandle這個用于給子進程傳入一個TCP Server或者一個socket,為process.on('message')回調(diào)的第二個參數(shù)接受。callback當(dāng)消息已經(jīng)發(fā)送,但是子進程還沒有接收到的時候觸發(fā),這個函數(shù)只有一個參數(shù)成功為null否則為Error對象。如果沒有指定callback同時消息也不能發(fā)送ChildProcess就會觸發(fā)error事件。當(dāng)子進程已經(jīng)退出就會出現(xiàn)這個情況。child.send返回false如果父子進程通道已經(jīng)關(guān)閉,或者積壓的沒有傳輸?shù)臄?shù)據(jù)超過一定的限度,否則這個方法返回true。這個callback方法可以用于實現(xiàn)流控制:
下面是發(fā)送一個Server的例子:
const child = require('child_process').fork('child.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);
});
子進程接受這個消息:
process.on('message', (m, server) => {
if (m === 'server') {
server.on('connection', (socket) => {
socket.end('handled by child');
});
}
}); 這時候server就被子進程和父進程共享了,一些連接可以被父進程處理,一些被子進程處理。上面的例子如果使用dgram那么就應(yīng)該message事件而不是connection,使用server.bind而不是server.listen,但是當(dāng)前只在UNIX平臺上可行。
下面的例子展示發(fā)送一個socket對象(產(chǎn)生兩個子進程,處理normal和special優(yōu)先級):
父進程為:
const normal = require('child_process').fork('child.js', ['normal']);
const special = require('child_process').fork('child.js', ['special']);
// Open up the server and send sockets to child
const server = require('net').createServer();
server.on('connection', (socket) => {
// If this is special priority
if (socket.remoteAddress === '74.125.127.100') {
special.send('socket', socket);
return;
}
// This is normal priority
normal.send('socket', socket);
});
server.listen(1337); 子進程為:
process.on('message', (m, socket) => {
if (m === 'socket') {
socket.end(`Request handled with ${process.argv[2]} priority`);
}
}); 當(dāng)socket被發(fā)送到子進程的時候那么父進程已經(jīng)無法追蹤這個socket什么時候被銷毀的。這時候.connections屬性就會成為null,因此我們建議不要使用.maxConnections。注意:這個方法在內(nèi)部JSON.stringify去序列化消息
child.stderr:
一個流對象,是一個可讀流表示子進程的stderr。他是child.stdio[2]的別名,兩者表示同樣的值。如果子進程是通過stdio[2]產(chǎn)生的,設(shè)置的不是pipe那么值就是undefined。
child.stdin:
一個可寫的流對象。注意:如果子進程等待讀取輸入,那么子進程會一直等到流調(diào)用了end方法來關(guān)閉的時候才會繼續(xù)讀取。如果子進程通過stdio[0]產(chǎn)生,同時不是設(shè)置的pipe那么值就是undefined。child.stdin是child.stdio[0]的別名,表示同樣的值。
const spawn = require('child_process').spawn;
const grep = spawn('grep', ['ssh']);
console.log(`Spawned child pid: ${grep.pid}`);
grep.stdin.end();//通過grep.stdin.end結(jié)束 child.stdio:
一個子進程管道的稀疏數(shù)組,是 child_process.spawn()函數(shù)的stdio選項,同時這個值被設(shè)置為pipe。child.stdio[0], child.stdio[1], 和 child.stdio[2]也可以通過child.stdin, child.stdout, 和 child.stderr訪問。下面的例子中只有子進程的fd1(也就是stdout)被設(shè)置為管道,因此只有父進程的child.stdio[1]是一個流,其他的數(shù)組中對象都是null:
const assert = require('assert');
const fs = require('fs');
const child_process = require('child_process');
const child = child_process.spawn('ls', {
stdio: [
0, // Use parents stdin for child
'pipe', // Pipe child's stdout to parent
fs.openSync('err.out', 'w') // Direct child's stderr to a file
]
});
assert.equal(child.stdio[0], null);
assert.equal(child.stdio[0], child.stdin);
assert(child.stdout);
assert.equal(child.stdio[1], child.stdout);
assert.equal(child.stdio[2], null);
assert.equal(child.stdio[2], child.stderr);
child.stdout:
一個可讀流,代表子進程的stdout。如果子進程產(chǎn)生的時候吧stdio[1]設(shè)置為除了pipe以外的任何數(shù),那么值就是undefined。其值和child.stdio[1]一樣
以上就是本文的全部內(nèi)容,希望對大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
創(chuàng)建簡單的node服務(wù)器實例(分享)
下面小編就為大家?guī)硪黄獎?chuàng)建簡單的node服務(wù)器實例(分享)。小編覺得挺不錯的,現(xiàn)在就分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2017-06-06
Node.js使用Express創(chuàng)建Web項目詳細(xì)教程
如果需要入門使用node.js進行web開發(fā),正在學(xué)習(xí) nodejs web開發(fā)指南 的和想快速了解node.js web開發(fā)模式的朋友,相信本文是有一定幫助意義的。2017-03-03
k8s node節(jié)點重新加入master集群的實現(xiàn)
這篇文章主要介紹了k8s node節(jié)點重新加入master集群的實現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2021-02-02
module.exports和exports使用誤區(qū)案例分析
module.exports和exports使用誤區(qū),使用require()模塊時,得到的永遠(yuǎn)都是module.exports指向的對象2023-04-04
使用pify實現(xiàn)Node.js回調(diào)函數(shù)的Promise化
使用pify庫,你可以非常便利地將任何遵循Node.js回調(diào)風(fēng)格的函數(shù)轉(zhuǎn)換為返回Promise對象的函數(shù),從而使得你的異步代碼更加清晰和易于維護,本文通過豐富的代碼示例引導(dǎo)你如何有效地利用pify,需要的朋友可以參考下2024-06-06

