Pomelo PRC

Pomelo中游戲服務(wù)器是一個(gè)多進(jìn)程相互協(xié)作的環(huán)境,各個(gè)進(jìn)程之間通信采用RPC(遠(yuǎn)程過(guò)程調(diào)用)的形式完成,通過(guò)底層統(tǒng)一的RPC框架來(lái)實(shí)現(xiàn),服務(wù)器間的RPC調(diào)用實(shí)現(xiàn)了零配置。游戲服務(wù)器進(jìn)程的擴(kuò)展以達(dá)到支撐較多的用戶(hù),降低服務(wù)器壓力等要求。

RPC框架主要解決了“進(jìn)程間消息的路由”和“RPC底層通訊協(xié)議的選擇”兩個(gè)問(wèn)題

  1. 進(jìn)程間的路由策略
    Pomelo提供了一套靈活的路由機(jī)制,并允許開(kāi)發(fā)者根據(jù)需要自由地控制了路由信息。

  2. RPC底層的通信協(xié)議選擇
    Pomelo現(xiàn)在支持具有socket.io的通信機(jī)制和基于原生socket的通信機(jī)制。RPC框架目前在底層采用socket.io作為通訊協(xié)議,協(xié)議對(duì)上層是透明的,可替換為任意的協(xié)議。

pomelo服務(wù)器之間通信采用rpc調(diào)用方式,依賴(lài)pomelo-rpc項(xiàng)目,pomelo-rpc項(xiàng)目使用stream-pkg庫(kù)序列化rpc通信數(shù)據(jù),可選擇使用wsSocket或tcpSocket連接模式。pomelo-rpc是pomelo項(xiàng)目底層的rpc框架,提供了一個(gè)多服務(wù)器進(jìn)程間進(jìn)行rpc調(diào)用的基礎(chǔ)設(shè)施,pomelo-rpc分為客戶(hù)端和服務(wù)端兩部分。客戶(hù)端提供rpc代理生成、消息路由、網(wǎng)路通訊等功能,并支持動(dòng)態(tài)添加代理和遠(yuǎn)程服務(wù)器配置。服務(wù)端提供遠(yuǎn)程服務(wù)暴露、請(qǐng)求派發(fā)、網(wǎng)路通訊等功能。

命名空間

pomelo內(nèi)部rpc協(xié)議格式

msg{
  route: nameSpace.serverType.module.method//路由信息
  args:[]//調(diào)用參數(shù)
}

nameSpace命名空間有兩種分別是系統(tǒng)sys和用戶(hù)user

命名空間 描述
sys 系統(tǒng)命名空間
user 用戶(hù)命名空間
  • 系統(tǒng)命名空間sys

系統(tǒng)命名空間對(duì)應(yīng)app.sysRpc方法及app.rpcInvoke方法,pomelo系統(tǒng)內(nèi)部處理的rpc事件需經(jīng)過(guò)common.remote包下的系統(tǒng)rpc服務(wù)處理。

  • 用戶(hù)命名空間user

用戶(hù)命名空間對(duì)應(yīng)app.rpc方法,是由服務(wù)器內(nèi)部組件發(fā)起的rpc調(diào)用,無(wú)需經(jīng)過(guò)common.remote包下的系統(tǒng)rpc服務(wù)處理,最終邏輯由app.servers.serverType.remote.module.method執(zhí)行。

命名空間的分類(lèi)方法從實(shí)現(xiàn)角度來(lái)說(shuō)

  • 系統(tǒng)命名空間sys
  1. 系統(tǒng)提供了一些輔助服務(wù)比如sessionRemote和channelRemote,突顯connector負(fù)責(zé)為所有后端服務(wù)廣播消息給客戶(hù)端,從而起到隔離后端服務(wù)器的作用。

  2. 系統(tǒng)實(shí)現(xiàn)基于長(zhǎng)連接模式比如wsSocket下的C/S架構(gòu)的request/response通信框架,比如msgRemote的forwardMessage消息轉(zhuǎn)發(fā)服務(wù)。

根據(jù)這兩大類(lèi)的基礎(chǔ)功能,可將系統(tǒng)命名空間內(nèi)的rpc調(diào)用理解為客戶(hù)端與服務(wù)器之間的基礎(chǔ)通信框架,因此pomelo將其實(shí)現(xiàn)放在common.remote包下。

  • 用戶(hù)命名空間user

用戶(hù)命名空間pomelo未作特別處理,pomelo-server庫(kù)直接將rpc調(diào)用分派到對(duì)應(yīng)由pomelo使用者開(kāi)發(fā)的后端服務(wù)器rpc接口上,所以這個(gè)命名空間稱(chēng)為user。

