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 相关文章推荐
Javascript入门学习资料收集整理篇
Jul 06 Javascript
一个用javascript写的select支持上下键、首字母筛选以及回车取值的功能
Sep 09 Javascript
js以对象为索引的关联数组
Jul 04 Javascript
JavaScript单元测试ABC
Apr 12 Javascript
js去除空格的12种实用方法
Nov 08 Javascript
kindeditor修复会替换script内容的问题
Apr 03 Javascript
JQuery显示隐藏DIV的方法及代码实例
Apr 16 Javascript
Jquery左右滑动插件之实现超级炫酷动画效果附源码下载
Dec 02 Javascript
JavaScript使用简单正则表达式的数据验证功能示例
Jan 13 Javascript
浅谈angularjs $http提交数据探索
Jan 20 Javascript
jsonp跨域请求详解
Jul 13 Javascript
jq源码解析之绑在$,jQuery上面的方法(实例讲解)
Oct 13 jQuery
详解使用 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
PHILIPS D1835/D1875的电路分析与打理
2021/03/02 无线电
在PHP中利用wsdl创建标准webservice的实现代码
2011/12/07 PHP
php全排列递归算法代码
2012/10/09 PHP
PHP实现基于文本的摩斯电码生成器
2016/01/11 PHP
php实现文章置顶功能的方法
2016/10/20 PHP
Thinkphp5结合layer弹窗定制操作结果页面
2017/07/07 PHP
基于CI(CodeIgniter)框架实现购物车功能的方法
2018/04/09 PHP
php实现通过stomp协议连接ActiveMQ操作示例
2020/02/23 PHP
ECMAScript 基础知识
2007/06/29 Javascript
通过JS 获取Mouse Position(鼠标坐标)的代码
2009/09/21 Javascript
jQuery 连续列表实现代码
2009/12/21 Javascript
自制轻量级仿jQuery.boxy对话框插件代码
2010/10/26 Javascript
另一个javascript小测验(代码集合)
2011/07/27 Javascript
JavaScript(js)设置默认输入焦点(focus)
2012/12/28 Javascript
基于jquery自定义的漂亮单选按钮RadioButton
2013/11/19 Javascript
javascript实现信息的显示和隐藏如注册页面
2013/12/03 Javascript
JS实现一个列表中包含上移下移删除等功能
2014/09/24 Javascript
纯javascript实现自动发送邮件
2015/10/21 Javascript
两行代码轻松搞定JavaScript日期验证
2016/08/03 Javascript
微信小程序 下拉菜单的实现
2017/04/06 Javascript
JS中的Replace()传入函数时的用法详解
2017/09/11 Javascript
vue读取本地的excel文件并显示在网页上方法示例
2019/05/29 Javascript
js 递归json树实现根据子id查父id的方法分析
2019/11/08 Javascript
基于element-ui封装可搜索的懒加载tree组件的实现
2020/05/22 Javascript
[10:21]2018DOTA2国际邀请赛寻真——Winstrike
2018/08/11 DOTA
[52:36]VGJ.S vs Serenity 2018国际邀请赛小组赛BO2 第一场 8.19
2018/08/21 DOTA
[00:59]PWL开团时刻DAY7——我在赶
2020/11/06 DOTA
跟老齐学Python之??碌某?? target=
2014/09/12 Python
Python玩转PDF的各种骚操作
2019/05/06 Python
Python3+PyCharm+Django+Django REST framework配置与简单开发教程
2021/02/16 Python
国际化的太阳镜及太阳镜配件零售商:Sunglass Hut
2016/07/26 全球购物
美国性感内衣店:Yandy
2018/06/12 全球购物
我看到了用指针调用函数的不同语法形式
2014/07/16 面试题
我的理想演讲稿
2014/04/30 职场文书
公司担保书格式范文
2014/05/12 职场文书
2016年中秋节慰问信
2015/12/01 职场文书