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 相关文章推荐
学习YUI.Ext第五日--做拖放Darg&amp;Drop
Mar 10 Javascript
jQuery EasyUI API 中文文档 - TreeGrid 树表格使用介绍
Nov 21 Javascript
JQuery操作Select的Options的Bug(IE8兼容性视图模式)
Apr 21 Javascript
使用js检测浏览器的实现代码
May 14 Javascript
setTimeout()递归调用不加引号出错的解决方法
Sep 05 Javascript
深入探秘jquery瀑布流的实现
Jan 30 Javascript
jquery实现简单Tab切换菜单效果
Jul 17 Javascript
从零开始学习Node.js系列教程之SQLite3和MongoDB用法分析
Apr 13 Javascript
重新认识vue之事件阻止冒泡的实现
Aug 02 Javascript
JS获取表格视图所选行号的ids过程解析
Feb 21 Javascript
JavaScript代码压缩工具UglifyJS和Google Closure Compiler的基本用法
Apr 13 Javascript
VueX模块的具体使用(小白教程)
Jun 05 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
模拟OICQ的实现思路和核心程序(二)
2006/10/09 PHP
用php代码限制国内IP访问我们网站
2015/09/26 PHP
php导出生成word的方法
2015/12/25 PHP
yii2.0框架场景的简单使用示例
2020/01/25 PHP
IE浏览器打印的页眉页脚设置解决方法
2009/12/08 Javascript
Javascript this 的一些学习总结
2012/08/31 Javascript
2012年开发人员的16款新鲜的jquery插件体验分享
2012/12/28 Javascript
js实现文本框中焦点在最后位置
2014/03/04 Javascript
jQuery.extend 函数及用法详细
2015/09/06 Javascript
jQuery实现左侧导航模块的显示与隐藏效果
2016/07/04 Javascript
JavaScript中return用法示例
2016/11/29 Javascript
JS交互点击WKWebView中的图片实现预览效果
2018/01/05 Javascript
利用jqprint插件打印页面内容的实现方法
2018/01/09 Javascript
详解Vue-cli webpack移动端自动化构建rem问题
2018/04/07 Javascript
js调用设备摄像头的方法
2018/07/19 Javascript
[00:32]2018DOTA2亚洲邀请赛iG出场
2018/04/03 DOTA
Python中使用Beautiful Soup库的超详细教程
2015/04/30 Python
一百行python代码将图片转成字符画
2021/02/19 Python
Python2和Python3中urllib库中urlencode的使用注意事项
2018/11/26 Python
python占位符输入方式实例
2019/05/27 Python
python使用socket 先读取长度,在读取报文内容示例
2019/09/26 Python
如何基于Python代码实现高精度免费OCR工具
2020/06/18 Python
Python importlib模块重载使用方法详解
2020/10/13 Python
html5实现微信打飞机游戏
2014/03/27 HTML / CSS
约瑟夫·特纳男装:Joseph Turner
2017/10/10 全球购物
Fresh馥蕾诗英国官网:法国LVMH集团旗下高端天然护肤品牌
2018/11/01 全球购物
Quiksilver美国官网:始于1969年的优质冲浪服和滑雪板外套
2020/04/20 全球购物
Python中pass语句的作用是什么
2016/06/01 面试题
高二政治教学反思
2014/02/01 职场文书
药学专业学生的自我评价分享
2014/02/06 职场文书
学雷锋树新风演讲稿
2014/05/10 职场文书
债务追讨授权委托书范本
2014/10/16 职场文书
小学音乐课教学反思
2016/02/18 职场文书
创业计划书之水果店
2019/07/18 职场文书
php远程请求CURL案例(爬虫、保存登录状态)
2021/04/01 PHP
解决vue-router的beforeRouteUpdate不能触发
2022/04/14 Vue.js