Pomelo中游戏服务器是一个多进程相互协作的环境,各个进程之间通信采用RPC(远程过程调用)的形式完成,通过底层统一的RPC框架来实现,服务器间的RPC调用实现了零配置。游戏服务器进程的扩展以达到支撑较多的用户,降低服务器压力等要求。
RPC框架主要解决了“进程间消息的路由”和“RPC底层通讯协议的选择”两个问题
进程间的路由策略
Pomelo提供了一套灵活的路由机制,并允许开发者根据需要自由地控制了路由信息。RPC底层的通信协议选择
Pomelo现在支持具有socket.io的通信机制和基于原生socket的通信机制。RPC框架目前在底层采用socket.io作为通讯协议,协议对上层是透明的,可替换为任意的协议。
pomelo服务器之间通信采用rpc调用方式,依赖pomelo-rpc项目,pomelo-rpc项目使用stream-pkg
库序列化rpc通信数据,可选择使用wsSocket或tcpSocket连接模式。pomelo-rpc是pomelo项目底层的rpc框架,提供了一个多服务器进程间进行rpc调用的基础设施,pomelo-rpc分为客户端和服务端两部分。客户端提供rpc代理生成、消息路由、网路通讯等功能,并支持动态添加代理和远程服务器配置。服务端提供远程服务暴露、请求派发、网路通讯等功能。
命名空间
pomelo内部rpc协议格式
msg{
route: nameSpace.serverType.module.method//路由信息
args:[]//调用参数
}
nameSpace命名空间有两种分别是系统sys
和用户user
命名空间 | 描述 |
---|---|
sys | 系统命名空间 |
user | 用户命名空间 |
- 系统命名空间
sys
系统命名空间对应app.sysRpc
方法及app.rpcInvoke
方法,pomelo系统内部处理的rpc事件需经过common.remote
包下的系统rpc服务处理。
- 用户命名空间
user
用户命名空间对应app.rpc
方法,是由服务器内部组件发起的rpc调用,无需经过common.remote
包下的系统rpc服务处理,最终逻辑由app.servers.serverType.remote.module.method
执行。
命名空间的分类方法从实现角度来说
- 系统命名空间
sys
系统提供了一些辅助服务比如sessionRemote和channelRemote,突显connector负责为所有后端服务广播消息给客户端,从而起到隔离后端服务器的作用。
系统实现基于长连接模式比如wsSocket下的C/S架构的request/response通信框架,比如msgRemote的forwardMessage消息转发服务。
根据这两大类的基础功能,可将系统命名空间内的rpc调用理解为客户端与服务器之间的基础通信框架,因此pomelo将其实现放在common.remote包下。
- 用户命名空间
user
用户命名空间pomelo未作特别处理,pomelo-server库直接将rpc调用分派到对应由pomelo使用者开发的后端服务器rpc接口上,所以这个命名空间称为user。
与sys rpc相关的remote包括三种
- sessionRemote
sessionRemote仅在connector前端服务器中提供,以供后端服务器将其本地的session同步到前端服务器,使用bind或push之类的方法。
- channelRemote
channelRemote仅在connector前端服务器中提供,实现广播消息,即后端要广播消息时可调用此rpc服务。
- msgRemote
msgRemote仅在后端服务器中提供,通过forwardMessage方法委托server组件按路由信息serverType.module.method
分派消息到对应的handler去处理。
application对象中三个关于rpc方法
-
app.sysRpc()
表示命名空间为sys的rpc调用封装,app.sysRpc中的service对应app.rpc参数中的module。 -
app.rpcInvote()
表示直接的rpc调用,与pomelo-rpc/client中的rpc调用方法一致, 需指定所有调用参数,包括context、serverId、namespace、service、method、args。 -
app.rpc()
表示命名空间为user的rpc调用封装
RPC调用类型
Pomelo中使用RPC调用进行进程间通信,在Pomelo中RPC调用分为两类,使用namespace
命令空间进行区分。
命名空间 | 调用类型 | 描述 |
---|---|---|
sys | sys rpc | Handler |
user | user rpc | Remote |
- 系统远程调用
sys rpc
- 一般调用
Handler
,比如客户端调用网关服务器gate.gateHandler.entry
。 -
namespace
为sys
的是系统RPC调用,它对用户来说是透明的。
Handler.prototype.add = function(session, msg, callback){}
Pomelo中系统RPC调用有三种
- 后端服务器向前端服务器请求Session会话信息
- 后端服务器通过channel频道推送消息时对前端服务器的RPC调用
- 前端服务器将用户请求route路由给后端服务器时也是
sys rpc
调用
除了系统RPC调用之外,其余的由用户自定义的RPC调用属于user namespace
的RPC调用,需要用户自己完成RPC服务端remote
的handler
代码,并由RPC客户端显式地发起调用。
- 用户远程调用
一般调用Remote
,比如app.rpc.chat.chatRemote.kick
。
例如:定义聊天服务器的remote服务器的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);
};
其它服务器作为rpc客户端只需要通过接口即可实现rpc调用
app.rpc.chatRemote.kick(session, uid, player, function(data){});
这个rpc调用会根据特定的路由规则转到特定的服务器,比如场景服务器的请求会根据玩家在哪个场景直接转发到对应的服务器。
RPC调用原理
rpc组件是用于服务器进程间通讯的一种实现,能够以类似函数调用的方式来实现进程间通讯,此组件基于pomelo-rpc实现,也是额外封装了一层以便于使用。
pomelo中rpc的调用主要是通过proxy
组件和remote
组件实现的,proxy
组件和remote
组件结合pomelo内部使用的rpc协议分别实现了rpc-server
和rpc-client
的功能封装。
组件 | rpc | 描述 |
---|---|---|
proxy | rpc-client | 负责创建rpc客户端代理(rpc client stub)、加载路由策略、消息转发 |
remote | rpc-server | 负责加载rpc服务(rpc server stub) |
rpc客户端 / proxy组件
proxy组件主要负责创建RPC客户端代理,让开发者在Pomelo中更方便地进行RPC调用。
proxy组件是一个重量级的组件,它被除master以外的所有服务器加载。
例如:用户自定义rpc调用
// app 表示pomelo应用对象
// app.rpc表示前后台服务器的Remote RPC调用
// chat表示服务器的名称
// chatRemote表示i对应的文件名称
// add表示对应的方法名
// 为了实现这个RPC调用需要在对应的chat/remote/chatRemote.js文件中实现add方法
app.rpc.chat.chatRemote.add(session, uid, app.get("serverId"), param, cb);
在Pomelo中之所以能够如此简洁地进行RPC调用是因为JavaScript语言特性和Pomelo底层对RPC客户端进行的封装。
proxy组件在启动时首先会生成一个rpc客户端,同时监听系统中服务器增加、服务器移除、服务器替换事件。当这些事件被触发时,proxy组件会根据相应的事件信息对服务器代理对象进行相应的动态变化。比如当有新的服务器增加时,proxy组件会增加该服务器的代理对象。当有服务器被移除后,proxy组件会移除该服务器的代理对象。
在proxy组件启动完成时会将rpc客户端生成的代理对象挂载到app.rpc
下,这样开发者在进行rpc调用时就可以匹配到相应的代理对象,从而通过rpc客户端进行相应的rpc调用。
proxy组件会扫描具体应用服务器的目录,抽取其中的remote部分,由于JavaScript的动态性,可以轻易地得到remote中关于远程调用的元信息来生成存根stub,并将这些调用都挂到app.rpc
下。
当用户发起rpc调用时,proxy组件会查看器其扫描的存根stub信息以此决定此远程调用是否合法。同时proxy又会创建一个rpc客户端,当发起远程调用时,负责与远端的remote进行通信,并得到远程调用的结果供调用者使用。当进行远程调用时由于同类型的远程服务器可能有多个,所以这里同样需要配置相应的router。
配置proxy使用
app.set("proxyConfig", opts);
proxy的配置项
配置 | 描述 |
---|---|
cacheMsg | 配置cacheMsg为true则开启rpc调用时对消息的缓冲,而非直接有rpc请求就发出。 |
interval | 与配置cacheMsg配置使用,设置flush缓存的周期。 |
mailBoxFactory | rpc底层实现需要,用户可以定义自己的mailBoxFactory。 |
开启rpc调用日志
app.enable("rpcDebugLog");
rpc服务端 / remote组件
- remote组件主要负责加载RPC服务,包括系统的RPC服务和用户的RPC服务。
- remote组件在启动时会创建一个rpc服务器,同时加载系统中所有的rpc服务。
- remote组件在关闭时会停止rpc服务器的所有服务
后端服务器的remote用来给connector服务器提供rpc调用使用,比如玩家掉线,由于connector承载客户端连接,必然是connector服务器首先知道玩家掉线,因此掉线后connector服务器进行rpc调用chatRomte离开,进行后端服务器状态设置。
- remote模块是远程通讯模块和服务端监听模块
- remote模块的作用是作为各个模块间通讯对象而存在
- remote模块使用rpc协议作为rpc服务端存在
- remote对象在
app.components._remote_
变量中保存
remote组件是与proxy组件对等的组件,remote组件用来提供rpc调用服务。rpc组件完成对当前服务器的remote的加载,并开启监听端口,等待rpc客户端的连接及相应的rpc调用。当接收到具体的调用请求时,会根据调用请求中描述的调用请求信息,调用相应的remote中的相应方法。然后再将具体的处理记过返回给rpc客户端。rpc服务器还支持对调用请求的filter,也就是说跟server组件处理客户端请求一样,rpc服务端处理具体请求时也会使用filter-remote链进行处理。
项目中Remote逻辑设置有两个地方,一个是系统调用sys
对应pomelo/lib/common/remote
下的backend
或frontend
,具体哪个路径决定了当前服务器是前端服务器还是后端服务器,即在servers.json中配置的frontend
选项是true或是false。另一个是用户调用user
,user
对应了app/servers/服务器类型名称/remote
目录下的JS文件。
{"id":"connector-server-1", "host":"127.0.0.1", "port":4050, "clientPort": 3050, "frontend": true},
remote组件创建
//于创建rpc server的,用于接收别的服务器的远程调用的消息,并执行相应的方法
module.exports = function(app, opts) {
opts = opts || {};
opts.cacheMsg = opts.cacheMsg || false;
opts.interval = opts.interval || 30;
return new Remote(app, opts); //创建remote组件
};
/**
* Remote component class
*
* @param {Object} app current application context
* @param {Object} opts construct parameters
*/
//将一些数据保存
var Remote = function(app, opts) {
this.app = app;
this.opts = opts;
}
remote组件配置项
配置 | 描述 |
---|---|
cacheMsg | 与proxy组件的含义相同 |
interval | 与proxy组件的含义相同 |
acceptorFactory | rpc底层实现需要,可认为跟proxy配置中的mailBoxFactory是对等的。 |
配置remote组件使用
app.set("remoteConfig", opts);
开启rpcDebugLog来得到所有的rpc调用过程日志
app.enable("rpcDebugLog");
remote组件启动
真正进行创建的过程是在remote组件的启动中
pro.start = function(cb) {
this.opts.port = this.app.getCurServer().port; //用于接受远程服务器连接的端口进行rpc
this.remote = genRemote(this.app, this.opts); //创建remotes服务器,用接受远程调用的信息,其实这里是gateway
this.remote.start(); //启动rpc server
process.nextTick(cb);
};
remote组件启动时可分为两步,首先是创建pomelo自定义的rpc服务器,然后再启动它。
rpc服务器的创建 genRemote(app, opts)
//创建rpc服务器
var genRemote = function(app, opts) {
opts.paths = getRemotePaths(app); //执行远程rpc的源代码放的目录
opts.context = app;
return RemoteServer.create(opts);
};
rpc服务器创建时首先会获取执行远程调用的源代码文件路径用于加载。远程调用分为sys命名空间和user命名空间,用于执行远程调用也分为两部分。
远程服务器创建RemoteServer.create(opts)
//创建gateway,用于接收远程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); //创建远程服务执行的方法,这里也分为sys空间和user空间
opts.services = services; //保存刚刚创建的service
var gateway = Gateway.create(opts); //创建gateway
return gateway;
};
远程服务器的创建过程可分为两部分,首先是创建服务service,然后是创建gateway。
- service服务创建的过程是将路径下的源代码读取并将其定义的方法进行保存用于执行远程的调用。
- gateway用于监听端口,接收远程调用发送过来的信息。
加载远程服务loadRemoteServices(paths, context)
加载远程服务后,当执行远程调用时可直接使用索引相应的方法即可执行即可。
//将path下面的源文件读取进来,他们里面定义的方法将会用于执行远程的调用
var loadRemoteServices = function(paths, context) {
var res = {}, item, m;
for(var i=0, l=paths.length; i
创建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; //用于创建acceptor的工厂
this.services = opts.services;
var self = this;
this.acceptor = this.acceptorFactory.create(opts, function(msg, cb) { //创建acceptor,用于接受远程调用的信息
dispatcher.route(msg, self.services, cb);
});
};
util.inherits(Gateway, EventEmitter);
gateway创建中会创建acceptor,acceptor用于接收远程服务器发送过来的远程调用信息,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; //回调函数,用于处理接收到的rpc消息
};
util.inherits(Acceptor, EventEmitter);
acceptor创建的过程中需要注意的是需要传入一个回调函数,用于处理acceptor接收到的消息,回调函数的定义作用是调用dispatcher的route方法,根据接收到的消息中的namespace命名空间、服务service、method方法、args参数等参数信息,调用相应的函数来处理,然后将数据返回。
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]; //获取需要调用的发那个发
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); //用需要调用的方法来处理数据
};
RPC请求流程
对于发送rpc请求,RPC客户端采用了一种懒加载的机制,主要实现思想是客户端与服务端的连接并不是在服务器启动后就创建,而是当客户端第一次向服务器发起RPC请求时才真正建立连接。当客户端与相应的服务器建立连接后,以后有从该客户端到对应服务器的请求就无需再次建立连接,消息可以直接发送。消息的发送过程类似handler-filter链式处理模式,同样在RPC请求过程中开发者可以添加before和after filter对消息进行相应的处理,现在pomelo内建的rpc filter包括rpcLog和toobusy。
- 客户端请求的RPC调用事件处理流程
serverType.module.method
- 服务器发起的RPC调用事件的处理流程
RPC客户端
RPC客户端主要负责产生代理对象、加载路由策略、进行消息转发
- 数据收发模块 mail box
一个RPC客户端可能同时需要调用多个远端服务器提供的服务,在pomelo中每个server都被抽象为了一个mail box。
在RPC客户端的整体架构中,最底层是使用Mailbox的抽象隐藏了底层通讯协议的细节,一个Mailbox对应一个远程服务器的连接。
Mailbox对上提供了统一的接口,比如连接、发送、关闭等。
Mailbox内部则可以提供不同的实现,包括底层的传输协议、消息缓冲队列、传输数据的包装等。
开发者可以根据实际需要实现不同的Mailbox来满足不同的底层协议的需求。现在Pomelo提供了基于socket.io的Mailbox和基于原生socket的Mailbox,默认使用socket.io。
- mail station
mail station主要实现的功能:客户端状态控制、远程服务器信息管理、过滤器、消息路由
在Mailbox上面是MailStation层,负责管理底层所有Mailbox实例的创建和销毁以及对上层提供统一的消息分发接口。
上层代码只要传递一个目标Mailbox的ID,MailStation则可以知道如何通过底层相应的MailBox实例将这个消息发送出去。
开发者可以给MailStation传递一个MailBox的工厂方法即可以定制底层的MailBox实例的创建过程。比如连接到某个服务器,使用某类型的MailBox,而其它服务器则使用另外一个类型的MailBox。
- route
再往上是路由层,路由层主要工作是提供消息路由的算法。
路由函数是可以从外面定制的,开发者可以通过注入自定义的路由函数来实现自己的路由策略。
每个rpc消息分发前,都会调用路由函数进行路由计算。
容器会提供与该rpc相关的玩家会话对象(其中包含了该玩家当前的状态)和rpc的描述消息(其中包含了rpc的具体信息)。通过这两个对象,即可做出路由的决策。
路由的结果是目标Mailbox的ID,然后传递给底层的MailStation层即可。
- proxy
最上层的是代理层,代理层主要作用是隐藏底层RPC调用的细节。
Pomelo会根据远程接口生成代理对象,上层代码调用远程对象就像调用本地对象一样。
对远程代理对象有两个约定的规则:
- 第一个参数必须是玩家的session对象,如果没有可以填充为null,再路由函数中需做特殊处理。
- 最后一个参数是rpc调用结果的回调函数,调用的错误或结果全部通过该回调函数返回,且这个参数不能够省略。
而在远程服务的提供端,方法的声明与代理端相比,除了不需要第一个session参数,其余的参数是一样的。
RPC服务器
RPC服务端主要负责接收客户端的RPC请求,再将相应的消息转给客户端请求的RPC服务中,同时将RPC服务处理完成的消息返回给RPC客户端。
- Acceptor
RPC服务器最底层是Acceptor层主要负责网络监听、消息的接收和解析。
Acceptor层与Mailbox层相应,可以看成是网络协议栈上同一层上的两端,即从Mailbox层传入的消息与Acceptor层上传出的消息应该是同样的内容。
所以这两端的实例必须一致,使用同样的底层传输协议,对传输的数据使用同样格式进行封装。
在客户端替换了Mailbox的实现,则在服务提供端也必须替换成对应的Acceptor实现。
同Mailbox一样,Pomelo提供基于socket.io和基于原生socket的Acceptor。 - diapatch
RPC服务器往上是diapatch层,该层主要完成的工作是根据RPC描述消息将请求转发给上层的远程服务。 - remote
RPC服务器的最上层是远程服务层,即提供远程服务业务逻辑的地方,由Pomelo框架自动加载remote代码来完成。
RPC服务器结构中需要注意的是大部分的Mailbox序列化使用的是JSON.stringify
,当消息量过大时比如使用bufferMsg时会导致序列化时间过长,最终可能因为包太大发送失败。同时由于超时等待是需要计算序列化时间的,因此超时可能在消息较大的情况下直接就在本地发生。因此内存存在泄露并且超时消息会无法取消。
RPC服务器启动流程
getRemotePath()中主要完成设置sys命名空间与user命名空间的处理模块路径,传入rpc-server以加载对应的rpc服务处理逻辑。
RPC服务器部分是如何运行的呢?
pomelo中connector服务器的配置信息中
{"id":"connector-server-1", "host":"127.0.0.1", "port":4050, "clientPort": 3050, "frontend": true}
- frontend
表示当前connector服务器是否会接收客户端的连接,若没有该属性则表示该connector服务器不会与客户端进行直接连接。 - clientPort
表示用于接收客户端连接的端口 - port
表示pomelo内部服务器之间的连接端口,也就是用于rpc的。若配置了port属性,pomelo在启动服务器时会加载remote组件用于rpc的监听。
创建remote组件
//创建rpc server用于接收其它服务器远程调用的消息并执行相应的方法
module.exports = function(app, opts){
opts = opts || {};
opts.cacheMsg = opts.cacheMsg || false;//是否缓存消息
opts.interval = opts.interval || 30;
return new Remote(app, opts);//创建remote组件
}
remote组件的运行
pro.start = function(cb){
this.opts.port = this.app.getCurServer().port;//获取用于接收远程服务器连接的端口进行RPC
this.remote = genRemote(this.app, this.opts);//创建remote服务器用于接收远程调用信息,这里其实是getway。
this.remote.start();//启动rpc server
process.nextTick(cb);
}