背景
在Master-Worker的模式下,当所有worker进程(数量大于2)都监听同一个端口时,那么就仅只有一个worker可以与master连接(因为是 IPC 是类似管道的,所以只能由一个占有),为此其他的进程在监听的过程中都会抛出 EADDRINSE 异常。
解决以上问题的方法就是主进程对外接收所有的网络请求,然后再将请求分别代理到不同的端口的进程上。但是由于进程每接收到一个连接就会用掉一个文件描述符,因此代理方案中客户端连接到代理进程,代理进程连接到工作进程的过程就需要消耗两个文件描述符,然而文件描述符是有限的,该做法是在原本基础上消耗多一倍的文件描述符影响了系统的拓展能力。
(文件描述符:一个索引值,指向内核为每一个进程所维护的该进程打开文件的记录表。 当程序打开一个现有文件或者创建一个新文件时,内核向进程返回一个文件描述符)
目录
为了解决以上问题,Node引入了进程间发送句柄的功能
child.send(messgae, [sendHandle])
句柄是一种可以用来标识资源的引用,它的内部包含了指向对象的文件描述符。
比如句柄可以用来标识服务器端/客户端的socket对象,UDP套接字,管道等等。
通过句柄可以实现当主进程接收到socket以后,将这个socket直接发给工作进程,而不是重新与工作进程之间建立新的socket连接来转发数据,从而减少一次文件描述符的浪费。
子进程对象send() 方法可以发生的句柄类型包括如下几种:
net.Socket TCP套字节 net.Server TCP服务器,任意建立在TCP服务器上的应用层服务都可以享受到它带来的好处 net.Native C++层面的TCP套字节或IPC管道 dgram.Socket UDP套接字 dgram.Native C++层面的UDP套接字 send() 方法在将消息发送到IPC管道前,将消息组装成两个对象,一个参数是handle,另一个是message。
message如下
{ cmd: 'NODE_HANDLE', type: 'net.Server', msg: message }
发送到IPC管道的实际上是句柄文件描述符(实际上是一个整数值),message 对象写入到IPC管道时也会通过 JSON.stringify() 进行序列化。
所以最终发送到IPC通道中的信息都是字符串。
在子进程读取后,消息对象将字符串通过JSON.pares() 解析还原为对象后,才触发message事件将消息传给应用层使用。
message.cmd 的值
以 NODE_ 为前缀,将响应一个内部事件 internalMessage
以 NODE_HANDLE, 将取出message.type值和得到的文件描述符一起还原出一个对应的对象

以发送TCP服务器句柄为例,子进程收到消息后的还原如下
function(message, handle, emit) { var self = this; ver server = new net.Server(); server.listen(handle, funciton(){ emit(server); }); }从代码可知,子进程是根据message.type创建对应的TCP服务器对象,然后监听到文件描述符上。
Node进程之间只有消息传递,不会真正地传递对象,这种错觉是抽象封装的结果
为什么通过发送句柄后,多个进程可以同时监听相同的端口而不引起EADDRINUSE异常。
由于独立启动进程中,TCP服务器socket套接字的文件描述并不相同,导致监听到相同端口时会抛出异常(简单理解一个端口对应一个文件描述符)
Node底层对端口监听都设置了 SO_REUSEADDR选项,即不同进程可以用相同的网卡和端口进行监听,从而使得服务器套接字可以被不同的进程复用。
setsockopt(tcp -> io_watcher.fd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on))
由于独立进程启动互相之间并不知道文件描述符,所以监听相同接口就会失败。
对于send() 发送的句柄还原出来的服务而言,它们的文件描述符是相同的,所以监听相同端口不会引起异常。多个应用监听相同端口时,文件描述符同一时间只能被某个进程所用。