與sys rpc相關(guān)的remote包括三種

  • sessionRemote

sessionRemote僅在connector前端服務(wù)器中提供,以供后端服務(wù)器將其本地的session同步到前端服務(wù)器,使用bind或push之類(lèi)的方法。

  • channelRemote

channelRemote僅在connector前端服務(wù)器中提供,實(shí)現(xiàn)廣播消息,即后端要廣播消息時(shí)可調(diào)用此rpc服務(wù)。

  • msgRemote

msgRemote僅在后端服務(wù)器中提供,通過(guò)forwardMessage方法委托server組件按路由信息serverType.module.method分派消息到對(duì)應(yīng)的handler去處理。

application對(duì)象中三個(gè)關(guān)于rpc方法

  • app.sysRpc()
    表示命名空間為sys的rpc調(diào)用封裝,app.sysRpc中的service對(duì)應(yīng)app.rpc參數(shù)中的module。
  • app.rpcInvote()
    表示直接的rpc調(diào)用,與pomelo-rpc/client中的rpc調(diào)用方法一致, 需指定所有調(diào)用參數(shù),包括context、serverId、namespace、service、method、args。
  • app.rpc()
    表示命名空間為user的rpc調(diào)用封裝

RPC調(diào)用類(lèi)型

Pomelo中使用RPC調(diào)用進(jìn)行進(jìn)程間通信,在Pomelo中RPC調(diào)用分為兩類(lèi),使用namespace命令空間進(jìn)行區(qū)分。

命名空間 調(diào)用類(lèi)型 描述
sys sys rpc Handler
user user rpc Remote
  1. 系統(tǒng)遠(yuǎn)程調(diào)用sys rpc
  • 一般調(diào)用Handler,比如客戶(hù)端調(diào)用網(wǎng)關(guān)服務(wù)器gate.gateHandler.entry。
  • namespacesys的是系統(tǒng)RPC調(diào)用,它對(duì)用戶(hù)來(lái)說(shuō)是透明的。
Handler.prototype.add = function(session, msg, callback){}

Pomelo中系統(tǒng)RPC調(diào)用有三種

  • 后端服務(wù)器向前端服務(wù)器請(qǐng)求Session會(huì)話(huà)信息
  • 后端服務(wù)器通過(guò)channel頻道推送消息時(shí)對(duì)前端服務(wù)器的RPC調(diào)用
  • 前端服務(wù)器將用戶(hù)請(qǐng)求route路由給后端服務(wù)器時(shí)也是sys rpc調(diào)用

除了系統(tǒng)RPC調(diào)用之外,其余的由用戶(hù)自定義的RPC調(diào)用屬于user namespace的RPC調(diào)用,需要用戶(hù)自己完成RPC服務(wù)端remotehandler代碼,并由RPC客戶(hù)端顯式地發(fā)起調(diào)用。

  1. 用戶(hù)遠(yuǎn)程調(diào)用

一般調(diào)用Remote,比如app.rpc.chat.chatRemote.kick

例如:定義聊天服務(wù)器的remote服務(wù)器的rpc接口

$ vim /game-server/servers/chat/remote/chatRemote.js
const Remoet = function(app){
  this.app = app;
};

Remote.protype.kick = function(uid, player, cb){

}

module.exports = function(app){
  return new Remote(app);
};

其它服務(wù)器作為rpc客戶(hù)端只需要通過(guò)接口即可實(shí)現(xiàn)rpc調(diào)用

app.rpc.chatRemote.kick(session, uid, player, function(data){});

這個(gè)rpc調(diào)用會(huì)根據(jù)特定的路由規(guī)則轉(zhuǎn)到特定的服務(wù)器,比如場(chǎng)景服務(wù)器的請(qǐng)求會(huì)根據(jù)玩家在哪個(gè)場(chǎng)景直接轉(zhuǎn)發(fā)到對(duì)應(yīng)的服務(wù)器。

RPC調(diào)用原理

rpc組件是用于服務(wù)器進(jìn)程間通訊的一種實(shí)現(xiàn),能夠以類(lèi)似函數(shù)調(diào)用的方式來(lái)實(shí)現(xiàn)進(jìn)程間通訊,此組件基于pomelo-rpc實(shí)現(xiàn),也是額外封裝了一層以便于使用。

pomelo中rpc的調(diào)用主要是通過(guò)proxy組件和remote組件實(shí)現(xiàn)的,proxy組件和remote組件結(jié)合pomelo內(nèi)部使用的rpc協(xié)議分別實(shí)現(xiàn)了rpc-serverrpc-client的功能封裝。

