Node.js中的child_process模块详解
前言
本文主要给大家介绍了关于Node.js中child_process模块的相关内容,在介绍child_process模块之前,先来看一个例子。
consthttp=require('http');
constlongComputation=()=>{
letsum=0;
for(leti=0;i<1e10;i++){
sum+=i;
};
returnsum;
};
constserver=http.createServer();
server.on('request',(req,res)=>{
if(req.url==='/compute'){
constsum=longComputation();
returnres.end(`Sumis${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');
constchild=spawn('pwd');
pwd是shell的命令,用于获取当前的目录,上面的代码执行完控制台并没有任何的信息输出,这是为什么呢?
控制台之所以不能看到输出信息的原因是由于子进程有自己的stdio流(stdin、stdout、stderr),控制台的输出是与当前进程的stdio绑定的,因此如果希望看到输出信息,可以通过在子进程的stdout与当前进程的stdout之间建立管道实现
child.stdout.pipe(process.stdout);
也可以监听事件的方式(子进程的stdio流都是实现了EventEmitterAPI的,所以可以添加事件监听)
child.stdout.on('data',function(data){
process.stdout.write(data);
});
在Node.js代码里使用的console.log其实底层依赖的就是process.stdout
除了建立管道之外,还可以通过子进程和当前进程共用stdio的方式来实现
const{spawn}=require('child_process');
constchild=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');
constchild=spawn('ls-l');
child.stdout.pipe(process.stdout);
//报错
events.js:167
thrower;//Unhandled'error'event
^
Error:spawnls-lENOENT
atProcess.ChildProcess._handle.onexit(internal/child_process.js:229:19)
atonErrorNT(internal/child_process.js:406:16)
atprocess._tickCallback(internal/process/next_tick.js:63:19)
atFunction.Module.runMain(internal/modules/cjs/loader.js:746:11)
atstartup(internal/bootstrap/node.js:238:19)
atbootstrapNodeJSCore(internal/bootstrap/node.js:572:3)
Emitted'error'eventat:
atProcess.ChildProcess._handle.onexit(internal/child_process.js:235:12)
atonErrorNT(internal/child_process.js:406:16)
[...linesmatchingoriginalstacktrace...]
atbootstrapNodeJSCore(internal/bootstrap/node.js:572:3)
如果需要传递参数的话,应该采用数组的方式传入
const{spawn}=require('child_process');
constchild=spawn('ls',['-l']);
child.stdout.pipe(process.stdout);
如果要执行ls-l|wc-l命令的话可以采用创建两个spawn命令的方式
const{spawn}=require('child_process');
constchild=spawn('ls',['-l']);
constchild2=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也可以通过配置shelloption的方式来创建子shell进而支持管道命令,如下所示
const{spawn,execFile}=require('child_process');
constchild=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');
constchild=spawn('ls-l|wc-l',{
shell:true,
cwd:'/usr'
});
child.stdout.pipe(process.stdout);
env用于指定子进程的环境变量(如果不指定的话,默认获取当前进程的环境变量)
const{spawn,execFile,fork}=require('child_process');
constchild=spawn('echo$NODE_ENV',{
shell:true,
cwd:'/usr'
});
child.stdout.pipe(process.stdout);
NODE_ENV=randalnodeb.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=randalnodeb.js
//输出结果
randal
detached用于将子进程与父进程断开连接
例如假设存在一个长时间运行的子进程
//timer.js
while(true){
}
但是主进程并不需要长时间运行的话就可以用detached来断开二者之间的连接
const{spawn,execFile,fork}=require('child_process');
constchild=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');
constforked=fork('child.js');
forked.on('message',(msg)=>{
console.log('Messagefromchild',msg);
});
forked.send({hello:'world'});
child.js
process.on('message',(msg)=>{
console.log('Messagefromparent:',msg);
});
letcounter=0;
setInterval(()=>{
process.send({counter:counter++});
},1000);
nodeparent.js
//输出结果
Messagefromparent:{hello:'world'}
Messagefromchild{counter:0}
Messagefromchild{counter:1}
Messagefromchild{counter:2}
Messagefromchild{counter:3}
Messagefromchild{counter:4}
Messagefromchild{counter:5}
Messagefromchild{counter:6}
回到本文初的那个问题,我们就可以将密集计算的逻辑放到单独的js文件中,然后再通过fork的方式来计算,等计算完成时再通知主进程计算结果,这样避免主进程繁忙的情况了。
compute.js
constlongComputation=()=>{
letsum=0;
for(leti=0;i<1e10;i++){
sum+=i;
};
returnsum;
};
process.on('message',(msg)=>{
constsum=longComputation();
process.send(sum);
});
index.js
consthttp=require('http');
const{fork}=require('child_process');
constserver=http.createServer();
server.on('request',(req,res)=>{
if(req.url==='/compute'){
constcompute=fork('compute.js');
compute.send('start');
compute.on('message',sum=>{
res.end(`Sumis${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');
constchild=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');
constchild=exec('nodetimer.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');
constchild=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});
constcp=require('child_process');
constn=cp.fork(`${__dirname}/sub.js`);
n.on('message',(m)=>{
console.log('gotmessage:',m);//gotmessage:{foo:'bar',baz:null}
});
关于message有一种特殊情况要注意,下面的message并不会被子进程接收到
const{fork}=require('child_process');
constforked=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.org/node-js-child-processes-everything-you-need-to-know-e69498fe970a
https://nodejs.org/dist/latest-v10.x/docs/api/child_process.html
总结
以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对毛票票的支持。
声明:本文内容来源于网络,版权归原作者所有,内容由互联网用户自发贡献自行上传,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任。如果您发现有涉嫌版权的内容,欢迎发送邮件至:czq8825#qq.com(发邮件时,请将#更换为@)进行举报,并提供相关证据,一经查实,本站将立刻删除涉嫌侵权内容。