《游戏服务器端所完成的事》由会员分享,可在线阅读,更多相关《游戏服务器端所完成的事(11页珍藏版)》请在金锄头文库上搜索。
1、游戏服务端所完成的事情(一)从定义问题开始,简单直接地说,一套游戏服务端开发框架应该具有下面两种能力: 定义了 client 到 server、server 到 client、server 到 server 的消息 pipeline。 描述了游戏世界状态的维护方式。1 消息 pipeline1.1 经典消息 pipeline1.1.1 场景同步当讨论到游戏服务端的时候,我们首先想到的会是什么?要回答这个问题,我们需要从游戏服务端的需求起源说起。定义问题游戏对服务端的需求起源应该有两个: 第一种是单机游戏联网版,实现为主客机模式的话,主机部分可以看做服务端。 第二种是所有 mmo 的雏形 mud
2、,跟 webserver 比较类似,一个 host 服务多 clients,表现为 cs 架构。第一种需求长盛不衰,一方面是 console 游戏特别适合这一套,另一方面是最近几年手游起来了,碎片化的 PVE 玩法+ 开房间式同步 PVP 玩法也得到验证,毕竟 MMO 手游再怎么火也不可能改变手游时间碎片化的事实的,最近的皇家冲突也证明,手游不会再重走端游老路了。第二种需求就不用说了,网上大把例子可以参考。最典型的是假设有这样一块野地,上面很多玩家和怪,逻辑都在服务端驱动,好了,这类需求没其他额外的描述了。但是,解决方案毕竟是不断发展的,即使速度很慢。说不断发展是特指针对第一种需求的解决方案,
3、发展原因就是国情,外挂太多。像 war3 这种都还是纯正的主客机,但是后来对战平台出现、发展,逐渐过渡成了 cs 架构。真正的主机 其实是建在服务器的,这样其实服务器这边也维护了房间状态。后来的一系列 ARPG 端游也都是这个趋势,服务端越来越重,逐渐变得与第二种模式没什么区别。 同理如现在的各种 ARPG 手游。说发展速度很慢特指针对第二种需求的解决方案,慢的原因也比较有意思,那就是 wow 成了不可逾越的鸿沟。bigworld 在 wow 用之前名不见经传,wow 用了之后国内厂商也跟进。发展了这么多年,现在的无缝世界服务端跟当年的无缝世界服务端并无二致。发展慢的原因就观察来说可能需求本身
4、就不是特别明确,MMO 核心用户是重社交的,无缝世界核心用户是重体验的。前者跑去玩了天龙八部和倩女不干了,说这俩既轻松又妹子多;后者玩了 console 游戏也不干了,搞了半天 MMO 无缝世界是让我更好地刷刷刷的。所以仔细想想,这么多年了,能数得上的无缝世界游戏除了天下就是剑网,收入跟重社交的那几款完全不在一个量级。两种需求起源,最终其实导向了同一种业务需求。传统 MMO 架构(就是之前说的天龙、倩女类架构),一个进程维护多个场景,每个场景里多个玩家,额外的中心进程负责帮玩家从一个场景/进程切到另一个场景/进程。bigworld 架构,如果剥离开其围绕切进程所做的一些外围设施,核心工作流程基
5、本就能用这一段话描述。抽象一下问题,那我们谈到游戏服务端首先想到的就应该是多玩家对同一场景的 view 同步,也就是场景服务。本节不会讨论帧同步或是状态同步这种比较上层的问题,我们将重点放在数据流上。如何实现场景同步?首先,我们看手边工具,socket。之所以不提 TCP 或 UDP 是因为要不要用 UDP 自己实现一套 TCP 是另一个待撕话题,这篇文章不做讨论。因此,我们假设,后续的实现是建立在对底层协议一无所知的前提之上的,这样设计的时候只要适配各种协议,到时候就能按需切换。socket 大家都很熟悉,优点就是各操作系统上抽象统一。因此,之前的问题可以规约为:如何用 socket 实现场
6、景同步?拓扑结构是这样的(之后的所有图片连接箭头的意思表示箭头指向的对于箭头起源的来说是静态的):场景同步有两个需求: low latency rich interaction要做到前者,最理想的情况就是由游戏程序员把控消息流的整套 pipeline,换句话说,就是不借助第三方的消息库/连接库。当然,例外是你对某些第三方连接库特别熟悉,比如很多 C+服务端库喜欢用的 libevent,或者我在本篇文章提供的示例代码所依赖的,mono 中的 IO 模块。要做到后者,就需要保持场景同步逻辑的简化,也就是说,场景逻辑最好是单线程的,并且跟IO 无关。其核心入口就是一个主循环,依次更新场景中的所有 e
7、ntity,刷新状态,并通知 client。正是由于这两个需求的存在,网络库的概念就出现了。网络库由于易于实现,概念简单,而且笼罩着“底层”光环,所以如果除去玩具性质的项目之外,网络库应该是程序员造过最多的轮子之一。那么,网络库解决了什么问题?抛开多项目代码复用不谈,网络库首先解决的一点就是,将传输层的协议(stream-based 的TCP 协议或 packet-based 的 UDP 协议)转换为应用层的消息协议(通常是 packet-based)。对于业务层来说,接收到流和包的处理模型是完全不同的。对于业务逻辑狗来说,包显然是处理起来更直观的。流转包的方法很多,最简单的可伸缩的 non-
8、trivial buffer,ringbuffer ,bufferlist,不同的结构适用于不同的需求,有的方便做 zero-copy,有的方便做无锁,有的纯粹图个省事。因为如果没有个具体的 testcast 或者 benchmark,谁比谁一定好都说不准。buffer 需要提供的语义也很简单,无非就是 add、remove。buffer 是只服务于网络库的。网络库要解决的第二个问题是,为应用层建立 IO 模型。由于之前提到过的场景服务的 rich interaction 的特点,poll 模型可以避免大量共享状态的存在,理论上应该是最合适场景服务的。所谓 poll,就是 IO 线程准备好数据
9、放在消息队列中,用户线程负责轮询 poll,这样,应用层的回调就是由用户线程进入的,保证模型简单。而至于 IO 线程是如何准备数据的,平台不同做法不同。linux 上最合适的做法是reactor,win 最合适的做法就是 proactor,一个例外是 mono,mono 跑在 linux 平台上的时候虽然 IO 库是 reactor 模型,但是在 C#层面还是表现为 proactor 模型。提供统一 poll 语义的网络库可以隐藏这种平台差异,让应用层看起来就是统一的本线程 poll,本线程回调。网络库要解决的第三个问题是,封装具体的连接细节。cs 架构中一方是 client 一方是serve
10、r,因此连接细节在两侧是不一样的。而由于 socket 是全双工的,因此之前所说的 IO 模型对于任意一侧都是适用的。连接细节的不同就体现在,client 侧,核心需求是发起建立连接,外围需求是重连;server 侧,核心需求是接受连接,外围需求是主动断开连接。而两边等到连接建立好,都可以基于这个连接构建同样的 IO 模型就可以了。现在,简单介绍一种网络库实现。 一个连接好的 socket 对应一个 connector。 connector 负责向上提供 IO 模型抽象(poll 语义) 。同时,其借助维护的一个connector_buffer,来实现流转包。 网络库中的 client 部分主
11、要组件是 ClientNetwork,维护连接(与重连)与一条connector。 网络库中的 server 部分主要组件是 ServerNetwork,维护接受连接(与主动断开)与 N条 connector。 Network 层面的协议非常简单,就是 len+data。具体代码不再在博客里贴了。请参考:Network引入新的问题如果类比马斯洛需求中的层次,有了网络库,我们只能算是解决了生理需求:可以联网。但是后面还有一系列的复杂问题。最先碰到的问题就是,玩家数量增加,一个进程扛不住了。那么就需要多个进程,每个进程服务一定数量的玩家。但是,给定任意两个玩家,他们总有可能有交互的需求。对于交互需
12、求,比较直观的解决方案是,让两个玩家在各自的进程中跨进程交互。但是这就成了一个分布式一致性问题两个进程中两个玩家的状态需要保持一致。至于为什么一开始没人这样做,我只能理解为,游戏程序员的计算机科学素养中位程度应该解决不了这么复杂的问题。因此比较流行的是一种简单一些的方案。场景交互的话,就限定两个玩家必须在同一场景(进程),比如攻击。其他交互的话,就借助第三方的协调者来做,比如公会相关的通常会走一个全局服务器等等。这样,服务端就由之前的单场景进程变为了多场景进程+协调进程。新的问题出现了:玩家需要与服务端保持多少条连接?一种方法是保持 O(n)条连接,既不环保,扩展性又差,可以直接 pass 掉
13、。那么就只能保持 O(1)条连接,如此的话,如何确定玩家正与哪个服务端进程通信?要解决这个问题,我们只能引入新的抽象。 1.1.2 Gate定义问题整理下我们的需求: 玩家在服务端的 entity 可以在不同的进程中,也可以移动到同一个进程中。 玩家只需要与服务端建立有限条连接,即有访问到任意服务端进程的可能性。同时,这个连接数量不会随服务端进程数量增长而线性增长。要解决这些需求,我们需要引入一种反向代理(reverse proxy)中间件。反向代理是服务端开发中的一种常见基础设施抽象(infrastructure abstraction),概念很简单,简单说就是内网进程不是借助这种 prox
14、y 访问外部,而是被动地挂在 proxy 上,等外部通过这种 proxy 访问内部。更具体地说,反向代理就是这样一种 server:它接受 clients 连接,并且会将 client 的上行包转发给后端具体的服务端进程。很多年前 linux 刚支持 epoll 的时候,流行一个 c10k 的概念,解决 c10k 问题的核心就是借助性能不错的反向代理中间件。游戏开发中,这种组件的名字也比较通用,通常叫 Gate。Gate 解决了什么问题 首先,Gate 作为 server,可以接受 clients 的连接。这里就可以直接用我们上一节输出的网络库。同时,其可以接受服务端进程(之后简称 backe
15、nd)的连接,保持通信。 其次,Gate 能够将 clients 的消息转发到对应的 backend。与此对应的,backend 可以向 Gate 订阅自己关注的 client 消息。对于场景服务来说,这里可以增加一个约束条件,那就是限制 client 的上行消息不会被 dup,只会导到一个 backend 上。仅就这两点而言,Gate 已经能够解决上一节末提出的需求。做法就是 client 给消息加 head,其中的标记可以供 Gate 识别,然后将消息路由到对应的 backend 上。比如公会相关的消息,Gate 会路由到全局进程;场景相关的消息,Gate 会路由到订阅该 client 的
16、场景进程。同时,玩家要切场景的时候,可以由特定的 backend(比如同样由全局进程负责)调度,让不同的场景进程向Gate 申请修改对 client 场景相关消息的订阅关系,以实现将玩家的 entity 从场景进程 A 切到场景进程 B。站在比需求更高的层次来看 Gate 的意义的话,我们发现,现在 clients 不需要关注 backends的细节,backends 也不需要关注 clients 的细节,Gate 成为这一 pipeline 中唯一的静态部分(static part)当然,Gate 能解决的还不止这些。我们考虑场景进程最常见的一种需求。玩家的移动在多 client 同步。具体的流程就是,client上来一个请求移动包,路由到场景进程后进行一些检查、处理,再推送一份数据给该玩家及附近所有玩家对应的 clients。如果按之前说的,这个 backend 就得推送 N 份一样的数据到 Gate,Gate 再分别转给对应的clients。这时,就出现了对组播(mult