組件 rpc 描述
proxy rpc-client 負(fù)責(zé)創(chuàng)建rpc客戶(hù)端代理(rpc client stub)、加載路由策略、消息轉(zhuǎn)發(fā)
remote rpc-server 負(fù)責(zé)加載rpc服務(wù)(rpc server stub)

rpc客戶(hù)端 / proxy組件

proxy組件主要負(fù)責(zé)創(chuàng)建RPC客戶(hù)端代理,讓開(kāi)發(fā)者在Pomelo中更方便地進(jìn)行RPC調(diào)用。
proxy組件是一個(gè)重量級(jí)的組件,它被除master以外的所有服務(wù)器加載。

例如:用戶(hù)自定義rpc調(diào)用

// app 表示pomelo應(yīng)用對(duì)象
// app.rpc表示前后臺(tái)服務(wù)器的Remote RPC調(diào)用
// chat表示服務(wù)器的名稱(chēng)
// chatRemote表示i對(duì)應(yīng)的文件名稱(chēng)
// add表示對(duì)應(yīng)的方法名
// 為了實(shí)現(xiàn)這個(gè)RPC調(diào)用需要在對(duì)應(yīng)的chat/remote/chatRemote.js文件中實(shí)現(xiàn)add方法
app.rpc.chat.chatRemote.add(session, uid, app.get("serverId"), param, cb);

在Pomelo中之所以能夠如此簡(jiǎn)潔地進(jìn)行RPC調(diào)用是因?yàn)镴avaScript語(yǔ)言特性和Pomelo底層對(duì)RPC客戶(hù)端進(jìn)行的封裝。

proxy組件在啟動(dòng)時(shí)首先會(huì)生成一個(gè)rpc客戶(hù)端,同時(shí)監(jiān)聽(tīng)系統(tǒng)中服務(wù)器增加、服務(wù)器移除、服務(wù)器替換事件。當(dāng)這些事件被觸發(fā)時(shí),proxy組件會(huì)根據(jù)相應(yīng)的事件信息對(duì)服務(wù)器代理對(duì)象進(jìn)行相應(yīng)的動(dòng)態(tài)變化。比如當(dāng)有新的服務(wù)器增加時(shí),proxy組件會(huì)增加該服務(wù)器的代理對(duì)象。當(dāng)有服務(wù)器被移除后,proxy組件會(huì)移除該服務(wù)器的代理對(duì)象。

在proxy組件啟動(dòng)完成時(shí)會(huì)將rpc客戶(hù)端生成的代理對(duì)象掛載到app.rpc下,這樣開(kāi)發(fā)者在進(jìn)行rpc調(diào)用時(shí)就可以匹配到相應(yīng)的代理對(duì)象,從而通過(guò)rpc客戶(hù)端進(jìn)行相應(yīng)的rpc調(diào)用。

proxy組件會(huì)掃描具體應(yīng)用服務(wù)器的目錄,抽取其中的remote部分,由于JavaScript的動(dòng)態(tài)性,可以輕易地得到remote中關(guān)于遠(yuǎn)程調(diào)用的元信息來(lái)生成存根stub,并將這些調(diào)用都掛到app.rpc下。

當(dāng)用戶(hù)發(fā)起rpc調(diào)用時(shí),proxy組件會(huì)查看器其掃描的存根stub信息以此決定此遠(yuǎn)程調(diào)用是否合法。同時(shí)proxy又會(huì)創(chuàng)建一個(gè)rpc客戶(hù)端,當(dāng)發(fā)起遠(yuǎn)程調(diào)用時(shí),負(fù)責(zé)與遠(yuǎn)端的remote進(jìn)行通信,并得到遠(yuǎn)程調(diào)用的結(jié)果供調(diào)用者使用。當(dāng)進(jìn)行遠(yuǎn)程調(diào)用時(shí)由于同類(lèi)型的遠(yuǎn)程服務(wù)器可能有多個(gè),所以這里同樣需要配置相應(yīng)的router。

配置proxy使用

app.set("proxyConfig", opts);

proxy的配置項(xiàng)

配置 描述
cacheMsg 配置cacheMsg為true則開(kāi)啟rpc調(diào)用時(shí)對(duì)消息的緩沖,而非直接有rpc請(qǐng)求就發(fā)出。
interval 與配置cacheMsg配置使用,設(shè)置flush緩存的周期。
mailBoxFactory rpc底層實(shí)現(xiàn)需要,用戶(hù)可以定義自己的mailBoxFactory。

開(kāi)啟rpc調(diào)用日志

app.enable("rpcDebugLog");

