多进程构架
面对单进程单线程对多核使用不足问题,前人经验是启动多个进程,理想状态下每个进程各自利用一个cpu,以此实现多核cpu的利用,node 提供child_process 模块,利用fork函数实现进程的复制。
例如
// worker.js 文件
var http = require('http')
http.createServer(function (req, res) {
res.writeHead(200, {'Content.type': 'text/plain'})
res.end('hello word')
}).listen(Math.round((1 + Math.random()) * 1000), '127.0.0.1')
// master.js 文件
var fork = require('child_process').fork
var cpus = require('os').cpus()
for (var i = 0; i < cpus.length; i++) {
fork('./worker.js')
}
这段代码会根据当前机器的cpu数量启动对应数量的node进程
下图著名的master-worker模式(主从模式)
尽管Node提供了fork()供我们复制进程使每个cpu都用上,但是依然要切记fork()进程是昂贵的,node通过事件驱动的方式在单线程上解决了高并发问题,这里启动多个进程只是为了充分利用cpu资源,而不是为了解决并发问题
创建子进程
child_process模块提供了4个方法创建子进程
- spawn() 启动一个子进程来执行命令。例如
cp.spawn('node', 'worker.js')
- exec() 启动一个子进程来执行命令,与spawn不同的是,它有一个回调函数来获知子进程的状况。例如
cp.exec('node worker.js', function(){})
- execFile() 启动一个子进程来执行可执行文件。例如
cp.execFile('worker.js', function() {})
- fork() 需要指定可执行文件的路径。例如
cp.fork('./worker.js')
进程间通信
主进程和工作进程之间通过onmessage()和postMessage()进行通信,由send()方法实现发送数据,message事件实现监听发来的数据。例如
// parent.js
var cp = require('child_process')
var n = cp.fork('./sub.js')
n.on('message', function (data){
console.log(data)
})
n.send('lalalala')
// sub.js
process.on('message', function () {})
process.send('hahahahah')
进程通信原理
通过fork()或其他api创建子进程后,为了实现父子进程通信,父子进程之间会创建IPC通道,通过通道,父子进程之间才能通过message和send()传递信息。
IPC即进程间的通信,由libuv提供。
如图
父进程在实际创建子进程之前,会创建IPC通道并监听它,然后才真正创建子进程,并通过环境变量告诉子进程这个IPC通道的文件描述符,子进程在启动时根据文件描述符连接这个已经存在的IPC通道。
句柄传递
当启动子进程时无法监听同一个端口
发送句柄解决这个问题
代码如下
// parent.js
var cp = require('child_process')
var child1 = cp.fork('./child.js')
var child2 = cp.fork('./child.js')
var server = require('net').createServer()
server.listen(1234, function () {
child1.send('server', server)
child2.send('server', server)
server.colse()
})
// child.js
var cp = require('child_process')
var child1 = cp.fork('./child.js')
var child2 = cp.fork('./child.js')
var server = require('net').createServer()
server.listen(8081, function (req, res) {
child1.send('server', server, req)
child2.send('server', server, req)
server.close()
})
process.on('message', function (m, tcp) {
if (m === 'server') {
tcp.on('connection', function (socket) {
server.emit('connection', socket,)
})
}
})
发送到IPC管道中的实际上是我们要发送的句柄的管道描述符,这个message对象在写入IPC管道时也会通过JSON.stringfly() 进行序列化,所以最终发送到IPC管道中的信息都是字符串。
连接了IPC通道的子进程可以读取到父进程发来的消息,将字符串JSON.parse() 还原为对象后,才触发message事件将消息体传递给应用层使用,并和得到的文件描述符一起还原对应的对象。目前node只支持特定的几种类型的句柄。
监听共同的端口
我们在独立启动的进程中,TCP服务端的socket套接字的文件描述符并不相同,导致监听相同的端口会抛出异常。
但对于发送句柄还原出来的服务而言,它们的描述符是相同的,所以可以监听相同的端口,当多个进程监听相同端口时,文件描述符同一时间只能被某个进程应用,也就是说这些进程服务是抢占式的。
进程的稳定
除了message事件外,还有如下事件
- error : 当子进程无法被创建、无法被杀死、无法发送消息时触发
- exit : 子进程退出时触发。如果是子进程是正常退出事件的第一个参数为退出码,如果非正常退出则为null. 如果通过kill杀死,则会有第二个参数,为杀死进程时的信号
- close: 在子进程的标准输入输出流终止时触发
- disconnect: 在父进程或子进程中调用disconect()方法触发,disconnect()方法关闭监听IPC通道
自动重启
// master.js
var fork = require('child_process').fork
var cpus = require('os').cups()
var server = require('net').createrServer()
server.listen(1337)
var workers = {}
var createWorker = function () {
var worker = fork(__dirname + 'worker.js')
// 退出是重启新的进程
worker.on('exit', function () {
console.log('worker' + worker.pid + 'exited')
delete workers[worker.pid]
createrWorker()
})
// 句柄发送
worker.send('server', server)
workers[worker.pid] = worker
}
for (var i = 0; i < cpus.length; i++) {
createrWorker()
}
// 进行自己退出时让所有工作进程退出
process.on('exit', function () {
for (var pid in workers) {
workers[pid].kill()
}
})
// worker.js
var http = require('http')
var server = http.createrServer(function (req, res) {
res.writeHead(200, {'Content-type': 'text/plain'})
res.end('lalalalala')
})
var worker;
process.on('message', function (m, tcp) {
if (m === 'server') {
worker = tcp;
worker.on('connection', function (socket){
server.emit('connection', socket)
})
}
})
// 监听未捕获的异常
process.on('uncaughtException', function () {
// 停止接收新的连接,等待所有连接都断开后触发exit()
worker.close(function () {
process.exit(1) // 1 未捕获的致命异常
})
})
自杀信号
在极端情况下,所有的工作进程都停止接收新的连接,全都处于等待退出的状态,但在等到进程完全退出才重启的过程中有可能存在没有进程为新用户服务的情景。
解决方式:
// worker.js
// 监听未捕获的异常
process.on('uncaughtException', function () {
// 发送自杀信号
process.send({act: 'suicide'})
// 停止接收新的连接,等待所有连接都断开后触发exit()
worker.close(function () {
process.exit(1) // 1 未捕获的致命异常
})
})
// master.js
var fork = require('child_process').fork
var cpus = require('os').cups()
var server = require('net').createrServer()
server.listen(1337)
var workers = {}
var createWorker = function () {
var worker = fork(__dirname + 'worker.js')
worker.on('message', function (m) {
if (m.cat === 'suicide') {
createrWorker()
}
})
// 退出是重启新的进程
worker.on('exit', function () {
console.log('worker' + worker.pid + 'exited')
delete workers[worker.pid]
//createrWorker()
})
// 句柄发送
worker.send('server', server)
workers[worker.pid] = worker
}
for (var i = 0; i < cpus.length; i++) {
createrWorker()
}
// 进行自己退出时让所有工作进程退出
process.on('exit', function () {
for (var pid in workers) {
workers[pid].kill()
}
})
对套接字的理解
socket通常也称作"套接字",用于描述IP地址和端口,是一个通信链的句柄。应用程
序通常通过"套接字"向网络发出请求或者应答网络请求。
要通过互联网进行通信,你至少需要一对套接字,其中一个运行于客户机端,我们称之为
ClientSocket,另一个运行于服务器端,我们称之为ServerSocket。根据连接启动的方式以及本地套接字要连接的目标,套接字之间的连接过程可以分为三个 步骤:
- 服务器监听
- 客户端请求
- 连接确认。
服务器监听 是服务器端套接字并不定位具体的客户端套接字,而是处于等待连接的
状态,实时监控网络状态。
客户端请求 是指由客户端的套接字提出连接请求,要连接的目标是服务器端的套接
字。为此,客户端的套接字必须首先描述它要连接的服务器的套接字,指出服务器端套接字的
地址和端口号,然后就向服务器端套接字提出连接请求。
连接确认 是指当服务器端套接字监听到或者说接收到客户端套接字的连接请求,它
就响应客户端套接字的请求,建立一个新的线程,把服务器端套接字的描述发给客户端,一旦
客户端确认了此描述,连接就建立好了。而服务器端套接字继续处于监听状态,继续接收其他
客户端套接字的连接请求。