[转贴] 游戏服务器架构二

来自:http://www.libing.net.cn/read.php/1724.htmphp

服务器公共组件实现 -- 环形缓冲区数据库

  消息队列锁调用太频繁的问题算是解决了,另外一个让人有些苦恼的大概是这太多的内存分配和释放操做了。频繁的内存分配不但增长了系统开销,更使得内存碎片不断增多,很是不利于咱们的服务器长期稳定运行。也许咱们可使用内存池,好比SGI STL中附带的小内存分配器。可是对于这种按照严格的先进先出顺序处理的,块大小并不算小的,并且块大小也并不统一的内存分配状况来讲,更多使用的是一种叫作环形缓冲区的方案,mangos的网络代码中也有这么一个东西,其原理也是比较简单的。编程

  就比如两我的围着一张圆形的桌子在追逐,跑的人被网络IO线程所控制,当写入数据时,这我的就往前跑;追的人就是逻辑线程,会一直往前追直到追上跑的人。若是追上了怎么办?那就是没有数据可读了,先等会儿呗,等跑的人向前跑几步了再追,总不能让游戏没得玩了吧。那要是追的人跑的太慢,跑的人转了一圈过来反追上追的人了呢?那您也先歇会儿吧。要是一直这么反着追,估计您就只能换一个跑的更快的追逐者了,要不这游戏还真无法玩下去。设计模式

  前面咱们特别强调了,按照严格的先进先出顺序进行处理,这是环形缓冲区的使用必须遵照的一项要求。也就是,你们都得遵照规定,追的人不能从桌子上跨过去,跑的人固然也不容许反过来跑。至于为何,不须要多作解释了吧。缓存

  环形缓冲区是一项很好的技术,不用频繁的分配内存,并且在大多数状况下,内存的反复使用也使得咱们能用更少的内存块作更多的事。安全

  在网络IO线程中,咱们会为每个链接都准备一个环形缓冲区,用于临时存放接收到的数据,以应付半包及粘包的状况。在解包及解密完成后,咱们会将这个数据包复制到逻辑线程消息队列中,若是咱们只使用一个队列,那这里也将会是个环形缓冲区,IO线程往里写,逻辑线程在后面读,互相追逐。可要是咱们使用了前面介绍的优化方案后,可能这里便再也不须要环形缓冲区了,至少咱们并再也不须要他们是环形的了。由于咱们对同一个队列再也不会出现同时读和写的状况,每一个队列在写满后交给逻辑线程去读,逻辑线程读完后清空队列再交给IO线程去写,一段固定大小的缓冲区便可。不要紧,这么好的技术,在别的地方必定也会用到的。服务器

服务器公共组件实现 -- 发包的方式网络

  前面一直都在说接收数据时的处理方法,咱们应该用专门的IO线程,接收到完整的消息包后加入到主线程的消息队列,可是主线程如何发送数据尚未探讨过。session

  通常来讲最直接的方法就是逻辑线程何时想发数据了就直接调用相关的socket API发送,这要求服务器的玩家对象中保存其链接的socket句柄。可是直接send调用有时候有会存在一些问题,好比遇到系统的发送缓冲区满而阻塞住的状况,或者只发送了一部分数据的状况也时有发生。咱们能够将要发送的数据先缓存一下,这样遇到未发送完的,在逻辑线程的下一次处理时能够接着再发送。负载均衡

  考虑数据缓存的话,那这里这能够有两种实现方式了,一是为每一个玩家准备一个缓冲区,另外就是只有一个全局的缓冲区,要发送的数据加入到全局缓冲区的时候同时要指明这个数据是发到哪一个socket的。若是使用全局缓冲区的话,那咱们能够再进一步,使用一个独立的线程来处理数据发送,相似于逻辑线程对数据的处理方式,这个独立发送线程也维护一个消息队列,逻辑线程要发数据时也只是把数据加入到这个队列中,发送线程循环取包来执行send调用,这时的阻塞也就不会对逻辑线程有任何影响了。

  采用第二种方式还能够附带一个优化方案。通常对于广播消息而言,发送给周围玩家的数据都是彻底相同的,咱们若是采用给每一个玩家一个缓冲队列的方式,这个数据包将须要拷贝多份,而采用一个全局发送队列时,咱们只须要把这个消息入队一次,同时指明该消息包是要发送给哪些socket的便可。有关该优化的说明在云风描述其链接服务器实现的blog文章中也有讲到,有兴趣的能够去阅读一下。

