程式間通訊的另類實現

xiaoqb發表於2016-01-27

背景

Node.js 內建的程式間通訊使用非常簡單,但也有一定的侷限性,只能在父子程式間通訊。下面是官方文件給的一個例子。

首先是父程式的 parent.js :

const cp = require(`child_process`);
const n = cp.fork(`${__dirname}/sub.js`);

n.on(`message`, (m) => {
console.log(`PARENT got message:`, m);
});

n.send({ hello: `world` });

接著再看看子程式 sub.js 的實現:

process.on(`message`, (m) => {
console.log(`CHILD got message:`, m);
});

process.send({ foo: `bar` });

如果兩個程式間沒有父子這種親緣關係又如何通訊呢,本文就為大家講解 Midway 5.0 中如何使用更靈活的 socket 實現任意程式間的通訊。

協議設計

既然是通訊,那麼通訊協議的設計是必不可少的。就像以前經常在電影裡看到的兩個人通話時,都會加上一句 over 來告知對方自己要說的已經說完了。父子程式間的通訊協議也採用了這種最簡單最高效的方式,雙方在傳送訊息時都會在訊息末尾加上一個回車符 
,表示本次傳送的訊息就這麼多,也就是對方就收訊息時遇到 
 表明本次訊息接收完畢。

訊息接收後需要對其進行解碼,或者說是反序列化,最終便於識別和使用。父子程式間通訊就採用了 JSON.encode 和 JSON.decode 來實現訊息的編碼和解碼。

父子程式間採用的這種通訊協議非常的簡單,但是也非常高效,能夠滿足大部分使用場景。像 HSF 這類 RPC 呼叫通訊協議就比較複雜了,我們平時遇到最多的就是 HTTP 協議,做 web 開發的同學肯定都比較清楚協議的規則了。

本次實現的利用 socket 實現程式間通訊也採用這種最簡單的方式。

實現

實現協議之前回想一下整個通訊的流程,首先雙方建立一條全雙工的通訊通道,待 2 邊都 ready 後訊息便可以傳送訊息了,2 邊既是訊息的接收方也是訊息的傳送方。我們平時會將一方稱為 server,另一方稱為 client,這是在功能上的劃分,一般 server 會有多個client 同時連線。

協議解析

在雙方開始通訊之前,我們先來實現協議的解析 parse.js,非常的簡單。

`use strict`;
const StringDecoder = require(`string_decoder`).StringDecoder;
const EventEmitter = require(`events`);

class Parser extends EventEmitter {
constructor() {
super();
this.decoder = new StringDecoder(`utf8`);
this.jsonBuffer = ``;
}

encode(message) {
return JSON.stringify(message) + ` `;
}

feed(buf) {
let jsonBuffer = this.jsonBuffer;
jsonBuffer += this.decoder.write(buf);
let i, start = 0;
while ((i = jsonBuffer.indexOf(` `, start)) >= 0) {
const json = jsonBuffer.slice(start, i);
const message = JSON.parse(json);
this.emit(`message`, message);
start = i + 1;
}
this.jsonBuffer = jsonBuffer.slice(start);
}
}

module.exports = Parser;

socket 通訊

我們平時用到的 socket 大都是 TCP 型別的,因為要涉及到兩個遠端程式間的通訊。如果只是實現本地程式間通訊,可以選擇更高效的檔案 socket,同時也避免額外佔用一個埠的情況。

Client

在使用上 TCP socket 和 file socket 差別不大,前者監聽某個埠,後者監聽某個臨時檔案,需要注意的是監聽檔案的路徑在 Windows 和 Unix 上有些不一樣。

On Windows, the local domain is implemented using a named pipe. The path must refer to an entry in ?pipe or .pipe.

開始之前,大致列出客戶端需要有哪些功能

  • 連線到伺服器
  • 監聽伺服器傳送的資料,按照協議規則解析出訊息實體
  • 提供向伺服器傳送訊息的介面

client.js

`use strict`;
const path = require(`path`);
const net = require(`net`);
const Parser = require(`./parser`);
const EventEmitter = require(`events`);
const os = require(`os`);
const tmpDir = os.tmpDir();
let sockPath = path.join(tmpDir, `midway.sock`);

