网络游戏服务器构架设计(三):刀剑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进行一些规划。
总结:
优点:服务器程序扮演上帝的角色,可以获得几乎所有的信息,这对逻辑编写带来极大的便利。和“自我”模式一样,最好把移动的相关逻辑独立出来使用不同的锁,以免照成过多的加锁冲突和等待。“上帝之手”模式更容易进行整体规划,更容易实现模块化设计,降低程序的耦合度。
缺点:上帝也不是这么好演的,“上帝之手”模式对整体框架、接口设计、加锁策略等有更高的要求,同时对编程人员的要求也会更高。
References:
[1]像素软件技术总监--魏华:《刀剑Online服务器构架分析》
[2]张昆:《Mangos服务器的游戏对象和AI系统》