rpc服務(wù)端 / remote組件

  • remote組件主要負(fù)責(zé)加載RPC服務(wù),包括系統(tǒng)的RPC服務(wù)和用戶(hù)的RPC服務(wù)。
  • remote組件在啟動(dòng)時(shí)會(huì)創(chuàng)建一個(gè)rpc服務(wù)器,同時(shí)加載系統(tǒng)中所有的rpc服務(wù)。
  • remote組件在關(guān)閉時(shí)會(huì)停止rpc服務(wù)器的所有服務(wù)

后端服務(wù)器的remote用來(lái)給connector服務(wù)器提供rpc調(diào)用使用,比如玩家掉線,由于connector承載客戶(hù)端連接,必然是connector服務(wù)器首先知道玩家掉線,因此掉線后connector服務(wù)器進(jìn)行rpc調(diào)用chatRomte離開(kāi),進(jìn)行后端服務(wù)器狀態(tài)設(shè)置。

  • remote模塊是遠(yuǎn)程通訊模塊和服務(wù)端監(jiān)聽(tīng)模塊
  • remote模塊的作用是作為各個(gè)模塊間通訊對(duì)象而存在
  • remote模塊使用rpc協(xié)議作為rpc服務(wù)端存在
  • remote對(duì)象在app.components._remote_變量中保存

remote組件是與proxy組件對(duì)等的組件,remote組件用來(lái)提供rpc調(diào)用服務(wù)。rpc組件完成對(duì)當(dāng)前服務(wù)器的remote的加載,并開(kāi)啟監(jiān)聽(tīng)端口,等待rpc客戶(hù)端的連接及相應(yīng)的rpc調(diào)用。當(dāng)接收到具體的調(diào)用請(qǐng)求時(shí),會(huì)根據(jù)調(diào)用請(qǐng)求中描述的調(diào)用請(qǐng)求信息,調(diào)用相應(yīng)的remote中的相應(yīng)方法。然后再將具體的處理記過(guò)返回給rpc客戶(hù)端。rpc服務(wù)器還支持對(duì)調(diào)用請(qǐng)求的filter,也就是說(shuō)跟server組件處理客戶(hù)端請(qǐng)求一樣,rpc服務(wù)端處理具體請(qǐng)求時(shí)也會(huì)使用filter-remote鏈進(jìn)行處理。

項(xiàng)目中Remote邏輯設(shè)置有兩個(gè)地方,一個(gè)是系統(tǒng)調(diào)用sys對(duì)應(yīng)pomelo/lib/common/remote下的backendfrontend,具體哪個(gè)路徑?jīng)Q定了當(dāng)前服務(wù)器是前端服務(wù)器還是后端服務(wù)器,即在servers.json中配置的frontend選項(xiàng)是true或是false。另一個(gè)是用戶(hù)調(diào)用user,user對(duì)應(yīng)了app/servers/服務(wù)器類(lèi)型名稱(chēng)/remote目錄下的JS文件。

{"id":"connector-server-1", "host":"127.0.0.1", "port":4050, "clientPort": 3050, "frontend": true},

remote組件創(chuàng)建

 //于創(chuàng)建rpc server的,用于接收別的服務(wù)器的遠(yuǎn)程調(diào)用的消息,并執(zhí)行相應(yīng)的方法
module.exports = function(app, opts) {
  opts = opts || {};
  opts.cacheMsg = opts.cacheMsg || false;
  opts.interval = opts.interval || 30;
  return new Remote(app, opts);  //創(chuàng)建remote組件
};
 
/**
 * Remote component class
 *
 * @param {Object} app  current application context
 * @param {Object} opts construct parameters
 */
 //將一些數(shù)據(jù)保存
var Remote = function(app, opts) {
  this.app = app;
  this.opts = opts;
}

remote組件配置項(xiàng)

配置 描述
cacheMsg 與proxy組件的含義相同
interval 與proxy組件的含義相同
acceptorFactory rpc底層實(shí)現(xiàn)需要,可認(rèn)為跟proxy配置中的mailBoxFactory是對(duì)等的。

配置remote組件使用

app.set("remoteConfig", opts);

開(kāi)啟rpcDebugLog來(lái)得到所有的rpc調(diào)用過(guò)程日志

app.enable("rpcDebugLog");

remote組件啟動(dòng)

真正進(jìn)行創(chuàng)建的過(guò)程是在remote組件的啟動(dòng)中

pro.start = function(cb) {
  this.opts.port = this.app.getCurServer().port;  //用于接受遠(yuǎn)程服務(wù)器連接的端口進(jìn)行rpc
  this.remote = genRemote(this.app, this.opts);  //創(chuàng)建remotes服務(wù)器,用接受遠(yuǎn)程調(diào)用的信息,其實(shí)這里是gateway
  this.remote.start();  //啟動(dòng)rpc server 
  process.nextTick(cb);
};

