背景
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/
作者:淘傑