本文深入解析Node.js child_process模块,涵盖exec、execfile、spawn和fork四种创建子进程的方法。重点讲解Node.js如何利用IPC管道实现父子进程通信,并通过句柄传递技术解决多进程共享单一端口的问题,展示Master-Worker模式在充分利用CPU多核及优化文件描述符方面的实际应用。
Node.js多进程基础与child_process模块概述
Node.js采用单进程模型,无法像Java那样通过多线程实现并发执行。尽管Node.js基于事件驱动机制,通常无需并发处理即可保持非阻塞运行,但单进程架构难以充分利用现代CPU的多核性能。基于前人经验,通过创建多个进程可以有效利用CPU多核资源,Node.js正是通过child_process模块来实现这一多进程操作。
child_process模块赋予了Node.js创建任意子进程的能力。官方文档提供了四种API,它们在操作系统层面均对应创建子进程的操作,但对于开发者而言,这些API的使用场景和参数细节存在差异。
创建子进程的四种API详解
child_process.exec(command[, options][, callback]):该方法启动子进程以执行Shell命令,并可通过回调参数获取脚本的执行结果。
child_process.execfile(file[, args][, options][, callback]):与exec不同,此方法执行的是指定的可执行文件,而非Shell命令。
child_process.spawn(command[, args][, options]):该方法仅执行Shell命令,适用于不需要获取执行结果的场景。
child_process.fork(modulePath[, args][, options]):用于执行Node.js可运行的.js文件,同样无需获取执行结果。通过fork创建的子进程必定是Node进程。
exec()与execfile()在创建子进程时均可指定timeout属性以设置超时时间。一旦执行时间超过设定值,子进程将被强制终止。
若使用execfile()执行可执行文件,该文件的头部必须包含#!/usr/bin/env node声明。
进程间通信与句柄传递机制
进程间通信
Node.js与子进程之间的通信依赖于IPC(进程间通信)管道机制。如果子进程也是Node进程(通过fork创建),则可以通过监听message事件以及使用send()方法进行通信。
main.js
var cp = require('child_process');
//只有使用fork才可以使用message事件和send()方法
var n = cp.fork('./child.js');
n.on('message',function(m){
console.log(m);
})
n.send({"message":"hello"});
child.js
var cp = require('child_process');
process.on('message',function(m){
console.log(m);
})
process.send({"message":"hello I am child"})
父子进程之间会建立IPC通道,message事件和send()方法便利用该通道进行通信。
Master-Worker模式与端口共享方案
句柄传递
掌握子进程创建后,我们可以创建一个HTTP服务,并启动多个进程以充分利用CPU多核资源。
worker.js
var http = require('http');
http.createServer(function(req,res){
res.end('Hello,World');
//监听随机端口
}).listen(Math.round((1+Math.random())*1000),'127.0.0.1');
main.js
var fork = require('child_process').fork;
var cpus = require('os').cpus();
for(var i=0;i<cpus.length;i++){
fork('./worker.js');
}
上述代码会根据CPU核数创建相应数量的fork进程,每个进程监听一个随机端口以提供HTTP服务。
上述实现了一个典型的Master-Worker主从复制模式,在分布式应用中用于并行处理业务,具备良好的伸缩性和稳定性。需要注意的是,fork一个进程的代价较高,Node.js单进程事件驱动已具备良好性能。此处的多fork进程旨在充分利用CPU核数,而非解决并发问题。
上述示例存在占用过多端口的问题。能否让多个子进程共用同一个端口,对外仅暴露一个端口?尝试将上述随机端口改为8080并启动,会发现抛出如下异常。
events.js:72
throw er;//Unhandled 'error' event
Error:listen EADDRINUSE
XXXX
抛出端口被占用的异常,这意味着只有一个worker.js能监听8080端口,其余进程会抛出异常。
若要解决对外提供一个端口的问题,可参考nginx反向代理的做法。Master进程使用80端口对外提供服务,而fork的子进程使用随机端口。Master进程接收请求后将其转发至子进程。
在上述代理模式中,每个进程接收一个连接会消耗一个文件描述符。因此,客户端连接代理进程,代理进程再连接子进程,共消耗两个文件描述符。由于操作系统中文件描述符有限,Node.js引入了进程间发送句柄的功能以解决此问题。
在Node.js的IPC进程通信API中,send(message,[sendHandle])的第二个参数即为句柄。
基于句柄传递的轻量级服务实现
句柄是一种标识资源的引用,内部包含指向对象的文件描述符。句柄可用于描述socket对象、UDP套接字或管道。主进程向工作进程发送句柄意味着,当主进程接收到客户端socket请求后,直接将该socket发送给工作进程,无需再与工作进程建立socket连接,从而避免了文件描述符的浪费。示例代码如下:
main.js
var cp = require('child_process');
var child = cp.fork('./child.js');
var server = require('net').createServer();
//监听客户端的连接
server.on('connection',function(socket){
socket.end('handled by parent');
});
//启动监听8080端口
server.listen(8080,function(){
//给子进程发送TCP服务器(句柄)
child.send('server',server);
});
child.js
process.on('message',function(m,server){
if(m==='server'){
server.on('connection',function(socket){
socket.end('handle by child');
});
}
});
使用telnet或curl均可进行测试:
wang@wang ~/code/nodeStudy $ curl 192.168.10.104:8080 handled by parent wang@wang ~/code/nodeStudy $ curl 192.168.10.104:8080 handle by child wang@wang ~/code/nodeStudy $ curl 192.168.10.104:8080 handled by parent wang@wang ~/code/nodeStudy $ curl 192.168.10.104:8080 handled by parent
测试结果显示,每次客户端连接可能由父进程处理,也可能由子进程处理。现在我们尝试仅提供HTTP服务,并为了使父进程更轻量,仅让父进程传递句柄给子进程而不处理请求:
main.js
var cp = require('child_process');
var child1 = cp.fork('./child.js');
var child2 = cp.fork('./child.js');
var child3 = cp.fork('./child.js');
var child4 = cp.fork('./child.js');
var server = require('net').createServer();
//父进程将接收到的请求分发给子进程
server.listen(8080,function(){
child1.send('server',server);
child2.send('server',server);
child3.send('server',server);
child4.send('server',server);
//发送完句柄后关闭监听
server.close();
});
child.js
var http = require('http');
var serverInChild = http.createServer(function(req,res){
res.end('I am child.Id:'+process.pid);
});
//子进程收到父进程传递的句柄(即客户端与服务器的socket连接对象)
process.on('message',function(m,serverInParent){
if(m==='server'){
//处理与客户端的连接
serverInParent.on('connection',function(socket){
//交给http服务来处理
serverInChild.emit('connection',socket);
});
}
});
运行上述代码后,查看8080端口占用情况,结果如下:
wang@wang ~/code/nodeStudy $ lsof -i:8080 COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME node 5120 wang 11u IPv6 44561 0t0 TCP *:http-alt (LISTEN) node 5126 wang 11u IPv6 44561 0t0 TCP *:http-alt (LISTEN) node 5127 wang 11u IPv6 44561 0t0 TCP *:http-alt (LISTEN) node 5133 wang 11u IPv6 44561 0t0 TCP *:http-alt (LISTEN)
运行curl查看结果:
wang@wang ~/code/nodeStudy $ curl 192.168.10.104:8080 I am child.Id:5127 wang@wang ~/code/nodeStudy $ curl 192.168.10.104:8080 I am child.Id:5133 wang@wang ~/code/nodeStudy $ curl 192.168.10.104:8080 I am child.Id:5120 wang@wang ~/code/nodeStudy $ curl 192.168.10.104:8080 I am child.Id:5126 wang@wang ~/code/nodeStudy $ curl 192.168.10.104:8080 I am child.Id:5133 wang@wang ~/code/nodeStudy $ curl 192.168.10.104:8080 I am child.Id:5126
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持本站。