if (process.platform === `win32`) {
sockPath = sockPath.replace(/^//, ``);
sockPath = sockPath.replace(///g, `-`);
sockPath = `\\.\pipe\` + sockPath;
}

class Client extends EventEmitter{
constructor(options) {
options = options || {};
super();
if (options.socket) {
this.socket = options.socket;
} else {
this.socket = net.connect(sockPath);
}
this.bind();
}

bind() {
const parser = new Parser();
const socket = this.socket;
socket.on(`data`, (buf) => {
parser.feed(buf);
});

parser.on(`message`, (message) => {
this.emit(`message`, message);
});
this.parser = parser;
}

send(message) {
this.socket.write(this.parser.encode(message));
}
}

module.exports = Client;

Server

實現之前先梳理下 server 端要有哪些基礎的功能,

  • 建立一個 net server
  • 監聽某個檔案
  • 接收新連線上的客戶端
  • 根據接收到的資料按協議規則解析出訊息實體,處理客戶端請求

下面就是一個簡單的 server 實現

`use strict`;
const path = require(`path`);
const fs = require(`fs`);
const net = require(`net`);
const Client = require(`./client`);
const EventEmitter = require(`events`);
const os = require(`os`);
const tmpDir = os.tmpDir();
let sockPath = path.join(tmpDir, `midway.sock`);

if (process.platform === `win32`) {
sockPath = sockPath.replace(/^//, ``);
sockPath = sockPath.replace(///g, `-`);
sockPath = `\\.\pipe\` + sockPath;
}

class Server extends EventEmitter{
constructor() {
super();
this.server = net.createServer((socket)=> this.handleConnection(socket));
}

listen(callback) {
if (fs.existsSync(sockPath)) {
fs.unlinkSync(sockPath);
}
this.server.listen(sockPath, callback);
}

handleConnection(socket) {
const client = new Client({
socket: socket
});
client.on(`message`, (message) => {
this.handleRequest(message, client);
});
this.emit(`connect`, client);
}

handleRequest(message, client) {
this.emit(`message`, message, client);
}
}

module.exports = Server;

demo

至此,我們已經實現了類似父子程式間通訊的功能了。還記得上篇《多程式下的測試覆蓋率》中使用到的那個簡單的 RPC demo,現在我們使用上面提到的 socket 通訊方式重新實現一個。

client.js

`use strict`;
const Client = require(`../lib/Client`);

let rid = 0;
const service = {};
const queue = [];
const requestQueue = new Map();

function start(ready) {
const client = new Client();

function send() {
rid++;
let args = [].slice.call(arguments);
const method = args.slice(0,1)[0];
const callback = args.slice(-1)[0];

const req = {
rid: rid,
method:method,
args:args.slice(1,-1)
};

requestQueue.set(rid,Object.assign({
callback: callback
}, req));

client.send(req);
}

client.on(`message`, function(message){
if (message.action === `register`) {
message.methods.forEach((method) => {
service[method] = send.bind(null, method);
});
ready(service);
} else {
const req = requestQueue.get(message.rid);
const callback = req.callback;
if (message.success) {
callback(null, message.data);
} else {
callback(new Error(message.error));
}
requestQueue.delete(message.rid);
}
});
}

start((service)=> {
service.add(1,2,3,4,5, function(err, result) {
console.log(`1+2+3+4+5 = ${result}`);
});

service.time(1,2,3,4,5, function(err, result) {
console.log(`1*2*3*4*5 = ${result}`);
});
});

server.js

`use strict`;
const Server = require(`../lib/server`);
const server = new Server();
server.listen();

const service = {
add() {
const args = [].slice.call(arguments);
return args.slice().reduce(function(a,b) {
return a+b;
});
},

time() {
const args = [].slice.call(arguments);
return new Promise((resolve, reject)=> {
setTimeout( ()=> {
const ret = args.slice().reduce(function(a,b) {
return a*b;
});
resolve(ret);
}, 1000);
});
}
}


server.on(`connect`, (client) => {
client.send({
action:`register`,
methods: Object.keys(service)
});
});

server.on(`message`, function(message, client) {
let ret = { success: false, rid: message.rid };
const method = message.method;
if (service[method]) {
try {
const result = service[method].apply(service, message.args);
ret.success = true;
if(result.then) {
return result.then((data)=> {
ret.data = data;
client.send(ret);
}).catch((err)=>{
ret.success = false;
ret.error = err.message;
client.send(err);
})
}
ret.data = result;
} catch (err) {
ret.error = err.message;
}
}
client.send(ret);
});

先啟動 server,然後執行 client,控制檯輸出

1+2+3+4+5 = 15
1*2*3*4*5 = 120

小結

不論是本文講解的簡單程式間通訊,還是更復雜的 RPC 呼叫,整個的設計實現流程相差不大。生產環境中還需要處理各種異常,網路連線錯誤,協議解析錯誤等等,有興趣的同學可以繼續在本 demo上繼續完善~

該文章來自:http://taobaofed.org/blog/2016/01/26/nodejs-ipc/

作者:淘傑


相關文章