服务器公共组件实现 -- 状态机

  有关State模式的设计意图及实现就不从设计模式中摘抄了,咱们只来看看游戏服务器编程中如何使用State设计模式。

  首先仍是从mangos的代码开始看起,咱们注意到登陆服在处理客户端发来的消息时用到了这样一个结构体:

  struct AuthHandler
  {
    eAuthCmd cmd;
    uint32 status;
    bool (AuthSocket::*handler)(void);
  };

  该结构体定义了每一个消息码的处理函数及须要的状态标识,只有当前状态知足要求时才会调用指定的处理函数,不然这个消息码的出现是不合法的。这个status状态标识的定义是一个宏,有两种有效的标识,STATUS_CONNECTED和STATUS_AUTHED,也就是未认证经过和已认证经过。而这个状态标识的改变是在运行时进行的,确切的说是在收到某个消息并正确处理完后改变的。

  咱们再来看看设计模式中对State模式的说明,其中关于State模式适用状况里有一条,当操做中含有庞大的多分支的条件语句,且这些分支依赖于该对象的状态,这个状态一般用一个或多个枚举变量表示。

  描述的状况与咱们这里所要处理的状况是如此的类似,也许咱们能够试一试。那再看看State模式提供的解决方案是怎样的,State模式将每个条件分支放入一个独立的类中。

  因为这里的两个状态标识只区分出了两种状态,因此,咱们仅须要两个独立的类,用以表示两种状态便可。而后,按照State模式的描述,咱们还须要一个Context类,也就是状态机管理类,用以管理当前的状态类。稍做整理,大概的代码会相似这样:

  状态基类接口:
  StateBase
  {
    void Enter() = 0;
    void Leave() = 0;
    void Process(Message* msg) = 0;
  };

  状态机基类接口:
  MachineBase
  {
    void ChangeState(StateBase* state) = 0;

    StateBase* m_curState;
  };

  咱们的逻辑处理类会从MachineBase派生,当取出数据包后交给当前状态处理,前面描述的两个状态类从StateBase派生,每一个状态类只处理该状态标识下须要处理的消息。当要进行状态转换时,调用MachineBase的ChangeState()方法,显示地告诉状态机管理类本身要转到哪个状态。因此,状态类内部须要保存状态机管理类的指针,这个能够在状态类初始化时传入。具体的实现细节就不作过多描述了。

  使用状态机虽然避免了复杂的判断语句,但也引入了新的麻烦。当咱们在进行状态转换时,可能会须要将一些现场数据从老状态对象转移到新状态对象,这须要在定义接口时作一下考虑。若是不但愿执行拷贝,那么这里公有的现场数据也可放到状态机类中,只是这样在使用时可能就不那么优雅了。

  正如同在设计模式中所描述的,全部的模式都是已有问题的另外一种解决方案,也就是说这并非惟一的解决方案。放到咱们今天讨论的State模式中,就拿登陆服所处理的两个状态来讲,也许用mangos所采用的遍历处理函数的方法可能更简单,但当系统中的状态数量增多,状态标识也变多的时候,State模式就显得尤为重要了。

  好比在游戏服务器上玩家的状态管理,还有在实现NPC人工智能时的各类状态管理,这些就留做之后的专题吧。

服务器公共组件 -- 事件与信号

关于这一节,这几天已经打了好几遍草稿,总以为说不清楚,也很差组织这些内容,可是打铁要趁热,为避免热情消退,先整理一点东西放这,好继续下面的主题,之后若是有机会再回来完善吧。本节内容欠考虑,但愿你们多给点意见。

有些相似于QT中的event与signal,我将一些动做请求消息定义为事件,而将状态改变消息定义为信号。好比在QT应用程序中,用户的一次鼠标点击会产生一个鼠标点击事件加入到事件队列中,当处理此事件时可能会致使某个按钮控件产生一个clicked()信号。

对应到咱们的服务器上的一个例子,玩家登陆时会发给服务器一个请求登陆的数据包,服务器可将其看成一个用户登陆事件,该事件处理完后可能会产生一个用户已登陆信号。

这样,与QT相似,对于事件咱们能够重定义其处理方法,甚至过滤掉某些事件使其不被处理,但对于信号咱们只是收到了一个通知,有些相似于Observe模式中的观察者,当收到更新通知时,咱们只能更新本身的状态,对刚刚发生的事件我不已不能作任何影响。

仔细来看,事件与信号其实并没有多大差异,从咱们对其需求上来讲,都只要能注册事件或信号响应函数,在事件或信号产生时可以被通知到便可。但有一项区别在于,事件处理函数的返回值是有意义的,咱们要根据这个返回值来肯定是否还要继续事件的处理,好比在QT中,事件处理函数若是返回true,则这个事件处理已完成,QApplication会接着处理下一个事件,而若是返回false,那么事件分派函数会继续向上寻找下一个能够处理该事件的注册方法。信号处理函数的返回值对信号分派器来讲是无心义的。

简单点说,就是咱们能够为事件定义过滤器,使得事件能够被过滤。这一功能需求在游戏服务器上是处处存在的。

