Rpc 调用原理
在这部分,我们将讨论关于 rpc 调用相关的问题。在 pomelo 中 rpc 的调用主要是通过 proxy 组件和 remote 组件实现,其中 proxy 组件主要负责创建 rpc 客户端代理,让开发者在 pomelo 中更方便地进行 rpc 调用;remote 组件主要负责加载 rpc 服务,包括系统的 rpc 服务和用户的 rpc 服务。Pomelo 的 rpc 框架主要解决了两个问题,第一个就是进程间的路由策略,第二个则是 rpc 底层的通信协议的选择。对于第一个问题,pomelo 提供了一套灵活的路由机制,并允许开发者根据需要自由地控制路由信息;对于第二个问题,pomelo 现在支持基于 socket.io 的通信机制和基于原生 socket 的通信机制。下面我们就分别介绍 rpc 客户端和服务端的具体实现。
RPC客户端
rpc 客户端主要负责产生代理对象,加载路由策略和进行消息的转发。
proxy 组件
在进行 pomelo 开发的过程中,进行 rpc 调用的代码如下:app.rpc.chat.chatRemote.add(session, uid, serverId, param, cb);
在 pomelo 中之所以能够如此简洁地进行 rpc 调用是因为 javascript 的语言特性和 pomelo 底层对 rpc 客户端进行的封装。proxy 组件在启动时首先会生成一个 rpc client,同时监听系统中服务器增加、服务器移除、服务器替换事件;当这些事件被触发时,proxy 组件会根据相应的事件信息对服务器代理对象进行相应的动态变化。例如,当有新的服务器增加时,proxy 组件会增加该服务器的代理对象;当有服务器被移除后,proxy 组件会移除该服务器的代理对象。在 proxy 组件启动完成时会将 rpc client 生成的代理对象挂载到 app.rpc 下,这样开发者在进行 rpc 调用时就可以匹配到对应的代理对象,从而通过 rpc client 进行相应的 rpc 调用。
RPC client
对于rpc client,其整体架构图如下所示:
在最底层,使用 mail box 的抽象隐藏了底层通讯协议的细节。一个 mail box 对应一个远程服务器的连接。Mail box 对上提供了统一的接口,如:连接,发送,关闭等。Mail box 内部则可以提供不同的实现,包括底层的传输协议,消息缓冲队列,传输数据的包装等。开发者可以根据实际需要,实现不同的 mail box,来满足不同的底层协议的需求。现在 pomelo 提供基于 socket.io 的 mail box 和基于原生 socket 的 mail box,默认使用 socket.io。
在 mail box 上面,是 mail station 层,负责管理底层所有 mail box 实例的创建和销毁,以及对上层提供统一的消息分发接口。上层代码只要传递一个目标 mail box 的 id,mail station 则可以知道如何通过底层相应的 mail box 实例将这个消息发送出去。开发者可以给 mail station 传递一个 mail box 的工厂方法,即可以定制底层的 mail box 实例的创建过程了,比如:连接到某个服务器,使用某一类型的 mail box,而其他的服务器,则使用另外一个类型的 mail box。
再往上的是路由层。路由层的主要工作就是提供消息路由的算法。路由函数是可以从外面定制的,开发者通过注入自定义的路由函数来实现自己的路由策略。每个 rpc 消息分发前,都会调用路由函数进行路由计算。容器会提供与该 rpc 相关的玩家会话对象(当中包含了该玩家当前的状态)和 rpc 的描述消息(包含了 rpc 的具体信息),通过这两个对象,即可做出路由的决策。路由的结果是目标 mail box 的 id,然后传递给底下的 mail station 层即可。
最上面的是代理层,其主要作用是隐藏底层 rpc 调用的细节。Pomelo 会根据远程接口生成代理对象,上层代码调用远程对象就像调用本地对象一样。但这里对远程代理对象有两个约定的规则,即第一个参数必须是相关玩家的 session 对象,如果没有这么一个对象可以填充 null,在路由函数中需做特殊处理。还有就是最后一个参数是 rpc 调用结果的回调函数,调用的错误或是结果全部通过该回调函数返回,且这个参数不能省略。而在远程服务的提供端,方法的声明与代理端的声明相比,除了不需要第一个 session 参数,其余的参数是一样的。
rpc 请求流程
对于发送 rpc 请求,rpc 客户端采用了一种懒加载的机制,其主要实现思路是客户端与服务端的连接并不是在服务器启动后就创建,而是当客户端第一次向服务端发起 rpc 请求时才真正建立连接。当客户端与相应的服务端建立连接后,以后有从该客户端到对应服务端的请求就无需再建立连接,消息可以直接发送。消息的发送过程类似前面介绍的 handler-filter 链处理模式,同样在 rpc 请求过程开发者可以添加 before 和 after filter 对消息进行相应的处理,现在 pomelo 内建的 rpc filter 包括 rpcLog 和 toobusy。RPC服务端
rpc 服务端主要负责接收客户端的rpc请求后将相应的消息转给客户端请求的 rpc 服务中,同时将 rpc 服务处理完成的消息返回给 rpc 客户端。
remote 组件
remote 组件在启动时会创建一个 rpc server,同时加载系统中所有的 rpc 服务;remote 组件在关闭时会停止 rpc server 的所有服务。RPC server
对于rpc server,其整体架构图如下所示:
最底下的是 acceptor 层,主要负责网络监听,消息的接收和解析。Acceptor 层与 mail box 层相对应,可以看成是网络协议栈上同一层上的两端,即从 mail box 层传入的消息与 acceptor 层上传出的消息应该是同样的内容。所以这两端的实例必须一致,使用同样的底层传输协议,对传输的数据使用同样格式进行封装。在客户端替换了 mail box 的实现,则在服务提供端也必须替换成对应的 acceptor 实现。同 mail box 一样,pomelo 提供基于 socket.io 的 acceptor 和基于原生 socket 的 acceptor。
往上是 dispatch 层。该层主要完成的工作是根据 rpc 描述消息将请求分发给上层的远程服务。
最上层的是远程服务层,即提供远程服务业务逻辑的地方,由 pomelo 框架自动加载 remote 代码来完成。
需要注意的地方
大部分的 mailbox 序列化使用的是 JSON.stringfiy,当消息量过大时(使用 bufferMsg 时),会导致序列化过程更长,最终可能会发送失败(包太大)。同时由于超时等待时,是需要计算序列化时间的,所以这个超时有可能在消息量较大的情况下直接就在本地发生(内存存在泄漏,并且超时消息会没法取消)。
总结
在本部分,详细介绍了 rpc 客户端和服务端的通信机制,包括对 mail box、mail station、acceptor、gateway 的功能进行了阐述,同时分析了 pomelo 中 proxy 组件和 remote 组件的相关功能。