本文转载自微信公众号「编程杂技 」,作者theanarkh。转载本文请联系编程杂技 众号。
之前提了一个问题:nodejs中如何实现兄弟进程间的通信,大家分别列举了redis、ZooKeeper,MessageChannel,还有linux操作系统提供的共享内存等一系列的进程间通信方式。所以今天来分享一下到底如何实现nodejs的进程间通信。这里的讨论只限于linux系统,本机的进程。情况分为两种:父子进程,兄弟进程。
在nodejs中,实现进程间通信的方式其实只有一种,那就是unix域。linux系统提供了很多种进程间通信的方式,那么为什么nodejs选择unix域的,因为unix域相比其他进程间通信方式,有一个独特的优势,那就是传递文件描述符。unix域的实现是基于c/s模式的,类似tcp,udp。首先需要启动一个unix域服务器,然后各个unix客户端就可以"连接"这个服务器进行通信。
而在nodejs中父子进程的通信,底层使用的是socketpair,socketpair底层是也是unix域,不过他不是基于c/s模式的,如下图所示。
那么nodejs中,兄弟进程是如何通信的呢?最简单的方式就是通过主进程
但是这里多了一次中转,很明显效率上面会存着一些问题(相对直达和多一次中转,性能比较是很明显的,但是没具体测过。而且据说egg就是这么搞的,有了解的同学可以交流一下)。所以我们一般通过unix域实现兄弟进程的通信,但是我们需要做的事情就比较多了。我们看看unix域的类型。Unix域支持两种数据模式
1 流式( SOCK_STREAM),类似tcp,数据为字节流,需要应用层处理粘包问题。
2 数据报模式( SOCK_DGRAM ),类似udp,不需要处理数据边界。
但是不巧的是Nodejs使用的是流式模式,所以问题就变得复杂。这时候我们通过c/s模式虽然可以实现兄弟进程间的通信,但是我们拿到的数据可能是"乱的",这时候为什么呢?一般情况下,客户端给服务器发送1个字节,然后服务器处理,如果是基于这种场景,那么数据就不会是乱的。因为每次就是一个需要处理的数据单位。但是如果客户端给服务器发送1个字节,服务器还没来得及处理,客户端又发送了一个字节,那么这时候服务器再处理的时候,就会有问题。因为两个字节混一起了。就好比在一个tcp连接上先后发送两个http请求一样,如果服务器没有办法判断两个请求的数据边界,那么处理就会有问题。
我们写一个测试的例子。
unix域服务器
const net = require('net');net.createServer((client) => { client.on('data', (data) => { console.log(data.toString('utf-8')) })}).listen('\\\\?\\pipe\\ipc')
unix域客户端
const net = require('net');const socket = net.connect({path: '\\\\?\\pipe\\ipc'});setInterval(() => { socket.write('1'); socket.write('2');},1000)
我们看一下输出
我们看到输出可能是1然后2。也可能是12。在tcp协议中,这叫做粘包。那么我们如何解决这个问题呢?我们可以定义一个应用层协议。类似http协议一样,有了协议我们就知道,如何去解析收到的数据。接着我们还需要实现这个协议的解析器和封包逻辑,做完这些,我们就可以实现兄弟进程的通信了。
具体可参考ipc库的实现https://github.com/theanarkh/nodejs-ipc