remote組件啟動(dòng)時(shí)可分為兩步,首先是創(chuàng)建pomelo自定義的rpc服務(wù)器,然后再啟動(dòng)它。

rpc服務(wù)器的創(chuàng)建 genRemote(app, opts)

//創(chuàng)建rpc服務(wù)器
var genRemote = function(app, opts) {
  opts.paths = getRemotePaths(app);  //執(zhí)行遠(yuǎn)程rpc的源代碼放的目錄
  opts.context = app;
  return RemoteServer.create(opts);
};

rpc服務(wù)器創(chuàng)建時(shí)首先會(huì)獲取執(zhí)行遠(yuǎn)程調(diào)用的源代碼文件路徑用于加載。遠(yuǎn)程調(diào)用分為sys命名空間和user命名空間,用于執(zhí)行遠(yuǎn)程調(diào)用也分為兩部分。

遠(yuǎn)程服務(wù)器創(chuàng)建RemoteServer.create(opts)

//創(chuàng)建gateway,用于接收遠(yuǎn)程rpc的連接
module.exports.create = function(opts) {
  if(!opts || !opts.port || opts.port < 0 || !opts.paths) {
    throw new Error('opts.port or opts.paths invalid.');
  }
 
  var services = loadRemoteServices(opts.paths, opts.context);  //創(chuàng)建遠(yuǎn)程服務(wù)執(zhí)行的方法,這里也分為sys空間和user空間
  opts.services = services;  //保存剛剛創(chuàng)建的service
  var gateway = Gateway.create(opts);  //創(chuàng)建gateway
  return gateway;
};

遠(yuǎn)程服務(wù)器的創(chuàng)建過(guò)程可分為兩部分,首先是創(chuàng)建服務(wù)service,然后是創(chuàng)建gateway。

  • service服務(wù)創(chuàng)建的過(guò)程是將路徑下的源代碼讀取并將其定義的方法進(jìn)行保存用于執(zhí)行遠(yuǎn)程的調(diào)用。
  • gateway用于監(jiān)聽(tīng)端口,接收遠(yuǎn)程調(diào)用發(fā)送過(guò)來(lái)的信息。

加載遠(yuǎn)程服務(wù)loadRemoteServices(paths, context)

加載遠(yuǎn)程服務(wù)后,當(dāng)執(zhí)行遠(yuǎn)程調(diào)用時(shí)可直接使用索引相應(yīng)的方法即可執(zhí)行即可。

//將path下面的源文件讀取進(jìn)來(lái),他們里面定義的方法將會(huì)用于執(zhí)行遠(yuǎn)程的調(diào)用
var loadRemoteServices = function(paths, context) {
  var res = {}, item, m;
  for(var i=0, l=paths.length; i<l; i++) {
    item = paths[i];
    m = Loader.load(item.path, context);
 
    if(m) {
      createNamespace(item.namespace, res);
      for(var s in m) {  //這里可以理解為遍歷讀進(jìn)來(lái)的源文件
        res[item.namespace][s] = m[s];  //用于將方法保存起來(lái),s就是源文件的名字,也就是所謂的service的名字,然后里面還要分method的名字
      }
    }
  }
 
  return res;
};
 
var createNamespace = function(namespace, proxies) {
  proxies[namespace] = proxies[namespace] || {};
);

創(chuàng)建gateway Gateway.create(opts)

module.exports.create = function(opts) {
  if(!opts || !opts.services) {
    throw new Error('opts and opts.services should not be empty.');
  }
 
  return new Gateway(opts);
};
var Gateway = function(opts) {
  EventEmitter.call(this);
  this.port = opts.port || 3050;
  this.started = false;
  this.stoped = false;
  this.acceptorFactory = opts.acceptorFactory || defaultAcceptorFactory;  //用于創(chuàng)建acceptor的工廠
  this.services = opts.services;
  var self = this;
  this.acceptor = this.acceptorFactory.create(opts, function(msg, cb) {  //創(chuàng)建acceptor,用于接受遠(yuǎn)程調(diào)用的信息
    dispatcher.route(msg, self.services, cb);
  });
};
util.inherits(Gateway, EventEmitter);

gateway創(chuàng)建中會(huì)創(chuàng)建acceptor,acceptor用于接收遠(yuǎn)程服務(wù)器發(fā)送過(guò)來(lái)的遠(yuǎn)程調(diào)用信息,acceptor分為tcp和websocket,以websocket為例。

