RabbitMQ提供了消息队列中间件能提供的所有基本语义,比如消息的ack机制和confirm机制、qos保证、各种pattern的支持、权限控制、集群、高可用、甚至是现成的图形化监控等等。接下来的讨论会尽量不涉及RabbitMQ具体细节,把它当做一个通常的消息队列中间件来集成到我们目前为止形成的游戏服务端之中。当然,Gate经过扩展之后也能替代MQ,但是这样就失去了其作为Gate的意义——Gate更多的是用在性能敏感的场合,比如移动同步,协议太重是没有必要的。而且,重新造个MQ的轮子,说实话意义真的不大。
MQ解决了什么问题?MQ与Gate的定位类似,都是整个生态中的static parts。但是MQ与Gate是两种不同的基础设施抽象,提供的语义也不尽相同。
这样,我们的服务端中就出现了两个static parts——一个是Gate,一个是MQ。Gate与MQ是两个完全无关的基础设施,这两部分先于其他所有dynamic parts启动、构建。场景服务连接Gate与MQ(前提是确实有其他服务会与场景服务进行通信),聊天服务连接MQ,client连接Gate与MQ。
引入MQ之后的拓扑:
再脑补一下,增加N个节点,就形成了Gate和MQ的双中心,网络拓扑优雅了很多。
就具体实现来说,之所以选择RabbitMQ,还因为其对mqtt协议支持的比较好,官网上就有插件下载。mqtt协议可以参考这里。client走mqtt协议跟MQ通信还是比较轻量级的。
引入新的问题现在,client或者服务都需要通过不同的协议(Gate、MQ)与其他部分通信。
这样对应用层开发者来说就是一个负担。
ps. Gate的私有协议和mqtt、amqp协议下面会统称为消息路由协议。
而且不同的API的调用模型都不一样,因此我们需要一种应用层统一的调用规范。
3.3 游戏服务端中的RPC与Pattern
3.3.1 RPC
定义问题
整理一下现状。
目前,client可以发送两类消息:一类交由Gate路由,一类交由MQ路由。service也可以接收两类消息:一类由Gate路由过来,一类由MQ路由过来。
我们希望的是,应用层只需要关心服务,也就是说发送的消息是希望转到哪个服务上,以及接收的消息是请求自己提供的哪个服务。
这样对于应用层来说,其看到的协议应该是统一的,而至于应用层协议的底层协议是Gate的协议还是MQ的协议,由具体的适配器(Adaptor)适配。
这个应用层的协议就是RPC的一部分。
RPC一直都是很有争议的。一方面,它能让代码看起来更优雅,省了不少打解包的重复代码;另一方面,程序员能调RPC了,系统就变得很不可控,特别是像某些架构下面RPC底层会绕很多,最后用的时候完全违背设计本意。
但是总的来说,RPC的优势还是比较明显的,毕竟游戏服务端的整体服务定义都是同一个项目组内做的,副作用严格可控,很少会出现调用一条RPC要绕很多个节点的情况。
RPC解决什么样的问题?RPC的定位是具体消息路由协议与应用层函数调用的中间层。一个标准的RPC框架要解决两个问题:
RPC的协议定义也可以做个划分:
RPC调用规范的核心设计意图就是让应用层程序员调用起来非常自然、不需要有太多包袱(类bigworld架构的rpc设计通常也是这个原则,尽量让应用层不关注切进程的细节)。调用规范的具体细节就跟语言和平台相关了。在支持异步语法的语言/平台,可以原生集成异步等待、执行完恢复上下文继续执行的语义。在不支持异步语法的语言/平台,那就只能callback。如果是不支持将函数作为参数传递的语言/平台,我想你应该已经离现代游戏开发太远了。
通用的部分确定之后,还得解决特定于具体路由方式的、需要适配的部分。
我将这部分逻辑称为Adaptor,很好理解,就是RPC到具体消息路由协议、具体消息路由协议到RPC的适配器。
下面,结合一种具体的RPC实现方式(下文称为Phial规范),来探讨下如何将上面提出的这几个概念串起来。
先通过一个大概的流程来厘清一次RPC流程中涉及的所有角色。
RPC既然作为一次远程过程调用,那么,对于调用方来说,其调用的是一个跟普通函数很像的函数(有可能表现为一个异步函数,也有可能表现为一个同步函数);对于被调用方来说,其被调用的就真的是自己的一个函数了。
整个的pipeline也很清晰: