游戏开发中,这种组件的名字也比较通用,通常叫Gate。
Gate解决了什么问题仅就这两点而言,Gate已经能够解决上一节末提出的需求。做法就是client给消息加head,其中的标记可以供Gate识别,然后将消息路由到对应的backend上。比如公会相关的消息,Gate会路由到全局进程;场景相关的消息,Gate会路由到订阅该client的场景进程。同时,玩家要切场景的时候,可以由特定的backend(比如同样由全局进程负责)调度,让不同的场景进程向Gate申请修改对client场景相关消息的订阅关系,以实现将玩家的entity从场景进程A切到场景进程B。
站在比需求更高的层次来看Gate的意义的话,我们发现,现在clients不需要关注backends的细节,backends也不需要关注clients的细节,Gate成为这一pipeline中唯一的静态部分(static part)。
当然,Gate能解决的还不止这些。
我们考虑场景进程最常见的一种需求。玩家的移动在多client同步。具体的流程就是,client上来一个请求移动包,路由到场景进程后进行一些检查、处理,再推送一份数据给该玩家及附近所有玩家对应的clients。
如果按之前说的,这个backend就得推送N份一样的数据到Gate,Gate再分别转给对应的clients。
这时,就出现了对组播(multicast)的需求。
组播是一种通用的message pattern,同样也是发布订阅模型的一种实现方式。就目前的需求来说,我们只需要为client维护组的概念,而不需要做inter-backend组播。
这样,backend需要给多clients推送同样的数据时,只需要推送一份给Gate,Gate再自己dup就可以了——尽管带来的好处有限,但是还是能够一定程度降低内网流量。
那接下来就介绍一种Gate的实现。
我们目前所得出的Gate模型其实包括两个组件:
Gate的工作流程就是,listen两个端口,一个接受外网clients连接,一个接受内网backends连接。
Gate有自己的协议,该协议基于Network的len+data协议之上构建。
clients的协议处理组件与backends的协议处理组件不同,前者只处理部分协议(不会识别组控制相关协议,订阅协议)。
在具体的实现细节上,判断一个client消息应该路由到哪个backend,需要至少两个信息:一个是clientId,一个是key。
同一个clientId的消息有可能会路由到不同的backend上。
当然,Gate的协议设计可以自由发挥,将clientId+key组成一个routingKey也是可以的。
引入Gate之后的拓扑:
具体代码请参考:GateSharp
引入新的问题现在我们在需求的金字塔上更上了一层。之前我们是担心玩家数量增长会导致服务端进程爆掉,现在我们已经可以随意扩容backend进程,我们还可以通过额外实现的全局协调者进程来实现Gate的多开与动态扩容。甚至,我们可以通过构建额外的中间层,来实现服务端进程负载动态伸缩,比如像bigworld那样,在场景进程与Gate之间再隔离出一层玩家agent层。
可以说,在这种方案成熟之后,程序员之间开始流行“游戏开发技术封闭”这种说法了。
为什么?
举一个简单的例子,大概描述下现在一个游戏项目的服务端生命周期状况:
结果就是,产出了几个玩具水平的服务器进程。要非得说是工业级或者生产环境级别的吧,也算是,毕竟bugfix的代码的体量是玩具项目比不了的。而且,为了更好地bugfix,通常会引入lua或者python,然后游戏逻辑全盘由脚本构建,这下更方便bugfix了,还是hotfix的,那开发期就更能随便写写写了,你说架构是什么东西?
至于具体拓扑,可以对着下图脑补一下,增加N个节点,N个节点之间互相连接。
玩具水平的项目再修修补补,也永远不会变成工艺品。
skynet别的不说,至少实现了一套轻量级的actor model,做服务分离更自然,服务间的拓扑一目了然,连接拓扑更是优雅。网易的mobile_server,说实话我真的看不出跟bigworld早期版本有什么区别,连接拓扑一塌糊涂,完全没有服务的概念,手游时代了强推这种架构,即使成了几款过亿流水又怎样?
大网易的游戏开发应届生招聘要求精通分布式系统设计,就mobile_server写出来的玩具也好意思说是“分布式系统”?
很多游戏服务端程序员,在游戏服务端开发生涯结束之前,其接触的,或者能接受的设计基本到此为止。如果是纯MMO手游,这样做没什么,毕竟十几年都这样过来了,开发成本更重要。更搞笑的是社交游戏、异步战斗的卡牌游戏也用mobile_server,真搞不明白怎么想的。
大部分游戏服务端实现中,服务器进程是原子单位。进程与进程之间的消息流建立的成本很低,结果就是服务端中很多进程互相之间形成了O(n^2)的连接数量。
这样的话会有什么问题?
一方面,连接拓扑关系很复杂。一种治标不治本的方法是抬高添加新进程的成本,比如如非必要上面不会允许你增加额外进程,这样更深度的解耦合就成了幻想。