module.exports.create = function(opts, cb) {
  return new Acceptor(opts || {}, cb);
};
var Acceptor = function(opts, cb){
  EventEmitter.call(this);
  this.cacheMsg = opts.cacheMsg;
  this.interval = opts.interval;  // flush interval in ms
  this._interval = null;          // interval object
  this.sockets = {};
  this.msgQueues = {};
  this.cb = cb;  //回調(diào)函數(shù),用于處理接收到的rpc消息
};
util.inherits(Acceptor, EventEmitter);

acceptor創(chuàng)建的過(guò)程中需要注意的是需要傳入一個(gè)回調(diào)函數(shù),用于處理acceptor接收到的消息,回調(diào)函數(shù)的定義作用是調(diào)用dispatcher的route方法,根據(jù)接收到的消息中的namespace命名空間、服務(wù)service、method方法、args參數(shù)等參數(shù)信息,調(diào)用相應(yīng)的函數(shù)來(lái)處理,然后將數(shù)據(jù)返回。

module.exports.route = function(msg, services, cb) {
  var namespace = services[msg.namespace];
  if(!namespace) {
    utils.invokeCallback(cb, new Error('no such namespace:' + msg.namespace));
    return;
  }
 
  var service = namespace[msg.service];
  if(!service) {
    utils.invokeCallback(cb, new Error('no such service:' + msg.service));
    return;
  }
 
  var method = service[msg.method];  //獲取需要調(diào)用的發(fā)那個(gè)發(fā)
  if(!method) {
    utils.invokeCallback(cb, new Error('no such method:' + msg.method));
    return;
  }
 
  var args = msg.args.slice(0);
  args.push(cb);
  method.apply(service, args);  //用需要調(diào)用的方法來(lái)處理數(shù)據(jù)
};

RPC請(qǐng)求流程

對(duì)于發(fā)送rpc請(qǐng)求,RPC客戶(hù)端采用了一種懶加載的機(jī)制,主要實(shí)現(xiàn)思想是客戶(hù)端與服務(wù)端的連接并不是在服務(wù)器啟動(dòng)后就創(chuàng)建,而是當(dāng)客戶(hù)端第一次向服務(wù)器發(fā)起RPC請(qǐng)求時(shí)才真正建立連接。當(dāng)客戶(hù)端與相應(yīng)的服務(wù)器建立連接后,以后有從該客戶(hù)端到對(duì)應(yīng)服務(wù)器的請(qǐng)求就無(wú)需再次建立連接,消息可以直接發(fā)送。消息的發(fā)送過(guò)程類(lèi)似handler-filter鏈?zhǔn)教幚砟J?,同樣在RPC請(qǐng)求過(guò)程中開(kāi)發(fā)者可以添加before和after filter對(duì)消息進(jìn)行相應(yīng)的處理,現(xiàn)在pomelo內(nèi)建的rpc filter包括rpcLog和toobusy。

  • 客戶(hù)端請(qǐng)求的RPC調(diào)用事件處理流程serverType.module.method
  • 服務(wù)器發(fā)起的RPC調(diào)用事件的處理流程

RPC客戶(hù)端

RPC客戶(hù)端主要負(fù)責(zé)產(chǎn)生代理對(duì)象、加載路由策略、進(jìn)行消息轉(zhuǎn)發(fā)

rpc客戶(hù)端架構(gòu)
  • 數(shù)據(jù)收發(fā)模塊 mail box

一個(gè)RPC客戶(hù)端可能同時(shí)需要調(diào)用多個(gè)遠(yuǎn)端服務(wù)器提供的服務(wù),在pomelo中每個(gè)server都被抽象為了一個(gè)mail box。

在RPC客戶(hù)端的整體架構(gòu)中,最底層是使用Mailbox的抽象隱藏了底層通訊協(xié)議的細(xì)節(jié),一個(gè)Mailbox對(duì)應(yīng)一個(gè)遠(yuǎn)程服務(wù)器的連接。

Mailbox對(duì)上提供了統(tǒng)一的接口,比如連接、發(fā)送、關(guān)閉等。

Mailbox內(nèi)部則可以提供不同的實(shí)現(xiàn),包括底層的傳輸協(xié)議、消息緩沖隊(duì)列、傳輸數(shù)據(jù)的包裝等。

開(kāi)發(fā)者可以根據(jù)實(shí)際需要實(shí)現(xiàn)不同的Mailbox來(lái)滿(mǎn)足不同的底層協(xié)議的需求?,F(xiàn)在Pomelo提供了基于socket.io的Mailbox和基于原生socket的Mailbox,默認(rèn)使用socket.io。

  • mail station

