下面的示例可能有助于更好地说明这一点。
这有一个父母和一个孩子,父母创建一个服务器,并在客户端连接时,将套接字句柄传递给孩子。
孩子只有一个消息循环,它在其中接收句柄,并且能够通过它到达客户端。
$ cat foo.js
const cp = require('child_process')
const n = require('net')
if (process.argv[2] === 'child') {
process.on('message', (m, h) => {
h.end('ok')
})
}
else {
const c = cp.fork(process.argv[1], ['child'])
const s = n.createServer();
s.on('connection', (h) => {
c.send({}, h)
}).listen(12000, () => {
const m = n.connect(12000)
m.on('data', (d) => {
console.log(d.toString())
})
})
}
strace 与握手的相关部分。
[pid 12055] sendmsg(11, {msg_name(0)=NULL, msg_iov(1)=[{"{\"cmd\":\"NODE_HANDLE\",\"type\":\"net.Socket\",\"msg\":{},\"key\":\"6::::12000\"}\n", 70}], msg_controllen=24, {cmsg_len=20, cmsg_level=SOL_SOCKET, cmsg_type=SCM_RIGHTS, {14}}, msg_flags=0}, 0) = 70
[pid 12061] recvmsg(3, {msg_name(0)=NULL, msg_iov(1)=[{"{\"cmd\":\"NODE_HANDLE\",\"type\":\"net.Socket\",\"msg\":{},\"key\":\"6::::12000\"}\n", 65536}], msg_controllen=24, {cmsg_len=20, cmsg_level=SOL_SOCKET, cmsg_type=SCM_RIGHTS, {12}}, msg_flags=MSG_CMSG_CLOEXEC}, MSG_CMSG_CLOEXEC) = 70
[pid 12061] getsockname(12, {sa_family=AF_INET6, sin6_port=htons(12000), inet_pton(AF_INET6, "::ffff:127.0.0.1", &sin6_addr), sin6_flowinfo=0, sin6_scope_id=0}, [28]) = 0
[pid 12061] getsockopt(12, SOL_SOCKET, SO_TYPE, [1], [4]) = 0
[pid 12061] write(3, "{\"cmd\":\"NODE_HANDLE_ACK\"}\n", 26) = 26
[pid 12055] <... epoll_wait resumed> {{EPOLLIN, {u32=11, u64=11}}}, 1024, -1) = 1
[pid 12055] recvmsg(11, {msg_name(0)=NULL, msg_iov(1)=[{"{\"cmd\":\"NODE_HANDLE_ACK\"}\n", 65536}], msg_controllen=0, msg_flags=MSG_CMSG_CLOEXEC}, MSG_CMSG_CLOEXEC) = 26
如您所见,父子进程参与传输套接字句柄的协议,子进程根据接收到的句柄信息重新创建 net.Socket 对象。随后,子进程能够处理此连接下的客户端。
此协议以及跨进程传输句柄和工作负载的方式是cluster 模块的核心和灵魂。