有一个应用场景:用C/C++/Java等编译型语言做NodeJS服务器背后高性能计算的组件,那么应该如何实现?
比较好的方法是用Socket通讯,但这就需要双方都要进行套接字编程。
NodeJS倒是好说,但其他语言的套接字编程就不一定简单了。
所以本文中以NodeJS/C++为例,用管道来进行通讯,就像标准输入输出一样简单。
本文仅作演示,默认所有文件都在同一个目录。
先以一个可以持续 输入/输出 循环的简单C++程序为例,使用阻塞输入可以很好地解决问题。
#include
using namespace std;
int main() {
int n;
while(cin >> n) {
cout << n + 1 << endl;
}
return 0;
}
使用 scanf, printf, cin, cout 可以凭自己喜好,经测试都是可以的。
是否使用 endl, flush, \n 都是可以的。
然后编译成 a
(Windows:a.exe
;Unix:a.out
,不用在意这些细节)
创建一个 i.js
的文件,并写入:
var path = require('path');
var child = require('child_process').spawn(path.join(__dirname, 'a'));
child.stdout.on('data', function (data) {
console.log('stdout: ' + data);
});
child.stderr.on('data', function (data) {
console.log('stderr: ' + data);
});
child.stdin.write('1\n'); // Attention!
注意child.stdin.write
的格式要与C++程序设计地一致。
本例中如果不加入\n
,C++中的输入不会结束,也就不会有输出,更不会触发child.stdout
的data
事件。
调用
child.stdin.end()
之后,C++程序会接收到 EOF 输入,正常的程序就会退出。所以在持续运行的程序中不要使用child.stdin.end()
方法。
使用
$ node i.js
即可看到
stdout: 2
显然我们需要在多个NodeJS模块使用同一个C++进程的。
使用同一个进程的好处是,C++程序的生命周期变长,可持久化的数据结构得以持续存在。
child.js
var path = require('path');
var spawn = require('child_process').spawn;
var child = spawn(path.join(__dirname, 'a'));
module.exports = child;
使用 child.stdout.on('data', (data) => { ... })
来设计 Middleware 模式,但数据不能通过中间件内部修改直接传递(因为参数 data 是 string 类型,不使用引用赋值),要另外封装一个变量来使用中间件。
对于某个特定的输入,我们通常要用一个逻辑处理C++返回的输出。
caller1.js
var child = require('./child');
function caller1(input) {
child.stdout.once('data', function (data) {
console.log('caller1 result: ' + data);
});
child.stdin.write(input);
}
module.exports = caller1;
可以类似地构造一个 caller2 ,分别调用 caller1 与 caller2 的时候,会只使用对应的回调函数处理。
有个坑点是:如果在过短的时间内调用 caller1 与 caller2 时,caller1, caller2接收到数据是两者的和。
因为他们的监听器在数据回收之前就被加入data
事件的监听器列表里了。
解决方案是延迟后一个caller函数的执行时间,当前一个caller的once监听器执行之后再回调后面这个caller。
或者简单一些直接估计data
的反应时间(这不保险)
Message Queue
为了解决之前提到的问题,我们不直接将消息写入child.stdin
,也不直接将监听器加到child.stdout
中。
sender.js
var child = require('./child');
var messages = []; // MQ Container
// fired when child response
child.stdout.on('data', function (data) {
// get handler and dequeue
var action = messages.shift();
// call handler if exists
action && action.handler && action.handler(data);
// send next message to child if exists
if (messages.length > 0) {
child.stdin.write(messages[0].msg);
}
})
// enqueue message
function send(msg, callbackFn) {
// if no message in queue, send the first message to child
if (messages.length === 0) {
child.stdin.write(msg);
}
// enqueue the message and handler
messages.push({
msg: msg,
handler: callbackFn
});
}
module.exports = send;
检查逻辑,你必须使得每次send都能得到C++程序的响应,不然就一直不会触发事件,导致MQ停滞,但NodeJS不会阻塞。
试用一下:
main.js
var sender = require('./sender');
sender('1\n', (data) => console.log('func 1: ' + data));
sender('3\n', (data) => console.log('func 2: ' + data));
console.log('MQ Enqueue Completed');
运行结果:
MQ Enqueue Completed
func 1: 2
func 2: 4
很好,基本没有问题,关于工程化的封装以后再说。
Child Process | node.js API docs v6.x