pomelo研究筆記-RPC服務端,pomelo-rpc服務端
POMELO 採用多進程的架構可以很好的實現遊戲伺服器(進程)的擴充性,達到支撐較多線上使用者、降低伺服器壓力等要求。處理序間通訊採用RPC的形式來完成,pomelo的RPC實現的相當精巧。採用類似如下的方式就可以調用remote伺服器提供的服務:
proxies.user.test.service.echo(routeParam, 'hello', function(err, resp) { if(err) { console.error(err.stack); return; } console.log(resp);});
上面的一段RPC調用可以理解為:
調用namespace類型為user、伺服器類型為test的service模組的echo介面
現在聽著有些拗口,沒關係,且聽我慢慢來分析:)
服務端源碼分析
pomelo-rpc的源碼我閱讀+debug了不下30次,下面我將依照從底層資料交換模組到上層商務邏輯分發處理的方式依次介紹服務端與用戶端的源碼架構。
1. 基於socket.io模組的資料通訊模組
一般來說我們在寫socket資料通訊模組有幾個問題是必須要去解決的,譬如說:
- 粘包的問題
- 丟包以及亂序的問題
- ip地址過濾
- 緩衝隊列的實現
- 與上層模組的互動模式
這裡把pomelo-rpc實現過的來說一說。nodejs 內建一個events模組。這也導致了把一個模組封裝成一個事件收發器是相當自然的一件事情:
var Acceptor = function(opts, cb) { EventEmitter.call(this); this.bufferMsg = opts.bufferMsg; this.interval = opts.interval || 300; this.whitelist= opts.whitelist; this._interval = null; this.sockets = {}; this.msgQueues = {}; this.server = null; this.notify = cb;};util.inherits(Acceptor, EventEmitter);
利用node內建的util提供的繼承函數,簡單兩句話Acceptor繼承了events.翻開nodejs原始碼 inherits
函數的實現也是相當簡單:
var inherits = function(sub, super) { var tmp = function() {} tmp.prototype = super.prototype; sub.prototype = new tmp();}
通過這種寄生組合式的繼承避免了調用兩次父類的建構函式,這裡就不多展開了。
看到Acceptor建構函式接收一些配置資訊:
bufferMsg
: 配置是否啟用緩衝隊列interval
: 配置定時資料發送模組的間隔, Acceptor開啟監聽的時候,根據配置資訊來確定是否開啟一個定時器,定時重新整理緩衝:
if(this.bufferMsg) { this._interval = setInterval(function() { flush(self); }, this.interval); }
flush函數主要做的是負責把緩衝的資料通過socket.io介面寫出去:
var flush = function(acceptor) { var sockets = acceptor.sockets; var queues = acceptor.msgQueues; var queue, socket; for(var socketId in queues) { socket = sockets[socketId]; if(!socket) { delete queues[socketId]; continue; } queue = queues[socketId]; if(!queue.length) { continue; } socket.emit('message', queue); queues[socketId] = []; }};
每個用戶端連結對應一個資料緩衝隊列,通過發送’message’訊息的方式把資料發出。
IP地址過濾
開啟監聽後,如果有用戶端連結(on connection 事件),第一件事情是IP地址過濾,IP地址白名單也是通過建構函式注入:whitelist
.若IP地址非法則關閉連結,輸出警告資訊。
資料處理模組
上層模組通知配置資訊注入一個notify
回呼函數, acceptor
監聽到資料後首先把資料拋給上層。上層處理完畢後判斷如果需要緩衝則寫入隊列,否則馬上發送出去:
acceptor.notify.call(null, pkg.msg, function() { var args = Array.prototype.slice.call(arguments); for(var i = 0, l = args.length; i < l; i++) { if(args[i] instanceof Error) { args[i] = cloneError(args[i]); } } var resp = {id: pkg.id, resp: Array.prototype.slice.call(args)}; if(acceptor.bufferMsg) { enqueue(socket, acceptor, resp); } else { socket.emit('message', resp); } });
2. 路由請求分發模組
架在acceptor模組上面的是gateway
模組,該模組主要負責acceptor模組的建立銷毀以及狀態控制。首先在建立acceptor模組的時候傳入一個函數:
this.acceptor = this.acceptorFactory.create(opts, function(msg, cb) { dispatcher.route(msg, cb); });
通過Factory 方法來構建一個acctpor執行個體,這樣底層資料處理模組可以方便的更換通訊協定。這裡回呼函數做的一個工作是調用分發函數,把請求交給具體的服務提供者。來看看dispatcher的實現:
var Dispatcher = function(services) { EventEmitter.call('this'); var self = this; this.on('reload', function(services) { self.services = services; }); this.services = services;};util.inherits(Dispatcher, EventEmitter);
同樣Dispatcher模組也變成一個事件收發器。同時構造器接收一個services
參數。依據改參數配合路由請求時傳入的參數,就能把請求交給具體的子模組。所以,dispatcher.route(msg, cb);
只不過是匹配下參數調用對應介面罷了。看到構造器還監聽了一個reload
事件,該事件有什麼作用呢?這其實就是pomelo的RPC 熱拔插模組的實現。實現起來比較簡單:
var watchServices = function(gateway, dispatcher) { var paths = gateway.opts.paths; var app = gateway.opts.context; for(var i = 0; i < paths.length; i++) { (function(index) { fs.watch(paths[index].path, function(event, name) { if(event === 'change') { var res = {}; var item = paths[index]; var m = Loader.load(item.path, app); if(m) { res[namespace] = res[namespace] || {}; for(var s in m) { res[item.namespace][s] = m[s]; } } dispatcher.emit('reload', res); } }); })(i); }};
gateway
模組在啟動的時候會根據配置資訊調用一個watchServices
監聽模組的變化。如果資料檔案發生變化則重新載入services
並通知路由分發模組。為了保證伺服器與用戶端的正常通訊,除了底層資料格式的一致,還有一個是路由資訊的匹配。如果調用Gateway傳入的配置路徑是如下形式:
var paths = [ {namespace: 'user', path: __dirname + '/remote/test'}];
假設目前的目錄下有/remote/test/service.js
檔案,檔案包含兩個介面test1/test2
。 load
之後返回的對象形式如下:
{ service: { test1: 'function xxx', test2: 'function yyy' }}
同時在pomelo你們有系統RPC服務以及自訂RPC服務,完整的路由資訊如下:
services: { sys: { sys_module1: { sys_module1_interface1: 'xxx' } }, user: { user_module1: { user_module1_interface1: 'yyy' } }}
服務端其他東西都比較簡單了,為了理清楚脈絡,以上代碼是經過刪減的,如果有興趣可以到這裡取。