关于事件和信号机制的实现,网络上的开源训也比较多,好比FastDelegate,sigslot,boost::signal等,其中sigslot还被Google采用,在libjingle的代码中咱们能够看到他是如何被使用的。

在实现事件和信号机制时或许能够考虑用同一套实现,在前面咱们就分析过,二者惟一的区别仅在于返回值的处理上。

另外还有一个须要咱们关注的问题是事件和信号处理时的优先级问题。在QT中,事件由于都是与窗口相关的,因此事件回调时都是从当前窗口开始,一级一级向上派发,直到有一个窗口返回true,截断了事件的处理为止。对于信号的处理则比较简单,默认是没有顺序的,若是须要明确的顺序,能够在信号注册时显示地指明槽的位置。

在咱们的需求中,由于没有窗口的概念,事件的处理也与信号相似,对注册过的处理器要按某个顺序依次回调,因此优先级的设置功能是须要的。

最后须要咱们考虑的是事件和信号的处理方式。在QT中,事件使用了一个事件队列来维护,若是事件的处理中又产生了新的事件,那么新的事件会加入到队列尾,直到当前事件处理完毕后,QApplication再去队列头取下一个事件来处理。而信号的处理方式有些不一样,信号处理是当即回调的,也就是一个信号产生后,他上面所注册的全部槽都会当即被回调。这样就会产生一个递归调用的问题,好比某个信号处理器中又产生了一个信号,会使得信号的处理像一棵树同样的展开。咱们须要注意的一个很重要的问题是会不会引发循环调用。

关于事件机制的考虑其实还不少,但都是一些不成熟的想法。在上面的文字中就同时出现了消息、事件和信号三个相近的概念,而在实际处理中,常常发现三者不知道如何界定的状况,实际的状况比我在这里描述的要混乱的多。

这里也就当是挖下一个坑,但愿可以有所交流。