mail station主要實(shí)現(xiàn)的功能:客戶(hù)端狀態(tài)控制、遠(yuǎn)程服務(wù)器信息管理、過(guò)濾器、消息路由

mail station

在Mailbox上面是MailStation層,負(fù)責(zé)管理底層所有Mailbox實(shí)例的創(chuàng)建和銷(xiāo)毀以及對(duì)上層提供統(tǒng)一的消息分發(fā)接口。

上層代碼只要傳遞一個(gè)目標(biāo)Mailbox的ID,MailStation則可以知道如何通過(guò)底層相應(yīng)的MailBox實(shí)例將這個(gè)消息發(fā)送出去。

開(kāi)發(fā)者可以給MailStation傳遞一個(gè)MailBox的工廠方法即可以定制底層的MailBox實(shí)例的創(chuàng)建過(guò)程。比如連接到某個(gè)服務(wù)器,使用某類(lèi)型的MailBox,而其它服務(wù)器則使用另外一個(gè)類(lèi)型的MailBox。

  • route

再往上是路由層,路由層主要工作是提供消息路由的算法。

路由函數(shù)是可以從外面定制的,開(kāi)發(fā)者可以通過(guò)注入自定義的路由函數(shù)來(lái)實(shí)現(xiàn)自己的路由策略。

每個(gè)rpc消息分發(fā)前,都會(huì)調(diào)用路由函數(shù)進(jìn)行路由計(jì)算。

容器會(huì)提供與該rpc相關(guān)的玩家會(huì)話(huà)對(duì)象(其中包含了該玩家當(dāng)前的狀態(tài))和rpc的描述消息(其中包含了rpc的具體信息)。通過(guò)這兩個(gè)對(duì)象,即可做出路由的決策。

路由的結(jié)果是目標(biāo)Mailbox的ID,然后傳遞給底層的MailStation層即可。

  • proxy

最上層的是代理層,代理層主要作用是隱藏底層RPC調(diào)用的細(xì)節(jié)。

Pomelo會(huì)根據(jù)遠(yuǎn)程接口生成代理對(duì)象,上層代碼調(diào)用遠(yuǎn)程對(duì)象就像調(diào)用本地對(duì)象一樣。

對(duì)遠(yuǎn)程代理對(duì)象有兩個(gè)約定的規(guī)則:

  • 第一個(gè)參數(shù)必須是玩家的session對(duì)象,如果沒(méi)有可以填充為null,再路由函數(shù)中需做特殊處理。
  • 最后一個(gè)參數(shù)是rpc調(diào)用結(jié)果的回調(diào)函數(shù),調(diào)用的錯(cuò)誤或結(jié)果全部通過(guò)該回調(diào)函數(shù)返回,且這個(gè)參數(shù)不能夠省略。

而在遠(yuǎn)程服務(wù)的提供端,方法的聲明與代理端相比,除了不需要第一個(gè)session參數(shù),其余的參數(shù)是一樣的。

RPC服務(wù)器

RPC服務(wù)端主要負(fù)責(zé)接收客戶(hù)端的RPC請(qǐng)求,再將相應(yīng)的消息轉(zhuǎn)給客戶(hù)端請(qǐng)求的RPC服務(wù)中,同時(shí)將RPC服務(wù)處理完成的消息返回給RPC客戶(hù)端。

RPC服務(wù)器架構(gòu)
  • Acceptor
    RPC服務(wù)器最底層是Acceptor層主要負(fù)責(zé)網(wǎng)絡(luò)監(jiān)聽(tīng)、消息的接收和解析。
    Acceptor層與Mailbox層相應(yīng),可以看成是網(wǎng)絡(luò)協(xié)議棧上同一層上的兩端,即從Mailbox層傳入的消息與Acceptor層上傳出的消息應(yīng)該是同樣的內(nèi)容。
    所以這兩端的實(shí)例必須一致,使用同樣的底層傳輸協(xié)議,對(duì)傳輸?shù)臄?shù)據(jù)使用同樣格式進(jìn)行封裝。
    在客戶(hù)端替換了Mailbox的實(shí)現(xiàn),則在服務(wù)提供端也必須替換成對(duì)應(yīng)的Acceptor實(shí)現(xiàn)。
    同Mailbox一樣,Pomelo提供基于socket.io和基于原生socket的Acceptor。
  • diapatch
    RPC服務(wù)器往上是diapatch層,該層主要完成的工作是根據(jù)RPC描述消息將請(qǐng)求轉(zhuǎn)發(fā)給上層的遠(yuǎn)程服務(wù)。
  • remote
    RPC服務(wù)器的最上層是遠(yuǎn)程服務(wù)層,即提供遠(yuǎn)程服務(wù)業(yè)務(wù)邏輯的地方,由Pomelo框架自動(dòng)加載remote代碼來(lái)完成。

