# Node.js 子进程
# cluster 模块
cluster 模块调用 fork 方法来创建子进程,该方法与 child_process 中的 fork 是同一个方法。 cluster 模块采用的是经典的主从模型,Cluster 会创建一个 master,然后根据你指定的数量复制出多个子进程,可以使用 cluster.isMaster 属性判断当前进程是 master 还是 worker(工作进程)。由 master 进程来管理所有的子进程,主进程不负责具体的任务处理,主要工作是负责调度和管理。
cluster 模块使用内置的负载均衡来更好地处理线程之间的压力,该负载均衡使用了 Round-robin
算法(也被称之为循环算法)。当使用 Round-robin 调度策略时,master accepts()所有传入的连接请求,然后将相应的 TCP 请求处理发送给选中的工作进程(该方式仍然通过 IPC 来进行通信)
开启多进程时候端口疑问讲解:如果多个 Node 进程监听同一个端口时会出现 Error:listen EADDRIUNS
的错误,而 cluster 模块为什么可以让多个子进程监听同一个端口呢?原因是 master 进程内部启动了一个 TCP 服务器,而真正监听端口的只有这个服务器,当来自前端的请求触发服务器的 connection 事件后,master 会将对应的 socket 具柄发送给子进程
const http = require('http');
const numCPUs = require('os').cpus().length;
const cluster = require('cluster');
if (cluster.isMaster) {
console.log('Master process id is', process.pid);
// fork workers
for (let i = 0; i < numCPUs; i++) {
cluster.fork();
}
cluster.on('exit', function (worker, code, signal) {
console.log('worker process died,id', worker.process.pid)
})
} else {
// Worker可以共享同一个TCP连接
// 这里是一个http服务器
http.createServer(function (req, res) {
res.writeHead(200);
res.end('hello word');
}).listen(8000);
}
# child_process 模块
我们可以使用 Node.js 的 child_process
模块很容易地衍生一个子进程,并且那些父子进程使用一个消息系统相互之间可以很容易地交流
child_process
模块使我们在一个运行良好的子进程内运行一些能够进入操作系统的命令
我们可以控制子进程的输入流
,并且监听它的输出流
。我们也可以控制传递给潜在的操作系统命令的参数,我们可以通过那个命令的输出做我们想做的事情。比如:可以将一个命令的输出作为另一个的输入(就像我们在 Linux 做的那样),因为那些命令的输入和输出都是使用流的形式呈现给我们。
在 Node 中,有 4 种方式创建子进程:spawn()
,fork()
,exec()
,execFile()
# spawn 函数
执行 spawn
函数(上面 child 对象)的结果是一个实现了 EventEmitter API 的 childProcess 实例。这意味着我们可以在这个 child 对象上面直接为某些事件注册处理器
const { spawn } = require('child_process');
const child = spawn('pwd');
child.on('exit', function (code, signal) {
console.log('child process exited with' + `code ${code} and signal ${signal}`);
});
上面的处理器接受到了子进程的退出 code
和 signal
,如果有的话,是被用来终止子进程的。如果子进程正常退出 signal 变量是 null
可以为 ChildProcess 实例注册的事件有disconnect
,error
, close
, message
, exit
- 当父进程手动调用
child.disconnect
的时候,disconnect
事件会被触发 - 如果进程不能被衍生(spawn)或者被 killed,
error
事件被触发 - 当一个子进程的
stdio
关闭的时候,close
事件被触发 - message 事件是最重要的一个。当子进程使用
process.send()
函数发送信息的时候,message 事件会被触发。这就是父子进程相会交流的方式
每一个子进程都会得到三个标准的输入输出流,我们可以通过 child.stdin
,child.stdout
和 child.stderr
进入
当那些流关闭之后,使用他们的子进程将会触发 close
事件。这个 close
事件和 exit
事件不同,因为多个子进程可能共享相同的 stdio
流,因此一个子进程退出不代表流关闭了
既然所有的流都是事件发射器,我们可以在那些被绑定到每一个子进程的 stdio 流监听不同的事件。不像在一个正常的进程中,在子进程中,stdout/stderr 流是可读的流,而 stdin 是可写的流
。基本上是主进程中相反类型的。可以在这些流上使用的事件是标准的。更重要的是,在可读流上,我们可以监听 data 事件,可以得到命令的输出和在执行命令时遇到的错误
child.stdout.on('data', (data) => {
console.log(`child stdout: ${data}`)
});
child.stderr.on('data', (data) => {
console.error(`stderror ${data}`);
});
上面的两个处理器将会在主进程的 stdout 和 stderr 的两种情况下打印日志。当执行 spawn 函数时,pwd 的命令结果将会被打印,子进程以 code 0 退出,表明没有错误发生
我们可以用 spawn 函数的第二个参数向执行的命令传递参数,是一个数组的参数。例如,在当前文件夹下执行带有参数 -type f
的 find 命令(将文件列出来),可以这样做
const child = spawn('find', ['.', '-type', 'f']);
如果在命令执行过程中发生错误了,例如,我们在上面给了一个非法的路径,child.stderr data
事件处理器将会被触发,exit
事件处理器会被报告一个退出码 1,表明有一个错误发生了。错误时退出码的值根据实际的宿主系统及错误类型
一个子进程 stdin 是一个可写的流。我们可以用它发送一个命令的输入。像任何一个可写流那样,最简单消费它的方式时使用 pipe 函数。我们简单地将一个可读流 pipe 到另一个可写流里。因为主进程的 stdin 是可读流,我们可以将它 pipe 到子进程的 stdin 流里
const {spawn} = require('child_process');
const child = spawn('wc');
process.stdin.pipe(child.stdin);
child.stdout.on('data', (data) => {
console.log(`child stdout: ${data}`);
});
子进程执行了 wc
命令,是一个计算行数,单词数,和字母数的 Linux 命令。我们可以将主进程的 stdin(是可读流)pipe 到子进程的 stdin(是一个可读流)。结合的结果是我们得到了一个标准的输入模式,我们可以输入一些东西,当 CTRL+ D
时,我们输入的将会被用来作为 wc
命令的参数
我们也可以在多进程标准输入输出之间相互 pipe,就像我们用 Linux 命令做的那样。例如:我们可以 pipe find 命令的 stdout 到 wc(在当前目录中统计所有的文件) 命令的 stdin 里
const {spawn} = require('child_process');
const find = spawn('find', ['.', '-type', 'f']);
const wc = spawn('wc', ['-l']);
find.stdout.pipe(wc.stdin);
wc.stdout.on('data', (data) => {
console.log(`Number of files ${data}`)
});
其他参数
spawn('command', {
stdio: 'inherit',
shell: true
});
studio:inherit
子进程将会继承主进程的 stdin,stdout,stderr
这会导致子进程数据事件处理器在主进程的 process.stdout
流中被触发,子进程的stdin,stdout,stderr
失效
shell: true
可以在传递的命令中使用shell
语法
# exec 函数
默认地,spawn 函数并没有创建一个 shell 去执行我们传入地命令。这使得它比 exec 函数执行稍微高效一点儿,exec 创建了个shell
。exec 函数有另一个主要地区别,将命令的输出放到缓冲区
,并且将整个输出值传递给一个回调(而不像 spawn 那样使用流
)
const { exec } = require('child_process');
exec('find . -type f | wc -l', (err, stdout, stderr) => {
if (err) {
console.error(`exec error: ${err}`);
return;
}
console.log(`Number of files ${stdout}`);
});
exec 的参数列表
第一个参数是要执行的命令
第二个参数是配置选项
第三个参数是回调函数
因为 exec 函数会使用 shell 去执行命令,因此我们可以直接使用 shell 语法代替 pipe 特性
exec 函数将输出放入缓存区,并且将它作为 stdout 传递给回调函数(exec 函数的第二个参数)。stdout 是我们想要打印的命令的输出。
如果你想要使用 shell 语法并且运行命令输出的所期望的数据比较小,建议使用 exec 函数(记住,exec 在返回结果数据之前,会在内存中缓存整个数据)。
如果期望的数据很大,那么建议使用 spawn 函数,因为数据可以被标准的 IO 对象流化(streamed)
配置选项
# execFile 函数
如果你要执行一个文件不需要使用 shell,execFile
函数就是你所需要的。它表现的和 exec 函数一样,但是不用 shell,这让它更高效一点儿
# xxSync 函数
从 child_process 模块导出的函数 spawn,exec,execFile 都有同步阻塞
的版本,等待直到子进程退出
const {
spawnSync,
execSync,
execFileSync,
} = require('child_process');
# fork 函数
fork 函数是 spawn 函数的另一种衍生(fork) node 进程的形式。spawn 和 fork 之间最大的不同是当使用 fork 函数时,到子进程的通信通道被建立了,因此我们可以在子进程里通过全局的 process 使用 send 函数,在父子进程之间交换信息
//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);
使用 fork 的另一个好处是可以主进程的事件循环不会被阻塞
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');
// 当一个子进程使用 process.send() 发送消息时会触发 'message' 事件
compute.on('message', sum => {
res.end(`Sum is ${sum}`)
//可以关闭子进程
//compute.kill()
})
} else {
res.end('OK');
}
});
server.listen(3000);
# 独立子进程
在正常情况下,父进程一定会等待子进程退出后,才退出。如果想让父进程先退出,不受到子进程的影响,那么应该
- 调用 ChildProcess 对象上的 unref()
- options.detached 设置为 true
- 子进程的 stdio 不能是连接到父进程
main.js
const { spawn } = require("child_process");
const subprocess = spawn(process.argv0, ["sub.js"], {
detached: true,
stdio: "ignore"
});
subprocess.unref();
child.js
setInterval(() => {
console.log('I am running');
}, 1000);