一、使用cluster示例
在node中,启动一个server非常简单,简单的几行代码就能启动一个服务器。而且node实例也是单线程的,并使用事件循环(event loop)来做到高并发。当我们为了让我们服务器的内核数都利用起来的时候(提升qps吞吐),那么其中就有一个cluster共我们使用,使用方法也很简单:
const cluster = require('cluster');
const http = require('http');
const numCPUs = require('os').cpus().length;
// 当fork出子进程后,子进程也会重新执行这个文件
console.log('Begin to run js file. pid = ' + process.pid);
// 默认程序第一次加载的是master逻辑,所以isMaster一定为true(后面会分析node源码)
if (cluster.isMaster) {
console.log(`主进程 ${process.pid} 正在运行`);
// 衍生工作进程。
for (let i = 0; i < numCPUs; i++) {
cluster.fork();
}
cluster.on('exit', (worker, code, signal) => {
console.log(`工作进程 ${worker.process.pid} 已退出`);
});
} else {
// 工作进程可以共享任何 TCP 连接。
// 在本例子中,共享的是 HTTP 服务器。
http.createServer((req, res) => {
res.writeHead(200);
res.end(`response from worker ${process.pid}`);
}).listen(9000);
console.log(`工作进程 ${process.pid} 已启动`);
}
按照上面的做法,一个多进程的node服务就搭建起来了。
(1)提出几个问题
但是,喜欢源码之美的我和你,一定会疑惑这怎么就把几个进程给启动了。 可能会列下面的问题:
- 如果进了if,那这是怎么进的else,并且新建了好几个子进程中的server的呢?
- 主进程和子进程各自的作用是什么?他们是如何协调工作的?
- 为什么子进程中创建server的时候,监听了同一个端口,但没有报端口占用的错误呢?
随着分析的深入,会有更多的疑问出现在面前,不着急,先把上面的框架问题解决。
先把结论抛出来。
(2)从架构层面做简单解答
问题一答案:
在cluster内部,在执行我们业务代码中的fork方法时,会把当前这个js文件传入,当做fork的第一个参数(具体可以看下child_process.fork api文档),所以,子进程在创建时,会同样执行这个js文件。
问题二答案:
这就涉及到主进程和子进程之间的关系了。先上图
主进程负责请求的收集和分发,也就是说: 一个请求来了后,会首先打到master,然后master运用某种方式转发到具体工作进程去处理。(这里的Master角色是不是跟nginx的反向代理很像?)。
在这时,一个细节问题就出来了,我们并没有在代码中创建master啊! 那我们可以大胆猜测,是cluster帮我们创建了。后面会详细的通过源码来讨论。
问题三答案:
cluster内部的处理方法是:当创建第一个工作进程的时候,如果发现主进程没有启动server,那就创建一个server,并且把那个端口进行监听。然后用方法不让子进程监听具体端口。这样,现象就是: 主进程启动了一个server,并且启动了端口监听;子进程启动一个server,但是没有去监听具体的端口。 然后,通过一些方法,把主进程和子进程进行连接通信,等有请求过来的时候,主进程握有子进程的句柄,就自然能够调用子进程进行处理,并在处理完成后把结果返回给主进程。
其实,通过上面的问答,对整个cluster就有了一个整体的框架观了。接下来,我将通过一个一个的例子来剖析作者为什么要这么做,这样,对cluster的理解就会更深一层。
现在如果要自己也实现一个多进程的的架构,要咱们处理呢?
二、 我能想到的实现多进程的方式
下面开始吧!
要自己实现,那么需要知道是需要通过child_process.fork()来创建子进程。cluster也是底层调用的这个方法,前面也说过。
2.1 直接创建子进程server
例1:
// master.js
const {fork} = require('child_process');
const os = require('os');
// 比如是4核cpu
for(let i=0;i< os.cpus().length; i++) {
fork('./worker.js');
}
// woker.js
const http = require('http');
http.createServer((req, res) => {
res.send('创建子进程');
}).listen(3000);
通过执行这个例子,可以发现报端口已被占用的错误了。 原因是通常来说Tcp链接中同一台机器的同一个端口只可以被一个进程使用。所以这种方式创建多个子进程是行不通的。
2.2 通过监听多个端口创建server
例2:
监听同一个端口号不行,那么我们就监听多个端口
// master.js
const {fork} = require('child_process');
const os = require('os');
for(let i=0;i< 1; i++) {
let port = Math.ceil(Math.random() * 5000);
fork('./worker.js', [port]);
}
// woker.js
const port = process.argv[2];
const http = require('http');
http.createServer((req, res) => {
res.send('创建子进程');
}).listen(port);
上面的这个方法,可以解决问题。但是nodejs大神们为什么不用呢?是因为端口本身就有限,且这不是一种优雅的方式,而且更重要的是现在无法做到请求的分发。
(其实,可以使用连接池来做,用连接池来调度子进程,这也是一种实现方式,但cluster并不是这么实现的)
2.3 创建主进程server和子进程server处理
例3:
// master.js
const {fork} = require('child_process');
const os = require('os');
const net = require('net');
const workers = [];
for (let i =0; i< os.cpus().length; i++) {
const worker = fork('./worker.js');
workers.push(worker);
}
// 创建一个tcp server,并且把tcp server传给工作进程
const server = net.createServer();
server.listen(3000, () => {
console.log('向worker进程发送');
workers.forEach(worker => {
worker.send('SERVER', server);
});
// 阻止 server 接受新的连接并保持现有的连接
server.close();
})
// worker.js
const http = require('http');
const httpServer = http.createServer((req, res) => {
res.end(`Hello worker by ${process.pid}`);
});
process.on('message', (msg, tcpServer) => {
console.log('工作进程监听message, pid:' + process.pid);
if (msg === 'SERVER') {
tcpServer.on('connection', socket => {
// 把主进程server的socket传给子进程
httpServer.emit('connection', socket);
})
}
})
例3是master把句柄(标识资源的引用,内部包含了指向对象的文件描述符)传给工作进程,这种方式相对来说最接近cluster的实现方式。但是,这种情况下,无法做到有效的负载均衡,4个进程去争抢过来的请求,结果是可能某个进程处理了97%以上的请求。
上面几种方式都有其缺陷。也相信大家通过上面的几个实现方式,对多进程能有所感触了。那么分析正式开始。
三、源码分析
这里想了很久怎么分享,因为自己理解的也不是那么那么的透彻,其中有些c++的底层逻辑我并说不太好,强说可能说错,但如果说的太粗,那么只知道骨架,没有血肉填充,总会有抓不到的感觉。所以,我还是在尽力讲明白总体思路的前提下,对js部分尽量讲的细致一些,c++的部分先说个大概,以后对c++这部分内部逻辑熟悉后再往上加。各位有补充的,那非常感谢。
3.1 解决客户端请求到来之前的多进程初始化问题
- 我们的cluster业务代码初始加载的情形
上来就这句判断,那么我们去node源码中看一看
if (cluster.isMaster) {
// cluster.js
'use strict';
// 在导入模块时,会首先判断NODE_UNIQUE_ID, 这个其实是子进程的id,后面会看到在子进程创建中,对其赋值
const childOrPrimary = 'NODE_UNIQUE_ID' in process.env ? 'child' : 'primary';
// 默认把primary(主进程)导出
module.exports = require(`internal/cluster/${childOrPrimary}`);
那么去primary.js中看一看
const handles = new SafeMap();
cluster.isWorker = false;
// 这里可以看到主动赋值为true,同时这个属性将会在未来版本去掉,用isPrimary代替
cluster.isMaster = true; // Deprecated alias. Must be same as isPrimary.
cluster.isPrimary = true;
cluster.Worker = Worker;
cluster.workers = {};
cluster.settings = {};
cluster.SCHED_NONE = SCHED_NONE; // Leave it to the operating system.
cluster.SCHED_RR = SCHED_RR;
再沿着业务代码逻辑继续向下走, 进入了fork逻辑, 那么也就进入了primary.js中的fork方法
cluster.fork = function(env) {
// 进行一些setting操作, 并且监听internalMessage事件
cluster.setupPrimary();
const id = ++ids;
// 开始创建worker进程,使用child_process 的fork
const workerProcess = createWorkerProcess(id, env);
// 实例化Worker
const worker = new Worker({
id: id,
process: workerProcess
});
worker.on('message', function(message, handle) {
cluster.emit('message', this, message, handle);
});
// ......
// 下面的逻辑后面再看
createWorkerProcess()方法主要是fork出子进程了。(画重点)
function createWorkerProcess(id, env) {
// NODE_UNIQUE_ID 这个变量出现了!!也就是只有创建子进程时才会出现。
const workerEnv = { ...process.env, ...env, NODE_UNIQUE_ID: `${id}` };
/* node --harmony script.js --version
process.execArgv 的结果:
['--harmony']
process.argv 的结果:
['/usr/local/bin/node', 'script.js', '--version']
*/
const execArgv = [...cluster.settings.execArgv];
const debugArgRegex = /--inspect(?:-brk|-port)?|--debug-port/;
const nodeOptions = process.env.NODE_OPTIONS || '';
// 与inspect相关,这个细节可以略过
// ......
// 开始执行fork,注意fork里执行的还是那个我们写的业务代码,这就对应上了为什么会再执行我们业务代码中的else逻辑。
// 所以,我们会发现开启子进程的业务代码文件会被多次执行,
// 而每次执行,它的process.pid实际上是不一样的。
return fork(cluster.settings.exec, cluster.settings.args, {
cwd: cluster.settings.cwd,
env: workerEnv,
serialization: cluster.settings.serialization,
silent: cluster.settings.silent,
windowsHide: cluster.settings.windowsHide,
execArgv: execArgv,
stdio: cluster.settings.stdio,
gid: cluster.settings.gid,
uid: cluster.settings.uid
});
}
创建出子进程后,通过new Worker 创建了worker实例。
看到这,相信开头的第一个问题已经从源码角度解决了。再看问题2: 主进程和子进程是如何协调工作的?在文章开头的时候起了一个头,现在详细分析。开篇的时候留下的疑问是主进程的server是如何创建的?
关键的关键在于创建服务时候的listen方法
目前,我们已经知道已经创建出了主进程和几个子进程,而且从我们的业务代码中,知道创建子进程成功后,就需要创建http或者net服务了。
http.createServer((req, res) => {
res.writeHead(200);
res.end(`response from worker ${process.pid}`);
}).listen(9000);
继续走到node的源码中去,其实http.createServer调用的也是net.createServer, 调用的也是net的listen方法。
Server.prototype.listen = function(...args) {
const normalized = normalizeArgs(args);
let options = normalized[0];
const cb = normalized[1];
// ......
const backlogFromArgs =
// (handle, backlog) or (path, backlog) or (port, backlog)
toNumber(args.length > 1 && args[1]) ||
toNumber(args.length > 2 && args[2]); // (port, host, backlog)
options = options._handle || options.handle || options;
// ....
// 在我们业务代码中监听的是port:9000,所以会进这段逻辑
if (typeof options.port === 'number' || typeof options.port === 'string') {
validatePort(options.port, 'options.port');
backlog = options.backlog || backlogFromArgs;
// start TCP server listening on host:port
if (options.host) {
lookupAndListen(this, options.port | 0, options.host, backlog,
options.exclusive, flags);
} else { // Undefined host, listens on unspecified address
// Default addressType 4 will be used to search for primary server
// 并没有传host,所以会走到这里
listenInCluster(this, null, options.port | 0, 4,
backlog, undefined, options.exclusive);
}
return this;
}
}
// listenInCluster 逻辑
function listenInCluster(server, address, port, addressType,
backlog, fd, exclusive, flags) {
exclusive = !!exclusive;
if (cluster === undefined) cluster = require('cluster');
if (cluster.isPrimary || exclusive) {
// Will create a new handle
// _listen2 sets up the listened handle, it is still named like this
// to avoid breaking code that wraps this method
// 如果是主进程,就直接进行监听,设置新的句柄,目前不会走这里的逻辑
server._listen2(address, port, addressType, backlog, fd, flags);
return;
}
// 目前会走这里逻辑,因为我们还是在子进程逻辑中
const serverQuery = {
address: address,
port: port,
addressType: addressType,
fd: fd,
flags,
};
// Get the primary's server handle, and listen on it
// 子进程 将Server实例直接传入函数,去执行./child.js中的_getServer,
// 因为在子进程中,所以会走到node-master/lib/internal/cluster/child.js中
// 并且,会继续执行 queryServer(),在primary.js中,开始创建RoundRobinHandle
cluster._getServer(server, serverQuery, listenOnPrimaryHandle);
function listenOnPrimaryHandle(err, handle) {
err = checkBindError(err, port, handle);
if (err) {
const ex = exceptionWithHostPort(err, 'bind', address, port);
return server.emit('error', ex);
}
// Reuse primary's server handle
server._handle = handle;
// _listen2 sets up the listened handle, it is still named like this
// to avoid breaking code that wraps this method
server._listen2(address, port, addressType, backlog, fd, flags);
}
}
现在在子进程中创建server,这条逻辑不要乱
所以,就会进入child.js中去执行_getServer 方法,并且我们假设是在类unix系统中,不考虑win32
// obj 是server实例
cluster._getServer = function(obj, options, cb) {
let address = options.address;
// Resolve unix socket paths to absolute paths
if (options.port < 0 && typeof address === 'string' &&
process.platform !== 'win32')
address = path.resolve(address);
const indexesKey = ArrayPrototypeJoin(
[
address,
options.port,
options.addressType,
options.fd,
], ':');
let indexSet = indexes.get(indexesKey);
if (indexSet === undefined) {
indexSet = { nextIndex: 0, set: new SafeSet() };
indexes.set(indexesKey, indexSet);
}
const index = indexSet.nextIndex++;
indexSet.set.add(index);
const message = {
act: 'queryServer',
index,
data: null,
...options
};
message.address = address;
// Set custom data on handle (i.e. tls tickets key)
if (obj._getServerData)
message.data = obj._getServerData();
// 子进程发送message后,执行回调,
// send函数中有这句: message = util._extend({ cmd: 'NODE_CLUSTER' }, message);
// 这会触发node内部的internalMessage事件。这里需要对照codejs API提一下
send(message, (reply, handle) => {
if (typeof obj._setServerData === 'function')
obj._setServerData(reply.data);
if (handle) {
// Shared listen socket
shared(reply, { handle, indexesKey, index }, cb);
} else {
// Round-robin.
rr(reply, { indexesKey, index }, cb);
}
});
obj.once('listening', () => {
cluster.worker.state = 'listening';
const address = obj.address();
message.act = 'listening';
message.port = (address && address.port) || options.port;
send(message);
});
};
请细致的看上面方面中的send方法。 send方法很重要,他会把message包装一下,继而发给主进程,主进程如果没有创建server,那么就会进行创建,并且监听端口。
// child.js
function send(message, cb) {
return sendHelper(process, message, null, cb);
}
// internal/cluster/utils.js
function sendHelper(proc, message, handle, cb) {
if (!proc.connected)
return false;
// Mark message as internal. See INTERNAL_PREFIX in lib/child_process.js
// 执行 internalMessage 事件
message = { cmd: 'NODE_CLUSTER', ...message, seq };
if (typeof cb === 'function')
callbacks.set(seq, cb);
seq += 1;
return proc.send(message, handle);
}
{cmd: 'NODE_CLUSTER'} 是node内部的事件标识,具体解释在nodejs api中:
子 Node.js 进程有一个自己的 process.send() 方法,允许子进程发送消息回父进程。
当发送 {cmd: 'NODE_foo'} 消息时有一种特殊情况。 cmd 属性中包含 NODE_ 前缀的消息是预留给 Node.js 内核内部使用的,将不会触发子进程的 'message' 事件。 相反,这种消息可使用 'internalMessage' 事件触发,且会被 Node.js 内部消费。 应用程序应避免使用此类消息或监听 'internalMessage' 事件,因为它可能会被更改且不会通知。
传到主进程,主进程在fork函数中进行监听的,是的,就是前面分析的child_process中的fork,只是前面的时候没有把fork函数全部写出来。现在补充完整
cluster.fork = function(env) {
// 进行一些setting操作, 并且监听internalMessage事件
cluster.setupPrimary();
const id = ++ids;
// 开始创建worker进程,使用child_process 的fork
const workerProcess = createWorkerProcess(id, env);
// 实例化Worker
const worker = new Worker({
id: id,
process: workerProcess
});
// 设置监听每个子进程的事件
worker.on('message', function(message, handle) {
cluster.emit('message', this, message, handle);
});
worker.process.once('exit', (exitCode, signalCode) => {
/*
* Remove the worker from the workers list only
* if it has disconnected, otherwise we might
* still want to access it.
*/
if (!worker.isConnected()) {
removeHandlesForWorker(worker);
removeWorker(worker);
}
worker.exitedAfterDisconnect = !!worker.exitedAfterDisconnect;
worker.state = 'dead';
worker.emit('exit', exitCode, signalCode);
cluster.emit('exit', worker, exitCode, signalCode);
});
worker.process.once('disconnect', () => {
/*
* Now is a good time to remove the handles
* associated with this worker because it is
* not connected to the primary anymore.
*/
removeHandlesForWorker(worker);
/*
* Remove the worker from the workers list only
* if its process has exited. Otherwise, we might
* still want to access it.
*/
if (worker.isDead())
removeWorker(worker);
worker.exitedAfterDisconnect = !!worker.exitedAfterDisconnect;
worker.state = 'disconnected';
worker.emit('disconnect');
cluster.emit('disconnect', worker);
});
// 当消息到达后,消息对象要进行过滤,如果message.cmd的值如果以NODE为前缀,它将响应一个内部事件internalMessage;
// 如果message.cmd值为NODEHANDLE,它将取出 message.type值和得到的文件描述符一起还原出一个对应的对象。
// 子进程根据message.type创建对应的TCP服务器对象
worker.process.on('internalMessage', internal(worker, onmessage));
process.nextTick(emitForkNT, worker);
// 把创建的worker进程存储到cluster.workers中,
// 供以后Round-robin调用
cluster.workers[worker.id] = worker;
return worker;
};
// onmessage方法
function onmessage(message, handle) {
// 主进程收到子进程在创建http/net服务器时,执行listen方法时的逻辑
const worker = this;
// 在创建时message.act会传入 'queryServer'
const fn = methodMessageMapping[message.act];
if (typeof fn === 'function')
// 执行 queryServer方法
fn(worker, message);
}
// queryServer 方法执行
function queryServer(worker, message) {
// Stop processing if worker already disconnecting
if (worker.exitedAfterDisconnect)
return;
const key = `${message.address}:${message.port}:${message.addressType}:` +
`${message.fd}:${message.index}`;
let handle = handles.get(key);
// 第一次执行的时候,handle是undefined,所以会走下面的逻辑
if (handle === undefined) {
let address = message.address;
// Find shortest path for unix sockets because of the ~100 byte limit
if (message.port < 0 && typeof address === 'string' &&
process.platform !== 'win32') {
address = path.relative(process.cwd(), address);
if (message.address.length < address.length)
address = message.address;
}
// UDP is exempt from round-robin connection balancing for what should
// be obvious reasons: it's connectionless. There is nothing to send to
// the workers except raw datagrams and that's pointless.
// 开始创建 RoundRobinHandle实例,会在父进程中生成实例。
// 这里是非常重要的。也就找到了主进程的server是怎么创建的。
if (schedulingPolicy !== SCHED_RR ||
message.addressType === 'udp4' ||
message.addressType === 'udp6') {
// 如果创建UDP服务
handle = new SharedHandle(key, address, message);
} else {
// 创建TCP服务,走Round-Robin算法,负责处理请求时,主进程server选取哪个子进程server进行处理。
handle = new RoundRobinHandle(key, address, message);
}
handles.set(key, handle);
}
if (!handle.data)
handle.data = message.data;
// Set custom server data
// 执行RoundRobinHandle实例的add方法
handle.add(worker, (errno, reply, handle) => {
const { data } = handles.get(key);
if (errno)
handles.delete(key); // Gives other workers a chance to retry.
// 又重新发送给子进程, 但hanle为null,也就是说并没有把主进程的句柄传给子进程。
send(worker, {
errno,
key,
ack: message.seq,
data,
...reply
}, handle);
});
}
是很有必要看一下RoundRobinHandle做了什么的。
function RoundRobinHandle(key, address, { port, fd, flags }) {
this.key = key;
this.all = new SafeMap();
this.free = new SafeMap();
this.handles = [];
this.handle = null;
// 在这里创建了主进程中的server,并且监听在子进程中定义的端口
this.server = net.createServer(assert.fail);
// 判断文件描述符
if (fd >= 0)
this.server.listen({ fd });
else if (port >= 0) {
// 业务代码中传入了port
this.server.listen({
port,
host: address,
// Currently, net module only supports `ipv6Only` option in `flags`.
ipv6Only: Boolean(flags & constants.UV_TCP_IPV6ONLY),
});
} else
this.server.listen(address); // UNIX socket path.
this.server.once('listening', () => {
this.handle = this.server._handle;
// 当执行onconnection时,会执行this.distribute
this.handle.onconnection = (err, handle) => this.distribute(err, handle);
this.server._handle = null;
this.server = null;
});
}
因为执行this.server.listen(),所以又走到了我们开始时的Server.prototype.listen方法,同样也会进 listenInCluster方法,只是这次, 走的是主进程逻辑(完整代码在前面有,可以回过头看看):
if (cluster.isPrimary || exclusive) {
// Will create a new handle
// _listen2 sets up the listened handle, it is still named like this
// to avoid breaking code that wraps this method
// 如果是主进程,就直接进行监听,设置新的句柄,目前不会走这里的逻辑
server._listen2(address, port, addressType, backlog, fd, flags);
return;
}
server._listen2 中会创建主进程的TCP连接句柄,这个句柄会在以后客户端请求的时候用到。具体会执行 setupListenHandle方法,这个后面会讲,这里知道主进程走这个函数的功能就好了。
小结: 到这一阶段,创建了子进程的server,并且也在主进程中创建了一个server,并且主进程也监听了我们定义的端口。
下一步需要解决端口问题: 现在主进程的server已经监听好我们预设的端口,但是并没有把主进程server句柄传给子进程。 那么子进程server需要做:
- 子进程不要进行端口的监听。否则会出现例1中出现的端口被占用,或者例2中的开多个端口,这都与cluster的初衷不符。
当创建好主进程的server并且监听了端口后, 子进程还在等着回调函数的执行,当执行后面的
handle.add(worker, (errno, reply, handle) => {
// ....
}
会去调用RoundRobinHandle中的add原型方法:
RoundRobinHandle.prototype.add = function(worker, send) {
assert(this.all.has(worker.id) === false);
this.all.set(worker.id, worker);
const done = () => {
if (this.handle.getsockname) {
const out = {};
this.handle.getsockname(out);
// TODO(bnoordhuis) Check err.
// send方法就是调用add方法的那个回调函数,进而向子进程发送一个事件。
send(null, { sockname: out }, null);
} else {
console.log('【round_robin_handle.js】 send(null, null, null);');
send(null, null, null); // UNIX socket.
}
// 将当前的子进程推入this.free中(现在只是推入,等客户端请求时,会取出子进程使用)
this.handoff(worker); // In case there are connections pending.
};
if (this.server === null)
return done();
// Still busy binding.
this.server.once('listening', done);
this.server.once('error', (err) => {
send(err.errno, null);
});
};
发送操作中, child_process.js中的target.send会执行,进而最终让上面所说的回调函数触发:
// child.js
send(message, (reply, handle) => {
if (typeof obj._setServerData === 'function')
obj._setServerData(reply.data);
if (handle) {
// Shared listen socket
shared(reply, { handle, indexesKey, index }, cb);
} else {
// Round-robin.
// 这时走了这里,进入rr的方法执行
rr(reply, { indexesKey, index }, cb);
}
});
// rr 方法
function rr(message, { indexesKey, index }, cb) {
if (message.errno)
return cb(message.errno, null);
let key = message.key;
function listen(backlog) {
// TODO(bnoordhuis) Send a message to the primary that tells it to
// update the backlog size. The actual backlog should probably be
// the largest requested size by any worker.
return 0;
}
function close() {
// lib/net.js treats server._handle.close() as effectively synchronous.
// That means there is a time window between the call to close() and
// the ack by the primary process in which we can still receive handles.
// onconnection() below handles that by sending those handles back to
// the primary.
if (key === undefined)
return;
send({ act: 'close', key });
handles.delete(key);
removeIndexesKey(indexesKey, index);
key = undefined;
}
function getsockname(out) {
if (key)
ObjectAssign(out, message.sockname);
return 0;
}
// Faux handle. Mimics a TCPWrap with just enough fidelity to get away
// with it. Fools net.Server into thinking that it's backed by a real
// handle. Use a noop function for ref() and unref() because the control
// channel is going to keep the worker alive anyway.
// 这里做了hack操作, fake了一个假的handle对象,欺骗上层的调用者。当listen函数执行时,
// 返回的永远是0
const handle = { close, listen, ref: noop, unref: noop };
if (message.sockname) {
handle.getsockname = getsockname; // TCP handles only.
}
assert(handles.has(key) === false);
handles.set(key, handle);
// 把一个假的handle传给了cb
cb(0, handle);
}
// 这里的cb是 net.js中的listenOnPrimaryHandle回调
function listenOnPrimaryHandle(err, handle) {
// 主进程创建完成后,把数据回传给子进程,然后子进程fack了一个假的handle,传给了这个回调函数
err = checkBindError(err, port, handle);
if (err) {
const ex = exceptionWithHostPort(err, 'bind', address, port);
return server.emit('error', ex);
}
// Reuse primary's server handle
server._handle = handle;
// _listen2 sets up the listened handle, it is still named like this
// to avoid breaking code that wraps this method
// 子进程实际上不需要再设置listened handle, 但是为了代码命名完整性,也调用这个函数
// 在这个函数中做区分
server._listen2(address, port, addressType, backlog, fd, flags);
}
这时,就会走上面说过的setupListenHandle方法,这是因为有这句:
Server.prototype._listen2 = setupListenHandle;
setupListenHandle函数分为两部分逻辑,一部分是上面提到过的,创建TCP句柄,另一部分功能是fack一个handle,实际上子进程就不会再进行端口监听了。
function setupListenHandle(address, port, addressType, backlog, fd, flags) {
debug('setupListenHandle', address, port, addressType, backlog, fd);
// If there is not yet a handle, we need to create one and bind.
// In the case of a server sent via IPC, we don't need to do this.
// 当子进程在调用进这个方法的时候,已经有_handle了,所以不会再进行handle的创建
if (this._handle) {
debug('setupListenHandle: have a handle already');
} else {
// 主进程会调用
debug('setupListenHandle: create a handle');
let rval = null;
// Try to bind to the unspecified IPv6 address, see if IPv6 is available
if (!address && typeof fd !== 'number') {
// 实际上是创建了主进程的handle句柄,创建好了tcp连接通路
rval = createServerHandle(DEFAULT_IPV6_ADDR, port, 6, fd, flags);
if (typeof rval === 'number') {
rval = null;
address = DEFAULT_IPV4_ADDR;
addressType = 4;
} else {
address = DEFAULT_IPV6_ADDR;
addressType = 6;
}
}
if (rval === null)
rval = createServerHandle(address, port, addressType, fd, flags);
if (typeof rval === 'number') {
const error = uvExceptionWithHostPort(rval, 'listen', address, port);
process.nextTick(emitErrorNT, this, error);
return;
}
this._handle = rval;
}
this[async_id_symbol] = getNewAsyncId(this._handle);
// 子进程也会绑定onconnection事件
this._handle.onconnection = onconnection;
this._handle[owner_symbol] = this;
// Use a backlog of 512 entries. We pass 511 to the listen() call because
// the kernel does: backlogsize = roundup_pow_of_two(backlogsize + 1);
// which will thus give us a backlog of 512 entries.
// 这里,主进程的_handle是真实存在的,定义主线程最多可积压511个请求,跟redis也是相同的,实际上就是为了防止不一致可能出现的问题
// 而子进程因为handle是fack的,所以不管传多少,都是返回0
const err = this._handle.listen(backlog || 511);
if (err) {
const ex = uvExceptionWithHostPort(err, 'listen', address, port);
this._handle.close();
this._handle = null;
defaultTriggerAsyncIdScope(this[async_id_symbol],
process.nextTick,
emitErrorNT,
this,
ex);
return;
}
// Generate connection key, this should be unique to the connection
// 主进程创建时返回 '6::::9000', 子进程创建时返回 '4:null:9000'
this._connectionKey = addressType + ':' + address + ':' + port;
// Unref the handle if the server was unref'ed prior to listening
if (this._unref)
this.unref();
defaultTriggerAsyncIdScope(this[async_id_symbol],
process.nextTick,
emitListeningNT,
this);
}
当走到这里的时候,整个多进程服务基本搭建就完成了,当然,还没有讲客户端请求来了后,主进程和子进程的链路。
3.2 客户端请求主进程、子进程处理链路
在3.1完成后,实际上还有一步,子进程向主进程emit ’listening‘, server的listen方法执行的时候会触发 listening事件。
this.server.once('listening', () => {
this.handle = this.server._handle;
// 客户端请求时 onconnection会执行
this.handle.onconnection = (err, handle) => this.distribute(err, handle);
this.server._handle = null;
this.server = null;
});
now, 一个客户端请求打过来了----连接事件首先触发:
// round_robin_handle.js
RoundRobinHandle.prototype.distribute = function(err, handle) {
// 先将客户端请求push进handles中
ArrayPrototypePush(this.handles, handle);
// eslint-disable-next-line node-core/no-array-destructuring
// round-robin选择worker进程的逻辑:从this.free中获取一个空闲的进程
const [ workerEntry ] = this.free; // this.free is a SafeMap
if (ArrayIsArray(workerEntry)) {
const { 0: workerId, 1: worker } = workerEntry;
this.free.delete(workerId);
this.handoff(worker);
}
};
// handoff
RoundRobinHandle.prototype.handoff = function(worker) {
if (!this.all.has(worker.id)) {
return; // Worker is closing (or has closed) the server.
}
// 从请求列表中(this.handles)获取一个
const handle = ArrayPrototypeShift(this.handles);
// 如果获取不到了,就说明主进程没有缓存的客户端请求了,
// 那么将当前的子进程推入this.free中,等待下一轮客户端请求。
if (handle === undefined) {
this.free.set(worker.id, worker); // Add to ready queue again.
return;
}
// 选出要处理的子进程后, 向这个子进程发送处理消息。
const message = { act: 'newconn', key: this.key };
// handle -> 主进程的Tcp句柄 传给前面选定的worker进程
// sendHelper 之前已经见过,包装成message = { cmd: 'NODE_CLUSTER', ...message, seq }; 发送给子进程。
sendHelper(worker.process, message, handle, (reply) => {
if (reply.accepted)
handle.close();
else
this.distribute(0, handle); // Worker is shutting down. Send to another. 也就是说如果当前的worker没有接受,则需要重新发送给另一个
this.handoff(worker);
});
};
这时,就会send到子进程,子进程开始处理了。
// child.js
function onmessage(message, handle) {
// 如果message.act是newconn,说明是worker进程获取到的新请求,需要处理
if (message.act === 'newconn')
onconnection(message, handle);
else if (message.act === 'disconnect')
ReflectApply(_disconnect, worker, [true]);
}
function onconnection(message, handle) {
// 'null:9000:4:undefined:0'
const key = message.key;
// 获取之前定义的fake handle
const server = handles.get(key);
// 表示可以接受本次请求
const accepted = server !== undefined;
// 这里主要是告诉主进程,我这个子进程可以接受此次请求并进行处理
send({ ack: message.seq, accepted });
if (accepted)
// 这里面会创建一个new Socket实例, 走c++底层逻辑,至于底层逻辑交互暂不清楚
server.onconnection(0, handle);
}
// net.js
function onconnection(err, clientHandle) {
debugger;
const handle = this;
const self = handle[owner_symbol];
debug('onconnection');
if (err) {
self.emit('error', errnoException(err, 'accept'));
return;
}
if (self.maxConnections && self._connections >= self.maxConnections) {
clientHandle.close();
return;
}
const socket = new Socket({
handle: clientHandle,
allowHalfOpen: self.allowHalfOpen,
pauseOnCreate: self.pauseOnConnect,
readable: true,
writable: true
});
self._connections++;
socket.server = self;
socket._server = self;
DTRACE_NET_SERVER_CONNECTION(socket);
// 构建node层的socket对象,并触发connection事件,完成底层socket与node net模块的连接与请求打通
self.emit('connection', socket);
}
具体的socket怎么通过c++进行处理的,因不熟悉c++, 无法详细的说。 但是,按照逻辑,一定是:
主进程server监听着端口,当客户端请求来的时候 ---> 从this.free中获取到空闲的子进程,并将TCP handle 发送到子进程 ----> 子进程判断是否接受请求处理----> 如果接受,则告知主进程,同时进行处理 ——---> 子进程net server 创建一个新socket实例,带着传进来的TCP handle , 跟底层c++ 进行交互 ---> 处理 ----> 处理结果返回主进程,主进程把数据返回给客户端。
通过上面两个大部分的分析,知道了总体的通路。我不知道的最后这部分c++底层逻辑,也欢迎大家告知~~
希望通过这1000多行的纯手敲分析,能给大家带来一点点启发,欢迎点赞/阅读/评论~~