RPC服務(wù)器結(jié)構(gòu)中需要注意的是大部分的Mailbox序列化使用的是JSON.stringify,當(dāng)消息量過(guò)大時(shí)比如使用bufferMsg時(shí)會(huì)導(dǎo)致序列化時(shí)間過(guò)長(zhǎng),最終可能因?yàn)榘蟀l(fā)送失敗。同時(shí)由于超時(shí)等待是需要計(jì)算序列化時(shí)間的,因此超時(shí)可能在消息較大的情況下直接就在本地發(fā)生。因此內(nèi)存存在泄露并且超時(shí)消息會(huì)無(wú)法取消。

RPC服務(wù)器啟動(dòng)流程

RPC服務(wù)器啟動(dòng)流程

getRemotePath()中主要完成設(shè)置sys命名空間與user命名空間的處理模塊路徑,傳入rpc-server以加載對(duì)應(yīng)的rpc服務(wù)處理邏輯。

RPC服務(wù)器部分是如何運(yùn)行的呢?

pomelo中connector服務(wù)器的配置信息中

{"id":"connector-server-1", "host":"127.0.0.1", "port":4050, "clientPort": 3050, "frontend": true}
  • frontend
    表示當(dāng)前connector服務(wù)器是否會(huì)接收客戶(hù)端的連接,若沒(méi)有該屬性則表示該connector服務(wù)器不會(huì)與客戶(hù)端進(jìn)行直接連接。
  • clientPort
    表示用于接收客戶(hù)端連接的端口
  • port
    表示pomelo內(nèi)部服務(wù)器之間的連接端口,也就是用于rpc的。若配置了port屬性,pomelo在啟動(dòng)服務(wù)器時(shí)會(huì)加載remote組件用于rpc的監(jiān)聽(tīng)。

創(chuàng)建remote組件

//創(chuàng)建rpc server用于接收其它服務(wù)器遠(yuǎn)程調(diào)用的消息并執(zhí)行相應(yīng)的方法
module.exports = function(app, opts){
  opts = opts || {};
  opts.cacheMsg = opts.cacheMsg || false;//是否緩存消息
  opts.interval = opts.interval || 30;
  return new Remote(app, opts);//創(chuàng)建remote組件
}

remote組件的運(yùn)行

pro.start = function(cb){
  this.opts.port = this.app.getCurServer().port;//獲取用于接收遠(yuǎn)程服務(wù)器連接的端口進(jìn)行RPC
  this.remote = genRemote(this.app, this.opts);//創(chuàng)建remote服務(wù)器用于接收遠(yuǎn)程調(diào)用信息,這里其實(shí)是getway。
  this.remote.start();//啟動(dòng)rpc server 
  process.nextTick(cb);
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 翻譯:死月,校對(duì):葉岬,原文連接 Pomelo(柚子)是一個(gè)可以讓開(kāi)發(fā)者便捷開(kāi)發(fā)的游戲服務(wù)端框架。下面是其一些Po...
    機(jī)巧死月不會(huì)碼代碼閱讀 19,672評(píng)論 4 16
  • 游戲服務(wù)器往往需要處理大量任務(wù),比如管理客戶(hù)端的連接、維護(hù)游戲世界的狀態(tài)、執(zhí)行游戲的邏輯等。每一項(xiàng)任務(wù)所需的系統(tǒng)資...
    JunChow520閱讀 598評(píng)論 0 1
  • 基于Pomelo開(kāi)發(fā)的聊天室應(yīng)用天生是多進(jìn)程的,因此可以非常容易地?cái)U(kuò)展服務(wù)器類(lèi)型和數(shù)量。對(duì)于多頻道聊天室,在Pom...
    JunChow520閱讀 1,461評(píng)論 0 1
  • 1 介紹 遠(yuǎn)程過(guò)程調(diào)用似乎是一種有用的范式,用于在以高級(jí)語(yǔ)言編寫(xiě)的程序之間提供跨網(wǎng)絡(luò)的通信。本文描述一個(gè)提供了遠(yuǎn)程...
    renwujie閱讀 4,508評(píng)論 0 6
  • 學(xué)習(xí)理解一個(gè)新的語(yǔ)言,除了其語(yǔ)法、書(shū)寫(xiě)規(guī)范需要了解之外,也可以從其對(duì)象、列表數(shù)組、map、方法、類(lèi)等角度先去了解其...
    AnonyPer閱讀 7,685評(píng)論 0 3

友情鏈接更多精彩內(nèi)容