再谈登陆服的实现

    离咱们的登陆服实现已经太远了,先拉回来一下。
   
    关于登陆服、大区服及游戏世界服的结构以前已作过探讨,这里再把各自的职责和关系列一下。

        GateWay/WorldServer   GateWay/WodlServer LoginServer LoginServer DNSServer WorldServerMgr
                |                     |                     |                 |            |
      ---------------------------------------------------------------------------------------------
                                             | | |
                                             internet
                                                |
                                              clients

    其中DNSServer负责带负载均衡的域名解析服务,返回LoginServer的IP地址给客户端。WorldServerMgr维护当前大区内的世界服列表,LoginServer会从这里取世界列表发给客户端。LoginServer处理玩家的登陆及世界服选择请求。GateWay/WorldServer为各个独立的世界服或者经过网关链接到后面的世界服。

    在mangos的代码中,咱们注意到登陆服是从数据库中取的世界列表,而在wow官方服务器中,咱们却会注意到,这个世界服列表并非一开始就固定,而是动态生成的。当每周一次的维护完成以后,咱们能够很明显的看到这个列表生成的过程。刚开始时,世界列表是空的,慢慢的,世界服会一个个加入进来,而这里若是有世界服当机,他会显示为离线,不会从列表中删除。可是当下一次服务器再维护后,全部的世界服都不存在了,所有从新开始添加。

    从上面的过程描述中,咱们很容易想到利用一个临时的列表来保存世界服信息,这也是咱们增长WorldServerMgr服务器的目的所在。GateWay/WorldServer在启动时会自动向WorldServerMgr注册本身,这样就把本身所表明的游戏世界添加到世界列表中了。相似的,若是DNSServer也可让LoginServer本身去注册,这样在临时LoginServer时就不须要去改动DNSServer的配置文件了。

    WorldServerMgr内部的实现很简单,监听一个固定的端口,接受来自WorldServer的主动链接,并检测其状态。这里能够用一个心跳包来实现其状态的检测,若是WorldServer的链接断开或者在规定时间内未收到心跳包,则将其状态更新为离线。另外WorldServerMgr还处理来自LoginServer的列表请求。因为世界列表并不常变化,因此LoginServer没有必要每次发送世界列表时都到WorldServerMgr上去取,LoginServer彻底能够本身维护一个列表,当WorldServerMgr上的列表发生变化时,WorldServerMgr会主动通知全部的LoginServer也更新一下本身的列表。这个或许就能够用前面描述过的事件方式,或者就是观察者模式了。

    WorldServerMgr实现所要考虑的内容就这些,咱们再来看看LoginServer,这才是咱们今天要重点讨论的对象。

    前面探讨一些服务器公共组件,那咱们这里也应该试用一下,不能只是停留在理论上。先从状态机开始,前面也说过了,登陆服上的链接会有两种状态,一是账号密码验证状态,一是服务器列表选择状态,其实还有另一个状态咱们不曾讨论过,由于它与咱们的登陆过程并没有多大关系,这就是升级包发送状态。三个状态的转换流程大体为:

        LogonState -- 验证成功 -- 版本检查 -- 版本低于最新值 -- 转到UpdateState
                                          |
                                           -- 版本等于最新值 -- 转到WorldState

    这个版本检查的和决定下一个状态的过程是在LogonState中进行的,下一个状态的选择是由当前状态来决定。密码验证的过程使用了SRP6协议,具体过程就很少作描述,每一个游戏使用的方式也都不大同样。而版本检查的过程就更无值得探讨的东西,一个if-else便可。

    升级状态其实就是文件传输过程,文件发送完毕后通知客户端开始执行升级文件并关闭链接。世界选择状态则提供了一个列表给客户端,其中包括了全部游戏世界网关服务器的IP、PORT和当前负载状况。若是客户端一直链接着,则该状态会以每5秒一次的频率不停刷新列表给客户端,固然是否值得这样作仍是有待商榷。

    整个过程彷佛都没有值得探讨的内容,可是,尚未完。当客户端选择了一个世界以后该怎么办?wow的作法是,当客户端选择一个游戏世界时,客户端会主动去链接该世界服的IP和PORT,而后进入这个游戏世界。与此同时,与登陆服的链接尚未断开,直到客户端确实链接上了选定的世界服而且走完了排队过程为止。这是一个很必要的设计,保证了咱们在因意外状况链接不上世界服或者发现世界服正在排队而想换另一个试试时不会须要从新进行密码验证。

    可是咱们所要关注的还不是这些,而是客户端去链接游戏世界的网关服时服务器该如何识别咱们。打个比方,有个不自觉的玩家不遵照游戏规则,没有去验证账号密码就直接跑去链接世界服了,就如同一个不自觉的乘客没有换登机牌就直接跑到登机口同样。这时,乘务员会客气地告诉你要先换登机牌,那登机牌又从哪来?检票口换的,人家会先验明你的身份,确认后才会发给你登机牌。同样的处理过程,咱们的登陆服在验明客户端身份后,也会发给客户端一个登机牌,这个登机牌还有一个学名,叫作session key。

    客户端拿着这个session key去世界服网关处就可正确登陆了吗?彷佛仍是有个疑问,他怎么知道我这个key是否是造假的?没办法,中国的假货太多,咱们不得不处处都考虑假货的问题。方法很简单,去找给他登机牌的那个检票员问一下,这张牌是否是他发的不就得了。但是,那么多的LoginServer,要一个个问下来,这效率也过低了,后面排的长队必定会开始叫唤了。那么,LoginServer将这个key存到数据库中,让网关服本身去数据库验证?彷佛也是个可行的方案。

    若是以为这样给数据库带来了太大的压力的话,也能够考虑相似WorldServerMgr的作法,用一个临时的列表来保存,甚至能够将这个列表就保存到WorldServerMgr上,他正好是全区惟一的。这两种方案的本质并没有差异,只是看你愿意将负载放在哪里。而无论在哪里,这个查询的压力都是有点大的,想一想,全区全部玩家呢。因此,咱们也能够试着考虑一种新的方案,一种不须要去全区惟一一个入口查询的方案。

    那咱们将这些session key分开存储不就得了。一个可行的方案是,让任意时刻只有一个地方保存一个客户端的session key,这个地方多是客户端当前正链接着的服务器,也能够是它正要去链接的服务器。让咱们来详细描述一下这个过程,客户端在LoginServer上验证经过时,LoginServer为其生成了本次会话的session key,但只是保存在当前的LoginServer上,不会存数据库,也不会发送给WorldServerMgr。若是客户端这时想要去某个游戏世界,那么他必须先通知当前链接的LoginServer要去的服务器地址,LoginServer将session key安全转移给目标服务器,转移的意思是要确保目标服务器收到了session key,本地保存的要删除掉。转移成功后LoginServer通知客户端再去链接目标服务器,这时目标服务器在验证session key合法性的时候就不须要去别处查询了,只在本地保存的session key列表中查询便可。

    固然了,为了session key的安全,全部的服务器在收到一个新的session key后都会为其设一个有效期,在有效期事后还没来认证的,则该session key会被自动删除。同时,全部服务器上的session key在链接关闭后必定会被删除,保证一个session key真正只为一次链接会话服务。

    可是,很显然的,wow并无采用这种方案,由于客户端在选择世界服时并无向服务器发送要求确认的消息。wow中的session key应该是保存在一个相似于WorldServerMgr的地方,或者如mangos同样,就是保存在了数据库中。无论是怎样一种方式,了解了其过程,代码实现都是比较简单的,咱们就再也不赘述了。

    有关登陆服的讨论或许该告一段落了吧。

相关文章
相关标签/搜索