游戏服务器端所完成的事.docx
- 文档编号:25527637
- 上传时间:2023-06-09
- 格式:DOCX
- 页数:11
- 大小:72.77KB
游戏服务器端所完成的事.docx
《游戏服务器端所完成的事.docx》由会员分享,可在线阅读,更多相关《游戏服务器端所完成的事.docx(11页珍藏版)》请在冰豆网上搜索。
游戏服务器端所完成的事
游戏服务端所完成的事情
(一)
从定义问题开始,简单直接地说,一套游戏服务端开发框架应该具有下面两种能力:
∙定义了client到server、server到client、server到server的消息pipeline。
∙描述了游戏世界状态的维护方式。
1消息pipeline
1.1经典消息pipeline
1.1.1场景同步
当讨论到游戏服务端的时候,我们首先想到的会是什么?
要回答这个问题,我们需要从游戏服务端的需求起源说起。
定义问题
游戏对服务端的需求起源应该有两个:
∙第一种是单机游戏联网版,实现为主客机模式的话,主机部分可以看做服务端。
∙第二种是所有mmo的雏形mud,跟webserver比较类似,一个host服务多clients,表现为cs架构。
第一种需求长盛不衰,一方面是console游戏特别适合这一套,另一方面是最近几年手游起来了,碎片化的PVE玩法+开房间式同步PVP玩法也得到验证,毕竟MMO手游再怎么火也不可能改变手游时间碎片化的事实的,最近的皇家冲突也证明,手游不会再重走端游老路了。
第二种需求就不用说了,网上大把例子可以参考。
最典型的是假设有这样一块野地,上面很多玩家和怪,逻辑都在服务端驱动,好了,这类需求没其他额外的描述了。
但是,解决方案毕竟是不断发展的,即使速度很慢。
说不断发展是特指针对第一种需求的解决方案,发展原因就是国情,外挂太多。
像war3这种都还是纯正的主客机,但是后来对战平台出现、发展,逐渐过渡成了cs架构。
真正的主机其实是建在服务器的,这样其实服务器这边也维护了房间状态。
后来的一系列ARPG端游也都是这个趋势,服务端越来越重,逐渐变得与第二种模式没什么区别。
同理如现在的各种ARPG手游。
说发展速度很慢特指针对第二种需求的解决方案,慢的原因也比较有意思,那就是wow成了不可逾越的鸿沟。
bigworld在wow用之前名不见经传,wow用了之后国内厂商也跟进。
发展了这么多年,现在的无缝世界服务端跟当年的无缝世界服务端并无二致。
发展慢的原因就观察来说可能需求本身就不是特别明确,MMO核心用户是重社交的,无缝世界核心用户是重体验的。
前者跑去玩了天龙八部和倩女不干了,说这俩既轻松又妹子多;后者玩了console游戏也不干了,搞了半天MMO无缝世界是让我更好地刷刷刷的。
所以仔细想想,这么多年了,能数得上的无缝世界游戏除了天下就是剑网,收入跟重社交的那几款完全不在一个量级。
两种需求起源,最终其实导向了同一种业务需求。
传统MMO架构(就是之前说的天龙、倩女类架构),一个进程维护多个场景,每个场景里多个玩家,额外的中心进程负责帮玩家从一个场景/进程切到另一个场景/进程。
bigworld架构,如果剥离开其围绕切进程所做的一些外围设施,核心工作流程基本就能用这一段话描述。
抽象一下问题,那我们谈到游戏服务端首先想到的就应该是多玩家对同一场景的view同步,也就是场景服务。
本节不会讨论帧同步或是状态同步这种比较上层的问题,我们将重点放在数据流上。
如何实现场景同步?
首先,我们看手边工具,socket。
之所以不提TCP或UDP是因为要不要用UDP自己实现一套TCP是另一个待撕话题,这篇文章不做讨论。
因此,我们假设,后续的实现是建立在对底层协议一无所知的前提之上的,这样设计的时候只要适配各种协议,到时候就能按需切换。
socket大家都很熟悉,优点就是各操作系统上抽象统一。
因此,之前的问题可以规约为:
如何用socket实现场景同步?
拓扑结构是这样的(之后的所有图片连接箭头的意思表示箭头指向的对于箭头起源的来说是静态的):
场景同步有两个需求:
∙lowlatency
∙richinteraction
要做到前者,最理想的情况就是由游戏程序员把控消息流的整套pipeline,换句话说,就是不借助第三方的消息库/连接库。
当然,例外是你对某些第三方连接库特别熟悉,比如很多C++服务端库喜欢用的libevent,或者我在本篇文章提供的示例代码所依赖的,mono中的IO模块。
要做到后者,就需要保持场景同步逻辑的简化,也就是说,场景逻辑最好是单线程的,并且跟IO无关。
其核心入口就是一个主循环,依次更新场景中的所有entity,刷新状态,并通知client。
正是由于这两个需求的存在,网络库的概念就出现了。
网络库由于易于实现,概念简单,而且笼罩着“底层”光环,所以如果除去玩具性质的项目之外,网络库应该是程序员造过最多的轮子之一。
那么,网络库解决了什么问题?
抛开多项目代码复用不谈,网络库首先解决的一点就是,将传输层的协议(stream-based的TCP协议或packet-based的UDP协议)转换为应用层的消息协议(通常是packet-based)。
对于业务层来说,接收到流和包的处理模型是完全不同的。
对于业务逻辑狗来说,包显然是处理起来更直观的。
流转包的方法很多,最简单的可伸缩的non-trivialbuffer,ringbuffer,bufferlist,不同的结构适用于不同的需求,有的方便做zero-copy,有的方便做无锁,有的纯粹图个省事。
因为如果没有个具体的testcast或者benchmark,谁比谁一定好都说不准。
buffer需要提供的语义也很简单,无非就是add、remove。
buffer是只服务于网络库的。
网络库要解决的第二个问题是,为应用层建立IO模型。
由于之前提到过的场景服务的richinteraction的特点,poll模型可以避免大量共享状态的存在,理论上应该是最合适场景服务的。
所谓poll,就是IO线程准备好数据放在消息队列中,用户线程负责轮询poll,这样,应用层的回调就是由用户线程进入的,保证模型简单。
而至于IO线程是如何准备数据的,平台不同做法不同。
linux上最合适的做法是reactor,win最合适的做法就是proactor,一个例外是mono,mono跑在linux平台上的时候虽然IO库是reactor模型,但是在C#层面还是表现为proactor模型。
提供统一poll语义的网络库可以隐藏这种平台差异,让应用层看起来就是统一的本线程poll,本线程回调。
网络库要解决的第三个问题是,封装具体的连接细节。
cs架构中一方是client一方是server,因此连接细节在两侧是不一样的。
而由于socket是全双工的,因此之前所说的IO模型对于任意一侧都是适用的。
连接细节的不同就体现在,client侧,核心需求是发起建立连接,外围需求是重连;server侧,核心需求是接受连接,外围需求是主动断开连接。
而两边等到连接建立好,都可以基于这个连接构建同样的IO模型就可以了。
现在,简单介绍一种网络库实现。
∙一个连接好的socket对应一个connector。
∙connector负责向上提供IO模型抽象(poll语义)。
同时,其借助维护的一个connector_buffer,来实现流转包。
∙网络库中的client部分主要组件是ClientNetwork,维护连接(与重连)与一条connector。
∙网络库中的server部分主要组件是ServerNetwork,维护接受连接(与主动断开)与N条connector。
∙Network层面的协议非常简单,就是len+data。
具体代码不再在博客里贴了。
请参考:
Network
引入新的问题
如果类比马斯洛需求中的层次,有了网络库,我们只能算是解决了生理需求:
可以联网。
但是后面还有一系列的复杂问题。
最先碰到的问题就是,玩家数量增加,一个进程扛不住了。
那么就需要多个进程,每个进程服务一定数量的玩家。
但是,给定任意两个玩家,他们总有可能有交互的需求。
对于交互需求,比较直观的解决方案是,让两个玩家在各自的进程中跨进程交互。
但是这就成了一个分布式一致性问题——两个进程中两个玩家的状态需要保持一致。
至于为什么一开始没人这样做,我只能理解为,游戏程序员的计算机科学素养中位程度应该解决不了这么复杂的问题。
因此比较流行的是一种简单一些的方案。
场景交互的话,就限定两个玩家必须在同一场景(进程),比如攻击。
其他交互的话,就借助第三方的协调者来做,比如公会相关的通常会走一个全局服务器等等。
这样,服务端就由之前的单场景进程变为了多场景进程+协调进程。
新的问题出现了:
玩家需要与服务端保持多少条连接?
一种方法是保持O(n)条连接,既不环保,扩展性又差,可以直接pass掉。
那么就只能保持O
(1)条连接,如此的话,如何确定玩家正与哪个服务端进程通信?
要解决这个问题,我们只能引入新的抽象。
1.1.2Gate
定义问题
整理下我们的需求:
∙玩家在服务端的entity可以在不同的进程中,也可以移动到同一个进程中。
∙玩家只需要与服务端建立有限条连接,即有访问到任意服务端进程的可能性。
同时,这个连接数量不会随服务端进程数量增长而线性增长。
要解决这些需求,我们需要引入一种反向代理(reverseproxy)中间件。
反向代理是服务端开发中的一种常见基础设施抽象(infrastructureabstraction),概念很简单,简单说就是内网进程不是借助这种proxy访问外部,而是被动地挂在proxy上,等外部通过这种proxy访问内部。
更具体地说,反向代理就是这样一种server:
它接受clients连接,并且会将client的上行包转发给后端具体的服务端进程。
很多年前linux刚支持epoll的时候,流行一个c10k的概念,解决c10k问题的核心就是借助性能不错的反向代理中间件。
游戏开发中,这种组件的名字也比较通用,通常叫Gate。
Gate解决了什么问题
∙首先,Gate作为server,可以接受clients的连接。
这里就可以直接用我们上一节输出的网络库。
同时,其可以接受服务端进程(之后简称backend)的连接,保持通信。
∙其次,Gate能够将clients的消息转发到对应的backend。
与此对应的,backend可以向Gate订阅自己关注的client消息。
对于场景服务来说,这里可以增加一个约束条件,那就是限制client的上行消息不会被dup,只会导到一个backend上。
仅就这两点而言,Gate已经能够解决上一节末提出的需求。
做法就是client给消息加head,其中的标记可以供Gate识别,然后将消息路由到对应的backend上。
比如公会相关的消息,Gate会路由到全局进程;场景相关的消息,Gate会路由到订阅该client的场景进程。
同时,玩家要切场景的时候,可以由特定的backend(比如同样由全局进程负责)调度,让不同的场景进程向Gate申请修改对client场景相关消息的订阅关系,以实现将玩家的entity从场景进程A切到场景进程B。
站在比需求更高的层次来看Gate的意义的话,我们发现,现在clients不需要关注backends的细节,backends也不需要关注clients的细节,Gate成为这一pipeline中唯一的静态部分(staticpart)
当然,Gate能解决的还不止这些。
我们考虑场景进程最常见的一种需求。
玩家的移动在多client同步。
具体的流程就是,client上来一个请求移动包,路由到场景进程后进行一些检查、处理,再推送一份数据给该玩家及附近所有玩家对应的clients。
如果按之前说的,这个backend就得推送N份一样的数据到Gate,Gate再分别转给对应的clients。
这时,就出现了对组播(multicast)的需求。
组播是一种通用的messagepattern,同样也是发布订阅模型的一种实现方式。
就目前的需求来说,我们只需要为client维护组的概念,而不需要做inter-backend组播。
这样,backend需要给多clients推送同样的数据时,只需要推送一份给Gate,Gate再自己dup就可以了——尽管带来的好处有限,但是还是能够一定程度降低内网流量。
那接下来就介绍一种Gate的实现。
我们目前所得出的Gate模型其实包括两个组件:
∙针对路由client消息的需求,这个组件叫Broker。
Broker的定义可以参考zguide对DEALER+ROUTERpattern的介绍。
Broker的工作就是将client的消息导向对应的backend。
∙针对组播backend消息的需求,这个组件叫Multicast。
简单来说就是维护一个组id到clientIdList的映射。
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层。
可以说,在这种方案成熟之后,程序员之间开始流行“游戏开发技术封闭”这种说法了。
为什么?
举一个简单的例子,大概描述下现在一个游戏项目的服务端生命周期状况:
∙第一阶段,大概到目前这篇文章的进度为止,实现了场景内跑跳打。
∙第二阶段,疯狂地为场景进程增加逻辑,各种跟游戏有关的逻辑全加进来,直到这部分的代码量占到整个服务端代码量的80%以上。
∙第三阶段,有节操的程序员考虑拆分进程,当然,一开始的协调者进程一直都会存在,毕竟有些需求是场景进程无论如何都实现不了的。
拆分进程的典型例子有聊天、邮件、公会等等。
拆分出来的进程基本上是对场景进程代码轮廓的拷贝粘贴,删掉逻辑就开始在这之上写了。
结果就是,产出了几个玩具水平的服务器进程。
要非得说是工业级或者生产环境级别的吧,也算是,毕竟bugfix的代码的体量是玩具项目比不了的。
而且,为了更好地bugfix,通常会引入lua或者python,然后游戏逻辑全盘由脚本构建,这下更方便bugfix了,还是hotfix的,那开发期就更能随便写写写了,你说架构是什么东西?
至于具体拓扑,可以对着下图脑补一下,增加N个节点,N个节点之间互相连接。
玩具水平的项目再修修补补,也永远不会变成工艺品。
skynet别的不说,至少实现了一套轻量级的actormodel,做服务分离更自然,服务间的拓扑一目了然,连接拓扑更是优雅。
网易的mobile_server,说实话我真的看不出跟bigworld早期版本有什么区别,连接拓扑一塌糊涂,完全没有服务的概念,手游时代了强推这种架构,即使成了几款过亿流水又怎样?
大网易的游戏开发应届生招聘要求精通分布式系统设计,就mobile_server写出来的玩具也好意思说是“分布式系统”?
很多游戏服务端程序员,在游戏服务端开发生涯结束之前,其接触的,或者能接受的设计基本到此为止。
如果是纯MMO手游,这样做没什么,毕竟十几年都这样过来了,开发成本更重要。
更搞笑的是社交游戏、异步战斗的卡牌游戏也用mobile_server,真搞不明白怎么想的。
大部分游戏服务端实现中,服务器进程是原子单位。
进程与进程之间的消息流建立的成本很低,结果就是服务端中很多进程互相之间形成了O(n^2)的连接数量。
这样的话会有什么问题?
一方面,连接拓扑关系很复杂。
一种治标不治本的方法是抬高添加新进程的成本,比如如非必要上面不会允许你增加额外进程,这样更深度的解耦合就成了幻想。
另一方面,游戏服务端的应用层与连接层难以分离。
举个例子,在这种设计思路下,两个进程有没有连接是一种不确定态,设计的时候觉得没有,结果某个需求来了,不建立连接就很难实现。
这样对于应用层来说,就需要提供连接的概念,某个进程必须先跟其他进程连接建立成功了,然后才能调用其他进程提供的服务。
而实际上,更优雅的设计是应用层完全不关注连接细节,只需要知道其他的进程提供了服务,自己就能获取到这种服务。
这样,我们就需要在游戏服务端中提供服务的概念。
场景同步服务是一种服务,聊天服务是另一种服务。
基于这个思路,我们继续探讨服务应该如何定义,服务有哪些类型,不同类型的服务的消息流应该是怎样的。
友情提示:
范文可能无法思考和涵盖全面,供参考!
最好找专业人士起草或审核后使用,感谢您的下载!
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- 游戏 服务器端 完成