Node.js中的child_process模块详解


Posted in Javascript onJune 08, 2018

前言

本文主要给大家介绍了关于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.org/node-js-child-processes-everything-you-need-to-know-e69498fe970a
https://nodejs.org/dist/latest-v10.x/docs/api/child_process.html

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对三水点靠木的支持。

Javascript 相关文章推荐
用ADODB.Stream转换
Jan 22 Javascript
JavaScript基本编码模式小结
May 23 Javascript
ExtJS DOM元素操作经验分享
Aug 28 Javascript
jQuery实现分隔条左右拖动功能
Nov 21 Javascript
全面解析Bootstrap中scrollspy(滚动监听)的使用方法
Jun 06 Javascript
javascript创建对象的3种方法
Nov 02 Javascript
Vue 固定头 固定列 点击表头可排序的表格组件
Nov 25 Javascript
Vue常用指令V-model用法
Mar 08 Javascript
解决angular双向绑定无效果,ng-model不能正常显示的问题
Oct 02 Javascript
微信小程序实现评论功能
Nov 28 Javascript
Vue2.x通用编辑组件的封装及应用详解
May 28 Javascript
vue远程加载sfc组件思路详解
Dec 25 Javascript
详解使用 Node.js 开发简单的脚手架工具
Jun 08 #Javascript
使用JavaScript生成罗马字符的实例代码
Jun 08 #Javascript
jQuery实现表单动态加减、ajax表单提交功能
Jun 08 #jQuery
Node.js中你不可不精的Stream(流)
Jun 08 #Javascript
用react-redux实现react组件之间数据共享的方法
Jun 08 #Javascript
vue指令只能输入正数并且只能输入一个小数点的方法
Jun 08 #Javascript
bootstrap treeview 树形菜单带复选框及级联选择功能
Jun 08 #Javascript
You might like
用PHP和ACCESS写聊天室(四)
2006/10/09 PHP
php 数组动态添加实现代码(最土团购系统的价格排序)
2011/12/30 PHP
PHP引用符&amp;的用法详细解析
2013/08/22 PHP
PHP实现简单实用的验证码类
2015/07/29 PHP
详解Grunt插件之LiveReload实现页面自动刷新(两种方案)
2015/07/31 PHP
PHPExcel中文帮助手册|PHPExcel使用方法(分享)
2017/06/09 PHP
关于图片按比例自适应缩放的js代码
2011/10/30 Javascript
JS截取字符串常用方法详细整理
2013/10/28 Javascript
JavaScript function 的 length 属性使用介绍
2014/09/15 Javascript
node.js中的fs.lstatSync方法使用说明
2014/12/16 Javascript
javascript实现禁止复制网页内容
2014/12/16 Javascript
jQuery检测输入的字符串包含的中英文的数量
2015/04/17 Javascript
jquery动感漂浮导航菜单代码分享
2020/04/15 Javascript
JS实现浏览器状态栏文字闪烁效果的方法
2015/10/27 Javascript
python 将字符串转换成字典dict
2013/03/24 Python
python正则表达式match和search用法实例
2015/03/26 Python
解决python通过cx_Oracle模块连接Oracle乱码的问题
2018/10/18 Python
Python实现定期检查源目录与备份目录的差异并进行备份功能示例
2019/02/27 Python
Python openpyxl模块原理及用法解析
2020/01/19 Python
python3.8动态人脸识别的实现示例
2020/09/21 Python
python+selenium爬取微博热搜存入Mysql的实现方法
2021/01/27 Python
怀俄明州飞钓:Platte River Fly Shop
2017/12/28 全球购物
销售文员的岗位职责
2013/11/20 职场文书
毕业生个人求职自荐信
2014/02/26 职场文书
教育技术职业规划范文
2014/03/04 职场文书
小学生评语集锦
2014/04/18 职场文书
应届毕业生自荐书
2014/06/18 职场文书
体育课外活动总结
2014/07/08 职场文书
2014年度安全生产目标管理责任书
2014/07/25 职场文书
超市店庆活动方案
2014/08/31 职场文书
介绍信怎么写
2015/01/30 职场文书
2015年出纳年终工作总结
2015/05/14 职场文书
党支部综合考察意见
2015/06/01 职场文书
2015年学校教科室工作总结
2015/07/20 职场文书
2015小学教育教学工作总结
2015/07/21 职场文书
Mysql开启外网访问
2022/05/15 MySQL