本文主要明确网络游戏服务器构架的设计目标,并作出一些限定。因为本系列所讨论的服务器端构架只适用于部分网游,并不是一个通用的网游服务器构架。
设计目标:
支持的游戏类型:大型MMORPG游戏,类似魔兽世界(有大世界,不是开房间式)。
连接方式:以TCP长连接为主。动作类游戏并不在本文讨论范围内(因为本人并没有参与开发过动作类游戏),如果有时间可以研究一下龙之谷(部分使用UDP传输)、天下贰(全部使用UDP传输),类似的逆向工程网上已经有人做了。
在线人数:保证最大1w人左右在线还能比较流畅的运行。如果在线人数大于1w对客户端的同学和策划的同学都是很大的挑战。
服务器可以以多进程的形式布置在不同的物理主机上,也可以布置在同一主机上,考虑效率的同时兼顾可扩展性。
能在普通配置的服务器上流程运行,物理主机配置:按照DELL 1950、DELL R610上32G内存的标准来部署主机,一般公司是用不起WOW的小霸王的.......
内存不用过多的考虑,因为现在服务器的内存已经很大了。减少内存使用会放到模块设计、详细设计里。不在构架分析的讨论范围内。
偏格斗的游戏会对CPU和带宽要求比较高,设计时需要进行讨论。
设计目标就这些多说无益,核心就是设计出能够支持类似魔兽世界的大型MMORPG游戏的网游服务器端。具体的设计以及设计时的取舍、需要解决的问题等,会在后续的文章进行详细的介绍。
网络游戏服务器构架设计(二):刀剑Online - 连接负载服务器CLS
本文并没有涉及什么逆向工程,只是拜读刀剑Online服务器端主程的文章后[1],想结合自己的经验谈一谈。
PS:由于题目范围太大,本系列的前言做了一些限制。
一、网络游戏服务器
要想设计好网络游戏服务器的构架,首先需要知道网络游戏服务器在玩家游戏过程中发挥什么作用。就我个人的理解:网游服务器在玩家游戏过程中扮演上帝的角色。玩家在服务器制定的规则下进行游戏,服务器负责同步在线玩家之间的属性、操作、状态等等,最终在多个不同的客户端呈现一个“统一”的游戏世界。
所谓的服务器构架在本系列blog中,主要是指如何将服务器各部分合理的安排,以实现最初的功能需求。好的结构不是一蹴而就的,是通过需求的推动一步步的完善。而且每个设计者心中的标准不尽相同,所以我认为并没有绝对优秀服务器构架。本系列文章中所谓的优秀构架是指各方面达到一种平衡(包括成本等的非技术因素)。
下面先介绍刀剑Online的服务器构架(后续还可能有WOW、天龙等):
二、刀剑Online
image
图1 刀剑Online服务器构架
看了像素的技术总监魏华的文章[1],感觉有点意思。文章中所介绍的服务器构架并不复杂,但满足一般MMORPG网游要求应该是绰绰有余了。按照魏华自己的话,这样的服务器构架主要满足能够接受以下几条限制的网络游戏:
游戏同时在线人数在1w人以下。
服务器为多进程程序,可部署在一台或者多台机器上。
服务器内存足够大,一般一个进程1~2G的内存需求还是应该满足的,64位系统能支持更大的内存需求。内存这块主要看游戏的设计需求。
刀剑属于格斗性质的网游,对CPU和带宽有一定的要求。(这篇文章写于2005年,当时刀剑可能是按照256k、512k或1M ADSL的网速进行设计的。现在已经开始普及10M网,带宽和网速应该可以满足),网络延迟的问题本文后面再做展开。对CPU的要求主要影响可承载人数。
地图采用独立小场景的管理模式,各个场景之间通过传送点来连接。每个场景服务器程序分管一部分地图。
服务器包括游戏中和游戏外两大部分,这里主要讨论游戏中的服务器构架,类似客户端自动更新等的游戏外服务器不在本文的讨论范围。
接下来对刀剑Online的服务器构架的各个部分进行详细的分析,其中包含很多我自己的想法,很多内容都是我猜测的,因此不能“信以为真”。
2.1 连接负载服务器(Connection Load Server,CLS)
游戏客户端在游戏过程中实际上是和连接负载服务器(简称CLS)进行连接并做数据交互的。如文中[1]所诉,CLS主要的作用是:
把网络连接和真正的游戏逻辑隔离开,降低游戏逻辑服务器处理网络交互的负担,同时提高游戏的安全性。有了CLS,刀剑Online的服务器对于玩家来说就是一个黑盒,如下图:
图2
使场景服务器(Zone Server)更为独立——客户端连接CLS而不是直接连接Zone Server的好处是:用户切换场景服务器时,并不会导致原来的TCP连接断开,从而使设计更为简单和独立。
提高发送广播消息的效率,比如需要全服广播时,游戏逻辑服务器只需要对CLS发送一条广播指令,而向每个用户的广播工作由CLS完成。
完成客户端数据交互的加密解密过程。
连接服务器的主要工作正如上述魏华谈到的,但有几点并没有做出强调,下面本人结合平时的实际工作给出一些补充(不一定正确):
根据经验:
CLS作为与client建立连接、进行数据交互的“Gate”,从程序角度来看CLS的代码应该是最简洁高效的。因为CLS主要负责与客户端交互数据是的加密解密、以及数据搬运。而使用流加密算法RC4对整个数据流进行加密解密是很耗CPU的,因此代码的高效在这个模块是十分的重要。
为了提高程序的运行效率,CLS程序往往会使用-O3的选项进行编译,这无形中又对代码的编写有更高的要求。CLS一般会有自己的打包机制(控制发送频率),因此常会使用TCP_NODELAY选项禁用Nagle算法。
CLS常会被分配到不同的物理机器上,因为操作系统在处理TCP包时,需要通过软中断来通知进程或者唤醒system call,在服务器十分繁忙的时候CPU可能处理不过来。解决办法是:使用多核的服务器,然后把TCP的软中断平均分配到多个CPU。(一些操作系统默认只使用0号CPU来处理,Fedora Core release 2默认就是只使用0号CPU,较新的版本我没有做研究)
CLS在做数据流的解密后,往往需要把数据包构造成内部服务器进程间通讯使用的protocol,这种protocol模块要独立,序列化和反序列化的接口要稳定,这样以后需要更换协议模块也不至于伤筋动骨。可以使用像google的protobuf这样的开源协议,减少开发难度。
CLS负责建立和client的连接,多会使用多个CLS进程才能支撑1w的在线人数,因此在CLS前端一般会有负载均衡的程序,负责把建立连接的请求均匀的提交到各个CLS。
有一个需要讨论的问题:作为服务器端的“Gate”,只负责数据转发的CLS是否需要对client发过来的数据进行完全的解密?或者只解密包头,知道转发的目的地即可?(RC4并没有增加流的长度,因此可以只做部分解密)
CLS只做部分解密:
好处:将耗费CPU资源的解密功能分摊到别的进程;各进程各服务可以在解密后用不同的方案来构造自己的protocol。
CLS做完全解密:
好处:可以提前过滤部分无效的消息,只做部分解密也可以做提前过滤,但是这样太过于依赖协议的设计;在CLS处做完全解密,则往后服务器端的之间的消息传递都是明文,利于抓包查错;由于CLS的功能比较简单,很容易通过加机器来进行扩展,因此计算放在CLS上是比较明智的选择。
总结:
CLS是一个功能相对简单但要求代码简洁高效的程序,在设计实现的时应该注重效率及代码编写规范。经过对比在CLS程序对数据流进行完全解密是利大于弊的,推荐使用这种方案。
网络游戏服务器构架设计(三):刀剑Online - 总控服务器、场景服务器
上一篇《网络游戏服务器构架设计(二)》介绍了刀剑Online的连接负载服务器CLS,博友提出质疑“说得不够详细,比如你怎么,场景服务器怎么才算一个场景服务器,场景服务器切换怎么处理不断线后连接另一个场景的,还有很多细节问题没有说到”,本篇就来介绍游戏服务器最为核心的部分:游戏逻辑服务器,同时也回答了这位博友的问题。
PS:本篇的文章结构主要分两个部分,前半部分(2.2节)介绍刀剑Online如何实现游戏逻辑服务器,后半部分(2.3节)为本人结合实际工作对这套服务器构架做出的一些展开解释及补充,主要对设计思想进行分析。精彩在后面哦!
先来回顾一下刀剑Online的总体构架图:
2.2 游戏逻辑服务器
顾名思义,就是和游戏具体逻辑相关的服务器(这应是一个统称)。这块是网游服务器端的核心部分,不同的游戏差别会很大。在刀剑中,游戏逻辑服务器分为两部分:总控服务器和场景服务器。
2.2.1 总控服务器(Master Server,MS)
关于总控服务器的作用,刀剑Online的主程是这么解释的:
总控服务器(以下简称MS)的作用之一是负责玩家在具体游戏内容之外的操作(即.玩家进入场景服务器之前地操作)。如:登录、注销、各种角色操作(创建、删除、选择)等等。
MS和所有地场景服务器都保持连接,这样它就成为各个场景服务器间的枢纽,当需要一些跨场景服务器的操作或者需要访问别的场景服务器数据的时候,指令都先发给MS,然后MS根据需要再转发给相应地场景服务器或者直接发给相应的用户,并进行后续地协调工作。
比如:在场景服务器1上的用户A希望向游戏中的用户B发出一条添加好友的请求,则场景服务器1向MS发送添加好友指令并附带了用户B的名字,MS查找发现有B这样的用户,则直接把指令发给CLS,然后由CLS转发给B用户;如果没有发现B用户则直接通知A未发现B。
又比如:在场景服务器1上的用户A点中了传送点,将要传到场景X,场景服务器1发现X场景并不在自己的管辖范围内,于是发送转移指令给MS,MS查找发现场景X在场景服务器2上,于是先发送用户A的离开指令给场景服务器1,让用户退回到MS上,然后再发送用户A的进入指令给场景服务器2,并说明用户将要进入的场景为X,这样一次跨服务器的场景转移就完成了。[1]
2.2.2 场景服务器(Zone Server,ZS)
关于场景服务器的作用,刀剑Online的主程是这么解释的:
场景服务器(以下简称ZS)就是具体负责游戏场景的服务器。玩家选择人物开始游戏之后就进入了这种服务器(即开始游戏之后CLS把所有玩家的操作指令都转给ZS)。
玩家的各种操作的逻辑都是由ZS完成的,同时,ZS也要负责各个场景以及场景中的NPC和场景中各个物品的逻辑运行。
每款游戏的真正游戏性的核心就是这些ZS。它的具体细节我就不过多的讲述了,各个游戏的具体内容应该都不相同。不过有几个原则是共同的:
一、是要高效。如果ZS对游戏逻辑的处理效率低,会直接影响玩家同时在线的数量,并导致游戏中的玩家感觉很“卡”,这是除了网络延时之外第二个会造成游戏 “卡”的地方。提高效率的方法除了对代码进行优化外,就是要使用高效的脚本系统,直接把脚本转化为程序代码编译到程序中去也不失为一个办法。
二、是要有灾难恢复机制,就是当ZS发生非法操作时(只要不停电)能够恢复出非法操作时各个用户的数据。这个在游戏运营初期服务器尚不稳定的时候非常重要。虽然我们也可以通过加快用户存盘间隔的方式(比如把每10分钟存一次盘改为每1分钟存一次盘),但是这会成倍加重数据库负担,同时也不能避免由于用户刚好在存盘间隔的时候获得了重要的金钱或道具而导致丢失的情况。刀剑采取的方法是在申请用户关键数据对象的时候通过包装的函数从共享内存中申请,这样即便ZS非法操作了,共享内存并不会消失,在重新启动的时候就可以从共享内存中恢复出程序非法时的用户数据。当然恢复的时候也需要对用户数据进行一些校验,以免把已经被破坏的数据存入数据库。[1]
2.3 设计思想分析
游戏逻辑服务器主要负责汇总所有在线的client发来的各种操作、状态等数据包,经过一系列的处理后有选择的广播给需要的client,从而给所有在线的玩家呈现一个“统一”的世界。
优秀的逻辑服务器架构需要优秀的设计思想,而优秀的设计思想又源于对游戏虚拟世界的适度抽象。抽象可以看作一个工程问题,同时也可以看作一个哲学问题,这正是游戏开发的魅力所在。本系列blog将围绕这种抽象一步步展开,结合不同的项目来诠释网络游戏服务器端的设计思想(希望能做到....)。
站在服务器端的角度对游戏虚拟世界进行抽象,首先要弄清楚构造虚拟世界需要些什么?让我们来想象一下吧(以下内容参照了《盗梦空间》-“Inception”),先来看一段视频:
Inception是我非常喜欢的影片,第一次看到这一段的时候,就感觉非常像游戏设计。今天能把它写下来也算没浪费几十块的电影票钱。
影片中饰演the architect(造梦师)的艾伦·佩姬(女),正在接受莱昂纳多(饰演the extractor-盗梦者)的训练。莱昂纳多说道:“Remember you are the dreamer you built this world。”,“I'm the subject my mind populates it”。值得关注的两个名词:world,subject。这就是我们要讨论的主题。那么构造网络游戏的虚拟世界需要些什么呢?其实莱昂纳多已经替我回答了这个问题:“We create and perceive out world simultaneously. You create the world of the dream, We bring the subject into that dream, and they fill it with their subconscious.”。world就相当于游戏里的场景,而subject就是一个个在线的玩家(player)。
游戏世界(world,这里的world泛指游戏世界及地图,见2.3.2)及游戏对象(object,包括player)是构造网络游戏服务器端时,需要关注的两个重点。如何处理好world和object的关系和地位直接影响到服务器端的构架。
2.3.1 游戏对象(object)
先来看游戏里一般会有那些object,以Mangos为例:
图2 mangos游戏对象的class diagram[2]
对于上面的类层次结构图这里不做展开(留到本系列后面的文章中展开),这里引用mangos的游戏对象是想给读者一个直观的印象,游戏中的object在服务器端是个什么摸样。类的层次是对游戏世界中的对象抽象后得到的结果,抽象需要“适度”:如果类的层次过深,维护起来困难,而且后期往往会导致基类过于臃肿、不堪重负;如果类的层次过浅,一些object的共性不能体现,很多代码会重复出现在各个子类里,复用性差。
2.3.2 游戏世界(world)
游戏世界在本文是泛指游戏对象所在的场景,以及附加在场景之上的地图管理和对象管理,这里统称游戏世界管理。
如何进行游戏世界的管理是一个复杂的工程问题,网络游戏常常会把整个游戏世界分为若干张地图,每张地图又会分为若干个区域进行管理。这若干张地图能无缝的过渡就称为无缝大地图模式(类似WOW),如果像刀剑Online那样只能通过传送服务在两张不同地图之间穿梭的,称为有缝地图。而相对来说服务器端会比客户端简单一些,例:如果在客户端看到的场景是下面这个样子:
服务器端根据划分区域的不同可能看到的地图是这个样子:
这里服务器端用到了tile-based的方式来管理地图,这种tile的方式在2d游戏中十分的常见(打格子),而很多3d网游的服务器端为了减少运算量也采用这种tile模式划分区域进行管理。如果对地图管理有兴趣请阅读云风的blog《用四叉树管理散布在平面上的对象》、《碰撞检测》,本文先不做详细的展开。
2.3.3 游戏对象和游戏世界的关系
如何处理游戏对象和游戏世界的关系和地位,是影响服务器端架构的最直接因素。为了体会到这点,下面将以刀剑Online为例,分析玩家对象(player,游戏对象中最为重要的部分)和游戏世界的关系对整个服务器构架设计的影响。
1. 玩家对象player的构建
刀剑Online把游戏逻辑服务器分为总控服务器(Master Server,MS)和场景服务器(Zone Server,ZS,本文提到的游戏世界多指ZS),那么在client成功登陆server后,服务器端应该在哪构建player对象呢?在MS上?还是在ZS上?这是个工程问题,同时也是个哲学问题.......
如果把player(以及其他的游戏对象)放在Master Server上:也就是说所有登陆的玩家的数据都会在MS的内存中有一份映像,MS将保存着player最新的数据。这么做的好处是player的数据统一,从而使同服玩家的一些交互变得十分方便,比如同服玩家组队、交友等不需要知道玩家在那个ZS里。切换场景服务器时也不需要把player数据从一个ZS拷贝到另一个ZS,只需要把player身上记的ZS信息修改一下;player在MS上构建的缺点也是显而易见的,所有中心节点式的系统都会遇到单点不可靠的问题,player的信息都保存在MS上,假如有1w人在线,内存占用就是一个很大的问题(毕竟单个进程的内存使用还是有限制的,实际开发中曾经遇到占用十几G内存的进程......相当可怕),而如果一个player借住外挂发送一些非法消息给MS,错误处理不当时很有可能会core进程,这样一来整个服务器的玩家都会掉线。还有一些和地图相关的逻辑会很难操作,比如player与场景中怪物进行PK时,由于player的数据保存在MS,因此ZS需要做一个Attach操作告知MS某个player和某个monster进行战斗,MS进行完伤害计算后发AttachResult给ZS,最后由ZS再广播给相关的客户端,如图:如图中标注的(1)~(4)的步骤,展示了这个过程,编程操作起来还是挺麻烦的。
如果把player放在Zone Server上:这是一种比较常见的方式,player和其他游戏对象在场景服务器中构造,隶属于Zone Server。由ZS来掌管player从编程角度讲好处多多:可以直接获得player的信息,对于场景内交互十分便利,场景内的操作不需要多余的数据传输。而在需要跨场景操作时只需用MS中转一下即可;但是这种方式缺点同样是很致命的,player在Zone Server上构造就失去数据放在中心节点的简洁,player的信息常常需要在ZS、MS和DB上进行同步,这种“三体同步”的痛苦只有做过的人才能体会。比如:几个独立的服务器之上需要增加一个跨服服务器,这几个普通服务器的玩家可以进入这个跨服服务器进行pk。那么就需要将player先从普通服务器ZS退到MS,由普通服务器的MS把player的数据发给跨服MS,最后再由跨服MS发送到跨服的ZS。如果还涉及DB则会更加的复杂。ZS、MS、DB相互独立,如果没有强制规定,这种同步往往是没有方向的:player的最新的数据一般在ZS上,但是有些交互不能在ZS上完成,那么可以有两种选择:(1)ZS把player数据发给MS,由MS进行操作,操作过程中ZS不能修改player的相关数据。(2)把player的最新数据存盘,然后由MS读盘取player的数据然后进行操作,操作过程中ZS还不能改变player的相关数据。不管哪种方法开发的逻辑多了以后都不好维护,从此维护人员都变成怨妇.......
刀剑Online应该采用的是第2中方式,把player放在Zone Server上。
2. 玩家对象player和world的关系
如何处理玩家对象player和world的关系集中体现了服务器端的哲学,值得细细品味。处理两者的关系在本人看来有两种主要的模式:一种是以player的中心的“自我”模式;另一种是以游戏世界为中心的“上帝之手”模式。这两种模式的哲学主要体现在加锁模式上,接下来进行详细介绍:
(一)player的“自我”模式
“自我”模式顾名思义,就是player以自我为中心,什么事都要亲历亲为。以player使用物品为例:
如上图是一个完整的player使用物品的流程。“自我”模式体现在上图的(1)位置,在(1)中首先取到服务器中对应的player,加锁后用player调用自己的处理函数进行处理。由于(1)相当于处理客户端发来的请求的入口处,因此在这里进行加锁和解锁操作是十分合适的,此后的(2)~(5)player在调用自己的处理函数时,编程人员完全不需要考虑加锁的问题。
所谓的“自我”模式,其实就是指在服务器端对player的操作其实都是player自己去完成的,“我”自己去把事情做完。处理逻辑时“我”(自己的player实例)不会主动去锁住对方的player实例,因此一个player不能修改另一个player的数据,world也不能修改player的数据。world和player的关系是“独立”的,player身上记着world的信息(相当于“我属于哪个世界”),world里存放着player的实例,但是它们直接不能直接修改对方的数据。这么做是为了避免死锁,使得加锁解锁变得简单而统一(如上图)。
“自我”模式有加锁解锁的便利,但是一个网络游戏怎么可能player和player、player和world直接没有交互呢?交互就需要获得或修改对方的数据,遇到这样的问题“自我”模式怎么处理呢?请看下图:
如上图:client A向服务器发送“向玩家B发起攻击”的消息,服务器端client A对应的实例Player A收到消息后,发现需要与player B进行交互,根据“自我”模型的限定,player A不能直接修改player B的血量等信息,这时player A需要做的是将自己的信息打包成一个msg结构然后push_back到消息队列message queue并指定player B作为接收者。在message queue上将这个msg转发给player B前,会先调用lock(playerB)将B锁住,接着把msg传给player B进行处理,在处理完毕后再调用unlock(playerB)。lock和unlock都在统一的地方调用,加锁解锁十分简洁,playerB处理msg消息时不用关心任何加锁问题。在上图的(2)中,player B进行伤害计算后将伤害消息广播给周围的玩家。
注意:“自我”模式下,伤害计算是在受攻击方进行的。player之间、player和world之间都是通过消息传递进行交互的。
总结:
优点:“自我”模式的优点集中体现在加锁上,编程人员在编写具体逻辑时不用担心加锁问题,也不用费尽心思来避免死锁,因为加锁都在消息入口处统一做了,同时不会主动去对别的player实例进行加锁操作。使用这种模式时,最好把移动的相关逻辑独立出来使用不同的锁,以免照成过多的加锁冲突和等待。
缺点:“自我”模式的缺点也是十分明显的,每个player实例作为不同的“我”独立存在,需要交互时只能通过消息队列来传递信息,极大的增加了交流的成本(内存、CPU占用率都会增加)。一个player往往不能即时的取到别的player的数据,这样一来很多的计算都只能做延后处理,比如伤害计算就只能放在被攻击者的player上,使得很多需要做先验判断的技能实现起来变得复杂,这类技能只能靠释放技能的player,发送请求消息给其他player,然后再由其他player把自己的信息通过msg queue发给释放技能的player。异步处理方式往往会但来更多的烦恼——需要增加很多错误判断、错误处理以及超时处理等等。在线人数高的时候,message queue的容量以及所占用的内存也是需要考虑的问题。
引用狄更斯的:“It was the best of times, it was the worst of times”。本人依葫芦画瓢:“自我”模式是一种nb的设计,也是一种sb的设计.......
(二)world的“上帝之手”模式
“上帝之手”模式是以world为中心(类似war3),以地图为单位进行划分,player、NPC、monster等游戏对象都隶属于world。在world的掌控之下,就像有一只上帝之手在拨弄着这些小玩意。以client A向服务器发送“向玩家B发起攻击”消息为例,服务器端的处理流程如下:
从上图中可以看出“上帝之手”模式的核心是world去完成这项任务:找到playerA和B,把他们都锁住,然后交给技能模块来进行伤害计算,最后把结果广播出去。整个过程就像在玩war3这样的RTS游戏,服务器就像一个神,以斜45度的上帝视角来观察所有的玩家。
“上帝之手”模式的设计难点在于加锁策略,因为需要对多个对象进行加锁,加解锁的顺序不当容易产生死锁。加锁策略有很多种,这里不做具体的展开。只介绍一种最常用的方法,即对加锁对象进行排序。游戏对象在产生时都会有一个唯一的id做标识,当lock ()函数能按照一种稳定的算法对这些id进行排序时,就可以避免死锁。比如对游戏对象进行分类:player、npc、monster等,先对分类进行排序,再对对象的id进行排序。需要注意的是MMORPG游戏经常需要进行合服操作,为了保证合服后player的id不会重复,需要对player_id进行一些规划。
总结:
优点:服务器程序扮演上帝的角色,可以获得几乎所有的信息,这对逻辑编写带来极大的便利。和“自我”模式一样,最好把移动的相关逻辑独立出来使用不同的锁,以免照成过多的加锁冲突和等待。“上帝之手”模式更容易进行整体规划,更容易实现模块化设计,降低程序的耦合度。
缺点:上帝也不是这么好演的,“上帝之手”模式对整体框架、接口设计、加锁策略等有更高的要求,同时对编程人员的要求也会更高。
网络游戏服务器构架设计(四):云风的轨迹
最近闲着没事把云风的《开发笔记》看了个遍,希望能从大牛的开发轨迹中得到一些启发。但可能是因为本人level太低,一遍看下来还是云里雾里,不甚明白。没办法只好再看一遍,希望能对他们的服务器端架构有个简单的认识,这里同时做些笔记。
PS:本文是我个人对云风的开发笔记的读后感,可能会有很多错误,慎入!
一、服务器划分原则
在现有的网络游戏服务器端架构中,多是以功能和场景来划分服务器结构的。负载均衡和集群暂且不在本文中讨论(bigworld、atlas)。服务器划分可以基于以下原则:
分离游戏中占用系统资源(cpu,内存,IO等)较多的功能,独立成服务器。
以多线程或多进程的编程方式适应多核处理器。
在同一个服务器架构下,应尽可能的复用某些服务器(进程级别的复用,比如场景服务器)。
运行时玩家数据的保存、修改及数据流向应该是设计的焦点,它同时也决定了服务器应该如何划分。
服务器的划分应该适度,在保证清晰的数据流向的前提下,根据游戏的类型和规模尽量减少服务器或服务器进程的个数,以减少服务器之间过多的复制数据、锁冲突(使用共享内存进行通讯时)。
主要按照场景划分进程,若需按功能划分,必须保持整个逻辑足够简单,并满足以上1、3两点[1]。
接下来我们来看看云风的服务器架构是如何处理好以上几点的。
图1 服务器架构(此图为本人猜测,可能有误)
二、运行时的玩家数据
网络游戏服务器程序一项重要的工作就是根据client发过来的数据包,在服务器端模拟玩家的行为操作并把这些行为广播出去。那么服务器程序在运行时就需要一些实体来保存玩家的数据,这些实体可以是一个类,也可以是一个线程,设计思想不同采用的实体差别也会很大。这里涉及服务器端设计的一个核心问题:运行时玩家数据的保存、修改及数据流向。
agent
云风通过抽象实体agent来处理单个client的服务请求,agent和client是1:1的关系(见图1)。agent是在gate程序后端,负责翻译、转发以及回应客户端发过来的请求。agent的主要工作内容见云风的《开发笔记 (1)》。值得补充的是设计agent的主要优势是:
把对单个 client 服务的代码集中写在 agent 服务中。由 agent 再和内部其它服务沟通。数据加载使用共享内存的方案,由 agent 向持久化模块发出信号,做加载或纯盘处理,通过共享内存得到结构化数据块。[2]
agent相当于client在服务器上对应的实体,玩家的属性和数据只能由agent来修改,别的服务只有读权限。通过attach操作获得数据(attach可能是通过服务器通讯框架skynet,也有可能直接mmap到共享内存sharedb上以获得数据)。
agent的设计使得整个系统对玩家数据的修改只有一个输入点,数据流十分的明确,易于维护。虽然这种设计可能会照成数据的多次复制,但是带来的代码维护和查错上的便利是十分可观的。
如果把所有的agent放在同一个进程里,在编程该程序时还应该考虑到容错问题,比如说(1)使用C++编写这个程序,agent以类的形式存在,使用thread pool来处理收到的数据包,实际操作时thread的数量是会远远小于agent的数量的,数据包到达后会在队列里等待thread调用agent的逻辑来处理。这是一种比较常见的设计方法,但要注意的是由于agent都放在一个进程里,程序的健壮性要求很高,一个进程core则会导致全服玩家掉线。而使用C++编写也增加了宕进程的可能性……..你懂的。(2)使用Java编写,对于这种“中心节点”式架构来说可能是更好的选择,起码不是因为一个玩家的误操作(可能使用外挂)导致全服玩家掉线。(3)云风似乎是使用lua coroutine来实现agent的相互隔离和协同工作的,这样可以减少单一agent失败对其他agent的影响(动态语言的好处)。
sharedb
sharedb在系统中的地位看上去像是database前端的cache,但就本人的理解sharedb的作用远不止是一个数据缓存。
和天龙八部的ShareMemory类似,sharedb也采用了定长的结构化数据(见《开发笔记 (6)》),通过共享内存来实现进程间的数据共享。sharedb的存在使得游戏逻辑处理和数据保存逻辑得到很好的隔离,游戏逻辑不用关心后端的数据是如何保存的,只要sharedb挂上定期存盘的服务,在接口定义明确的情况下,后端到底采用什么样的数据库变得不是那么重要,从而降低了系统的耦合度。
三、服务器底层框架skynet
skynet的设计思想见《Skynet 设计综述》:
我希望我们的游戏服务器(但 skynet 不仅限于用于游戏服务器)能够充分利用多核优势,将不同的业务放在独立的执行环境中处理,协同工作。这个执行环境,最早的时候,我期望是利用 OS 的进程,后来发现,如果我们必定采用嵌入式语言,比如 Lua 的话,独立 OS 进程的意义不太大。Lua State 已经提供了良好的沙盒,隔离不同执行环境。而多线程模式,可以使得状态共享、数据交换更加高效。而多线程模型的诸多弊端,比如复杂的线程锁、线程调度问题等,都可以通过减小底层的规模,精简设计,最终把危害限制在很小的范围内。这一点,Skynet 最终花了不到 3000 行 C 代码来实现核心层的代码,一个稍有经验的 C 程序员,都可以在短时间理解,做维护工作。
做为核心功能,Skynet 仅解决一个问题:
把一个符合规范的 C 模块,从动态库(so 文件)中启动起来,绑定一个永不重复(即使模块退出)的数字 id 做为其 handle 。模块被称为服务(Service),服务间可以自由发送消息。每个模块可以向 Skynet 框架注册一个 callback 函数,用来接收发给它的消息。每个服务都是被一个个消息包驱动,当没有包到来的时候,它们就会处于挂起状态,对 CPU 资源零消耗。如果需要自主逻辑,则可以利用 Skynet 系统提供的 timeout 消息,定期触发。
Skynet 提供了名字服务,还可以给特定的服务起一个易读的名字,而不是用 id 来指代它。id 和运行时态相关,无法保证每次启动服务,都有一致的 id ,但名字可以。
本人感觉skynet像一个发布订阅的消息中间件(还没看源码,可能有误),这种基于服务的即插即用式的框架给服务器端带来很大的可扩展性,同时也使得各模块之间独立清晰,具有良好的可维护性。但是这里有个疑问,服务都以so的形式挂在skynet上,那么这些服务从哪里获取玩家、怪物、NPC等object的数据?是从skynet中获得还是直接从sharedb中获得,出于性能的考虑是不是要把skynet和sharedb部署在同一台物理主机上?这样一来就会增加设计和具体逻辑的耦合度。看了《Skynet 集群及 RPC》,感觉skynet上的服务是要通过skynet来获得玩家的数据,这样操作会不会导致数据被复制很多次,不知道最终的效率是否受到影响?
四、gate
满足服务器划分原则里的第一点:分离游戏中占用系统资源(cpu,内存,IO等)较多的功能,独立成服务器。
gate的主要工作可以参见本系列blog的第二篇《网络游戏服务器构架设计(二):刀剑Online - 连接负载服务器CLS》
五、场景管理器
主要用于管理静态场景和动态副本,比如agent登录时查询自己所在场景对应的服务器地址。
六、场景服务器
场景服务器的内容我没有从《开发笔记》中得到太多的信息(可能level太低),更多的是以功能模块的形式写,比如AOI。不过其中有一点比较新颖的是云风认为player的位置、动作状态,战斗数值状态等都是场景的一部份,应该保存在场景中而不是agent中。本节有所更正见(八)补充。
据我的猜测,场景服务器应该会负责:
怪物行走控制,player移动更新及位置同步
怪物AI策略
区域性广播,场景广播
战斗逻辑
AOI服务(Area Of Interest )
碰撞检测
自动寻径
需要注意的是场景服务器修改的一些数据应该以什么样的频率通知agent呢?比如player的位置信息,该信息是完全保存在场景数据里还是说agent里也有一份?
七、总结
本文是一篇云风《开发笔记》系列blog的读后感,所述内容均是本人的猜测,虽恐贻笑大方,但也希望能抛砖引玉。收笔忐忑ing!
八、补充:
(1) 云风在微薄上的回复是:“我们最终采用的是单进程多线程, 每线程上一个 lua state 的结构. sharedb 是用来线程间数据交换的. gate 和 sharedb 以及 loader 和 agent map 一样, 都是 skynet 下的独立服务, 以 so 挂接进去的. 后来的商品交易, 掉落品分配也是 skynet 下的服务. ”
(2) 关于场景服务器,云风已经给出完整的说明,见《开发笔记(14)》
场景服务分成两个部分,一是副本管理器,二是地图服务。在角色数据上,记录有角色应该属于的地图。agent 向地图所属的副本管理器查询,得到他所属的地图服务地址。便可以把自己注册到具体地图上。
地图服务管理了所有其中的角色 id ,以及若干 npc 。他的义务在于把让这些 id 对应的 agent 相互了解。但具体逻辑则放在每个 agent 服务上。每个 agent 自己所属进程 attach 其它 id ,可以获取其他对象的状态。
.........
(3) 风哥在《开发笔记(25)》中已经提到最终使用单进程多线程的模式。看来简单设计是有共识的:-)
游戏服务器框架分析
一个大型的网落游戏服务器应该包含几个模块:网络通讯,业务逻辑,数据存储,守护监控(不是必须),其中业务逻辑可能根据具体需要,又划分为好几个子模块。
这里说的模块可以指一个进程,或者一个线程方式存在,本质上就是一些类的封装。
对于服务器的并发性,要么采用单进程多线程,要么采用多进程单线程的方式,说说两种方式的优缺点:
一、单进程多线程的服务器设计模式,只有一个进程,但一个进程包好多个线程:
网络通讯层,业务逻辑,数据存储,分别在独立的线程中,无守护进程。
优点:
1.数据共享和交换方便,使用全局变量或者单例就可以,数据存储方便。
2.单进程,服务器框架结构相对简单,编码容易。
缺点:
1.所有功能只能在单个物理服务器上,不能做成分布式。
2.不方便监控各个线程状态,容易死锁
3.一个线程出错,例如内存非法访问,栈空间被破坏,那么服务器进程就退出,所有玩家掉线,影响大。
二、多进程单线程的服务器设计模式,多个进程,每个进程只有一个线程:
网路通讯,业务逻辑,数据存储,守护进程,分别在不同的进程。
优点:
1.各个进程可以分布在不同的物理服务器上,可以做成分布式的服务器框架,例如可以将数据存储单独放到一个物理服务器上,供几个区的服务器使用。将网络通讯进程独立出来,甚至可以做成导向服务器,实现跨服战。
2.可以通过守护进程监控其它进程状态,例如有进程死掉,马上重启该进程,或者某个进程cpu使用率接近100%(基本可以判断是某个逻辑死循环了), 强制kill掉该进程,然后重启。
3.单个服务器进程异常退出,只要不是网络通讯进程(一般这个都会比较稳定,没什么逻辑),那么就可以及时被守护进程重启,不会造成玩家掉线,只会造成在1-2秒内,某个逻辑功能无法使用,甚至玩家都感觉不到。
4.服务器通过共享内存进行数据交换,那么如果其中一个服务器死掉,数据还在,可以保护用户数据(当然多线程也可以使用共享内存)。
5.并发性相对多线程要高点。
缺点:
1.不方便使用互斥锁,因为进程切换的时间片远远于线程切换,对于一个高并发服务器是无法允许这么高时间片的切换代价的。因此必须设计好服务器的框架,尽量避开使用锁机制,但要保证数据不出错。
2.多进程编程,在各个进程间会有很多通讯,跨服务器进程的异步消息较多,会让服务器的编码难度加大。
下面先按照一个游戏的功能,将服务器的功能分块框架画出来:
以上是一个游戏服务器最基础的功能框架图,接下来要做的就是设计服务器的框架了
1. 早期的MMORPG服务器结构
Client<->GameServer<->DB 所有业务,数据集中处理
优点:简单,快速开发 缺点: 1.所有业务放在一起,系统负担大大增加.一个bug可能导致整个服务器崩溃,造成所有玩家掉线甚至丢失等严重后果。 2.开服一刹那,所有玩家全部堆积在同一个新手村.->>>>卡,客户端卡(同屏人数过多渲染/广播风暴) 服务器卡(处理大量同场景消息/广播风暴)
2. 中期-用户分离集群式
GameServe1 Client | DB GameServer2
玩家不断增多->分线->程序自动或玩家手动选择进入 缺点:运营到后期,随着每条线玩家的减少, 互动大大减少。
3. 中后期 数据分离集群式
按地图划分服务器,当前主流 新手村问题:《天龙八部》提出了较好的解决方案,建立多个平行的新手村地图,一主多副,开服时尽可能多的同时容纳新用户的涌入,高等级玩家从其它地图回新手村只能到达主新手村。
4. 当前主流的网络游戏架构
注:在GateServer和CenterServer之间是有一条TCP连接的。而GameServer和LogServer之间的连接可以是UDP连接。这是有一个大概的图,很多地方需要细化。
GateServer:网关服务器,AgentServer、ProxyServer
优点: (1)作为网络通信的中转站,负责维护将内网和外网隔离开,使外部无法直接访问内部服务器,保障内网服务器的安全,一定程度上较少外挂的攻击。 (2)网关服务器负责解析数据包、加解密、超时处理和一定逻辑处理,这样可以提前过滤掉错误包和非法数据包。 (3)客户端程序只需建立与网关服务器的连接即可进入游戏,无需与其它游戏服务器同时建立多条连接,节省了客户端和服务器程序的网络资源开销。 (4)在玩家跳服务器时,不需要断开与网关服务器的连接,玩家数据在不同游戏服务器间的切换是内网切换,切换工作瞬问完成,玩家几乎察觉不到,这保证了游戏的流畅性和良好的用户体验。
缺点: 1.网关服务器成为高负载情况下的通讯瓶颈问题 2由于网关的单节点故障导致整组服务器无法对外提供服务的问题
解决:多网关技术。顾名思义,“多网关” 就是同时存在多个网关服务器,比如一组服务器可以配置三台GameGme。当负载较大时,可以通过增加网关服务器来增加网关的总体通讯流量,当一台网关服务器宕机时,它只会影响连接到本服务器的客户端,其它客户端不会受到任何影响。
DCServer:数据中心服务器。主要的功能是缓存玩家角色数据,保证角色数据能快速的读取和保存 CenterServer:全局服务器/中心服务器,也叫WorldServer. 主要负责维持GameServer之间数据的转发和数据广播。另外一些游戏系统也可能会放到Center上处理,比如好友系统,公会系统。
改进:将网关服务器细化为LogingateServer和多个GameGateServer.
5. 按业务分离式集群 由于网络游戏存在很多的业务,如聊天,战斗,行走,NPC等,可以将某些业务分到单独的服务器上。这样每个服务器的程序则会精简很多。而且一些大流量业务的分离,可以有效的提高游戏服务器人数上限。
优点: 1.业务的分离使得每种服务器的程序变的简单,这样可以降低出错的几率。即使出错,也不至于影响到每一个整个游戏的进行,而且通过快速启动另一台备用服务器替换出错的服务器。 2.业务的分离使得流量得到了分散,进而相应速度回得到提升 。 3.大部分业务都分离了成了单独的服务器,所以可以动态的添加,从而提高人数上限。
改进:甚至可以将登陆服务器细化拆分建角色,选择角色服务器
6. 一种简单实用的网络游戏服务器架构
下图中每个方框表示一个独立的进程APP组件,每个服务进程如果发生宕机会影响部分用户,整体服务但不会全部中断。在宕机进程重启后,又可以并入整体,全部服务得以继续。
gls:game login server,游戏登录服务器,某种程序上,其不是核心组件,gls调用外部的接口,进行基本的用户名密码认证。此外需要实现很多附属的功能:登录排队 (对开服非常有帮助),GM超级登录通道(GM可以不排队进入游戏),封测期间激活用户控制,限制用户登录,控制客户端版本等。 db:实质上是后台sql的大内存缓冲,隔离了数据库操作,比较内存中的数据,只把改变的数据定时批量写入sql。系统的算法,开发稳定性都要求非常高。 center:所有组件都要在这里注册,在线玩家的session状态都在这里集中存放,和各组件有心跳连接。所有对外的接口也全部通过这里。 角色入口:玩家登录游戏后的选择角色 gs:game server,最核心组件,同一地图,所有游戏逻辑相关的功能,都在这里完成。 gate:建立和用户的常链接,主要作sockt转发,屏蔽恶意包,对gs进行保护。协议加密解密功能,一个gate共享多个gs,降低跳转地图连接不上的风险。 IM,关系,寄售:表示其它组件,负责对应的跨地图发生全局的游戏逻辑。
7.另一个架构图
1- 这是一条WebService的管道,在用户激活该区帐号,或者修改帐号密码的时候,通过这条通道来插入和更新用户的帐号信息。 2- 这也是一条WebService管道,用来获取和控制用户该该组内的角色信息,以及进行付费商城代币之类的更新操作。 3- 这是一条本地的TCP/IP连接,这条连接主要用来进行服务器组在登陆服务器的注册,以及登陆服务器验证帐户后,向用户服务器注册帐户登陆信息,以及进行对已经登陆的帐户角色信息进行操作(比如踢掉当前登陆的角色),还有服务器组的信息更新(当前在线玩家数量等)。 4- 这也是一条本地TCP/IP连接,这条连接用来对连接到GameServer的客户端进行验证,以及获取角色数据信息,还有传回GameServer上角色的数据信息改变。 5- 这条连接也是一条本地的TCP/IP连接,它用来进行公共信息服务器和数个游戏服务器间的交互,用来交换一些游戏世界级的信息(比如公会信息,跨服组队信息,跨服聊天频道等)。 6- 这里的两条连接,想表达的意思是,UserServer和GameServer的Agent是可以互换使用的,也就是玩家进入组内之后,就不需要再切换 Agent。如果不怕乱套,也可以把登陆服务器的Agent也算上,这样用户整个过程里就不需要再更换Agent,减少重复连接的次数,也提高了稳定性。 (毕竟连接次数少了,也降低了连不上服务器的出现几率) 在这个架构里面,GameServer实际上是一个游戏逻辑的综合体,里面可以再去扩展成几个不同的逻辑服务器,通过PublicServer进行公共数据交换。 UserServer实 际上扮演了一个ServerGroup的领头羊的角色,它负责向LoginServer注册和更新服务器组的信息(名字,当前人数),并且对Agent进 行调度,对选择了该组的玩家提供一个用户量最少的Agent。同时,它也兼了一个角色管理服务器的功能,发送给客户端当前的角色列表,角色的创建,删除, 选择等管理操作,都是在这里进行的。而且,它还是一个用户信息的验证服务器,GameServer需要通过它来进行客户端的合法性验证,以及获取玩家选择 的角色数据信息。 采用这种架构的游戏,通常有以下表现。 1- 用户必须激活一个大区,才能在大区内登陆自己的帐号。 2- 用户启动客户端的时候,弹出一个登陆器,选择大区。 3- 用户启动真正的客户端的时候,一开始就是输入帐号密码。 4- 帐号验证完成之后,进行区内的服务器选择。 5- 服务器选择完成之后,进入角色管理。同时,角色在不同的服务器里不能共享。