mangos服务器架构.docx

上传人:b****6 文档编号:4323982 上传时间:2022-11-29 格式:DOCX 页数:11 大小:28.22KB
下载 相关 举报
mangos服务器架构.docx_第1页
第1页 / 共11页
mangos服务器架构.docx_第2页
第2页 / 共11页
mangos服务器架构.docx_第3页
第3页 / 共11页
mangos服务器架构.docx_第4页
第4页 / 共11页
mangos服务器架构.docx_第5页
第5页 / 共11页
点击查看更多>>
下载资源
资源描述

mangos服务器架构.docx

《mangos服务器架构.docx》由会员分享,可在线阅读,更多相关《mangos服务器架构.docx(11页珍藏版)》请在冰豆网上搜索。

mangos服务器架构.docx

mangos服务器架构

登录服的设计--功能需求

正如我们在前面曾讨论过的,登录服要实现的功能相当简单,就是帐号验证。

为了便于描述,我们暂不引入那些讨论过的优化手段,先以最简单的方式实现,另外也将基本以mangos的代码作为参考来进行描述。

想象一下帐号验证的实现方法,最容易的那就是把用户输入的明文用帐号和密码直接发给登录服,服务器根据帐号从数据库中取出密码,与用户输入的密码相比较。

这个方法存在的安全隐患实在太大,明文的密码传输太容易被截获了。

那我们试着在传输之前先加一下密,为了服务器能进行密码比较,我们应该采用一个可逆的加密算法,在服务器端把这个加密后的字串还原为原始的明文密码,然后与数据库密码进行比较。

既然是一个可逆的过程,那外挂制作者总有办法知道我们的加密过程,所以,这个方法仍不够安全。

哦,如果我们只是希望密码不可能被还原出来,那还不容易吗,使用一个不可逆的散列算法就行了。

用户在登录时发送给服务器的是明文的帐号和经散列后的不可逆密码串,服务器取出密码后也用同样的算法进行散列后再进行比较。

比如,我们就用使用最广泛的md5算法吧。

噢,不要管那个王小云的什么论文,如果我真有那么好的运气,早中500w了,还用在这考虑该死的服务器设计吗?

似乎是一个很完美的方案,外挂制作者再也偷不到我们的密码了。

慢着,外挂偷密码的目的是什么?

是为了能用我们的帐号进游戏!

如果我们总是用一种固定的算法来对密码做散列,那外挂只需要记住这个散列后的字串就行了,用这个做密码就可以成功登录。

嗯,这个问题好解决,我们不要用固定的算法进行散列就是了。

只是,问题在于服务器与客户端采用的散列算法得出的字串必须是相同的,或者是可验证其是否匹配的。

很幸运的是,伟大的数学字们早就为我们准备好了很多优秀的这类算法,而且经理论和实践都证明他们也确实是足够安全的。

这其中之一是一个叫做SRP的算法,全称叫做SecureRemotePassword,即安全远程密码。

wow使用的是第6版,也就是SRP6算法。

有关其中的数学证明,如果有人能向我解释清楚,并能让我真正弄明白的话,我将非常感激。

不过其代码实现步骤倒是并不复杂,mangos中的代码也还算清晰,我们也不再赘述。

登录服除了帐号验证外还得提供另一项功能,就是在玩家的帐号验证成功后返回给他一个服务器列表让他去选择。

这个列表的状态要定时刷新,可能有新的游戏世界开放了,也可能有些游戏世界非常不幸地停止运转了,这些状态的变化都要尽可能及时地让玩家知道。

不管发生了什么事,用户都有权利知道,特别是对于付过费的用户来说,我们不该藏着掖着,不是吗?

这个游戏世界列表的功能将由大区服来提供,具体的结构我们在之前也描述过,这里暂不做讨论。

登录服将从大区服上获取到的游戏世界列表发给已验证通过的客户端即可。

好了,登录服要实现的功能就这些,很简单,是吧。

确实是太简单了,不过简单的结构正好更适合我们来看一看游戏服务器内部的模块结构,以及一些服务器共有组件的实现方法。

这就留作下一篇吧。

服务器公共组件实现--mangos的游戏主循环

当阅读一项工程的源码时,我们大概会选择从main函数开始,而当开始一项新的工程时,第一个写下的函数大多也是main。

那我们就先来看看,游戏服务器代码实现中,main函数都做了些什么。

由于我在读技术文章时最不喜看到的就是大段大段的代码,特别是那些直接Ctrl+C再Ctrl+V后未做任何修改的代码,用句时髦的话说,一点技术含量都没有!

所以在我们今后所要讨论的内容中,尽量会避免出现直接的代码,在有些地方确实需要代码来表述时,也将会选择使用伪码。

先从mangos的登录服代码开始。

mangos的登录服是一个单线程的结构,虽然在数据库连接中可以开启一个独立的线程,但这个线程也只是对无返回结果的执行类SQL做缓冲,而对需要有返回结果的查询类SQL还是在主逻辑线程中阻塞调用的。

登录服中唯一的这一个线程,也就是主循环线程对监听的socket做select操作,为每个连接进来的客户端读取其上的数据并立即进行处理,直到服务器收到SIGABRT或SIGBREAK信号时结束。

所以,mangos登录服主循环的逻辑,也包括后面游戏服的逻辑,主循环的关键代码其实是在SocketHandler中,也就是那个Select函数中。

检查所有的连接,对新到来的连接调用OnAccept方法,有数据到来的连接则调用OnRead方法,然后socket处理器自己定义对接收到的数据如何处理。

很简单的结构,也比较容易理解。

 

只是,在对性能要求比较高的服务器上,select一般不会是最好的选择。

如果我们使用windows平台,那IOCP将是首选;如果是linux,epool将是不二选择。

我们也不打算讨论基于IOCP或是基于epool的服务器实现,如果仅仅只是要实现服务器功能,很简单的几个API调用即可,而且网上已有很多好的教程;如果是要做一个成熟的网络服务器产品,不是我几篇简单的技术介绍文章所能达到。

另外,在服务器实现上,网络IO与逻辑处理一般会放在不同的线程中,以免耗时较长的IO过程阻塞住了需要立即反应的游戏逻辑。

数据库的处理也类似,会使用异步的方式,也是避免耗时的查询过程将游戏服务器主循环阻塞住。

想象一下,因某个玩家上线而发起的一次数据库查询操作导致服务器内所有在线玩家都卡住不动将是多么恐怖的一件事!

另外还有一些如事件、脚本、消息队列、状态机、日志和异常处理等公共组件,我们也会在接下来的时间里进行探讨。

服务器公共组件实现--继续来说主循环

前面我们只简单了解了下mangos登录服的程序结构,也发现了一些不足之处,现在我们就来看看如何提供一个更好的方案。

正如我们曾讨论过的,为了游戏主逻辑循环的流畅运行,所有比较耗时的IO操作都会分享到单独的线程中去做,如网络IO,数据库IO和日志IO等。

当然,也有把这些分享到单独的进程中去做的。

另外对于大多数服务器程序来说,在运行时都是作为精灵进程或服务进程的,所以我们并不需要服务器能够处理控制台用户输入,我们所要处理的数据来源都来自网络。

这样,主逻辑循环所要做的就是不停要取消息包来处理,当然这些消息包不仅有来自客户端的玩家操作数据包,也有来自GM服务器的管理命令,还包括来自数据库查询线程的返回结果消息包。

这个循环将一直持续,直到收到一个通知服务器关闭的消息包。

主逻辑循环的结构还是很简单的,复杂的部分都在如何处理这些消息包的逻辑上。

我们可以用一段简单的伪码来描述这个循环过程:

while(Message*msg=getMessage())

{

if(msg为服务器关闭消息)

break;

处理msg消息;

}

这里就有一个问题需要探讨了,在getMessage()的时候,我们应该去哪里取消息?

前面我们考虑过,至少会有三个消息来源,而我们还讨论过,这些消息源的IO操作都是在独立的线程中进行的,我们这里的主线程不应该直接去那几处消息源进行阻塞式的IO操作。

很简单,让那些独立的IO线程在接收完数据后自己送过来就是了。

好比是,我这里提供了一个仓库,有很多的供货商,他们有货要给我的时候只需要交到仓库,然后我再到仓库去取就是了,这个仓库也就是消息队列。

消息队列是一个普通的队列实现,当然必须要提供多线程互斥访问的安全性支持,其基本的接口定义大概类似这样:

IMessageQueue

{

voidputMessage(Message*);

Message*getMessage();

}

网络IO,数据库IO线程把整理好的消息包都加入到主逻辑循环线程的这个消息队列中便返回。

有关消息队列的实现和线程间消息的传递在ACE中有比较完全的代码实现及描述,还有一些使用示例,是个很好的参考。

这样的话,我们的主循环就很清晰了,从主线程的消息队列中取消息,处理消息,再取下一条消息......

服务器公共组件实现--消息队列

既然说到了消息队列,那我们继续来稍微多聊一点吧。

我们所能想到的最简单的消息队列可能就是使用stl的list来实现了,即消息队列内部维护一个list和一个互斥锁,putMessage时将message加入到队列尾,getMessage时从队列头取一个message返回,同时在getMessage和putMessage之前都要求先获取锁资源。

实现虽然简单,但功能是绝对满足需求的,只是性能上可能稍稍有些不尽如人意。

其最大的问题在频繁的锁竞争上。

对于如何减少锁竞争次数的优化方案,GhostCheng提出了一种。

提供一个队列容器,里面有多个队列,每个队列都可固定存放一定数量的消息。

网络IO线程要给逻辑线程投递消息时,会从队列容器中取一个空队列来使用,直到将该队列填满后再放回容器中换另一个空队列。

而逻辑线程取消息时是从队列容器中取一个有消息的队列来读取,处理完后清空队列再放回到容器中。

这样便使得只有在对队列容器进行操作时才需要加锁,而IO线程和逻辑线程在操作自己当前使用的队列时都不需要加锁,所以锁竞争的机会大大减少了。

这里为每个队列设了个最大消息数,看来好像是打算只有当IO线程写满队列时才会将其放回到容器中换另一个队列。

那这样有时也会出现IO线程未写满一个队列,而逻辑线程又没有数据可处理的情况,特别是当数据量很少时可能会很容易出现。

GhostCheng在他的描述中没有讲到如何解决这种问题,但我们可以先来看看另一个方案。

这个方案与上一个方案基本类似,只是不再提供队列容器,因为在这个方案中只使用了两个队列,arthur在他的一封邮件中描述了这个方案的实现及部分代码。

两个队列,一个给逻辑线程读,一个给IO线程用来写,当逻辑线程读完队列后会将自己的队列与IO线程的队列相调换。

所以,这种方案下加锁的次数会比较多一些,IO线程每次写队列时都要加锁,逻辑线程在调换队列时也需要加锁,但逻辑线程在读队列时是不需要加锁的。

虽然看起来锁的调用次数是比前一种方案要多很多,但实际上大部分锁调用都是不会引起阻塞的,只有在逻辑线程调换队列的那一瞬间可能会使得某个线程阻塞一下。

另外对于锁调用过程本身来说,其开销是完全可以忽略的,我们所不能忍受的仅仅是因为锁调用而引起的阻塞而已。

两种方案都是很优秀的优化方案,但也都是有其适用范围的。

GhostCheng的方案因为提供了多个队列,可以使得多个IO线程可以总工程师的,互不干扰的使用自己的队列,只是还有一个遗留问题我们还不了解其解决方法。

arthur的方案很好的解决了上一个方案遗留的问题,但因为只有一个写队列,所以当想要提供多个IO线程时,线程间互斥地写入数据可能会增大竞争的机会,当然,如果只有一个IO线程那将是非常完美的。

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

消息队列锁调用太频繁的问题算是解决了,另一个让人有些苦恼的大概是这太多的内存分配和释放操作了。

频繁的内存分配不但增加了系统开销,更使得内存碎片不断增多,非常不利于我们的服务器长期稳定运行。

也许我们可以使用内存池,比如SGISTL中附带的小内存分配器。

但是对于这种按照严格的先进先出顺序处理的,块大小并不算小的,而且块大小也并不统一的内存分配情况来说,更多使用的是一种叫做环形缓冲区的方案,mangos的网络代码中也有这么一个东西,其原理也是比较简单的。

就好比两个人围着一张圆形的桌子在追逐,跑的人被网络IO线程所控制,当写入数据时,这个人就往前跑;追的人就是逻辑线程,会一直往前追直到追上跑的人。

如果追上了怎么办?

那就是没有数据可读了,先等会儿呗,等跑的人向前跑几步了再追,总不能让游戏没得玩了吧。

那要是追的人跑的太慢,跑的人转了一圈过来反追上追的人了呢?

那您也先歇会儿吧。

要是一直这么反着追,估计您就只能换一个跑的更快的追逐者了,要不这游戏还真没法玩下去。

前面我们特别强调了,按照严格的先进先出顺序进行处理,这是环形缓冲区的使用必须遵守的一项要求。

也就是,大家都得遵守规定,追的人不能从桌子上跨过去,跑的人当然也不允许反过来跑。

至于为什么,不需要多做解释了吧。

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

在网络IO线程中,我们会为每一个连接都准备一个环形缓冲区,用于临时存放接收到的数据,以应付半包及粘包的情况。

在解包及解密完成后,我们会将这个数据包复制到逻辑线程消息队列中,如果我们只使用一个队列,那这里也将会是个环形缓冲区,IO线程往里写,逻辑线程在后面读,互相追逐。

可要是我们使用了前面介绍的优化方案后,可能这里便不再需要环形缓冲区了,至少我们并不再需要他们是环形的了。

因为我们对同一个队列不再会出现同时读和写的情况,每个队列在写满后交给逻辑线程去读,逻辑线程读完后清空队列再交给IO线程去写,一段固定大小的缓冲区即可。

没关系,这么好的技术,在别的地方一定也会用到的。

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

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

一般来说最直接的方法就是逻辑线程什么时候想发数据了就直接调用相关的socketAPI发送,这要求服务器的玩家对象中保存其连接的socket句柄。

但是直接send调用有时候有会存在一些问题,比如遇到系统的发送缓冲区满而阻塞住的情况,或者只发送了一部分数据的情况也时有发生。

我们可以将要发送的数据先缓存一下,这样遇到未发送完的,在逻辑线程的下一次处理时可以接着再发送。

考虑数据缓存的话,那这里这可以有两种实现方式了,一是为每个玩家准备一个缓冲区,另外就是只有一个全局的缓冲区,要发送的数据加入到全局缓冲区的时候同时要指明这个数据是发到哪个socket的。

如果使用全局缓冲区的话,那我们可以再进一步,使用一个独立的线程来处理数据发送,类似于逻辑线程对数据的处理方式,这个独立发送线程也维护一个消息队列,逻辑线程要发数据时也只是把数据加入到这个队列中,发送线程循环取包来执行send调用,这时的阻塞也就不会对逻辑线程有任何影响了。

采用第二种方式还可以附带一个优化方案。

一般对于广播消息而言,发送给周围玩家的数据都是完全相同的,我们如果采用给每个玩家一个缓冲队列的方式,这个数据包将需要拷贝多份,而采用一个全局发送队列时,我们只需要把这个消息入队一次,同时指明该消息包是要发送给哪些socket的即可。

有关该优化的说明在云风描述其连接服务器实现的blog文章中也有讲到,有兴趣的可以去阅读一下。

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

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

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

structAuthHandler

{

eAuthCmdcmd;

uint32status;

bool(AuthSocket:

:

*handler)(void);

};

该结构体定义了每个消息码的处理函数及需要的状态标识,只有当前状态满足要求时才会调用指定的处理函数,否则这个消息码的出现是不合法的。

这个status状态标识的定义是一个宏,有两种有效的标识,STATUS_CONNECTED和STATUS_AUTHED,也就是未认证通过和已认证通过。

而这个状态标识的改变是在运行时进行的,确切的说是在收到某个消息并正确处理完后改变的。

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

描述的情况与我们这里所要处理的情况是如此的相似,也许我们可以试一试。

那再看看State模式提供的解决方案是怎样的,State模式将每一个条件分支放入一个独立的类中。

由于这里的两个状态标识只区分出了两种状态,所以,我们仅需要两个独立的类,用以表示两种状态即可。

然后,按照State模式的描述,我们还需要一个Context类,也就是状态机管理类,用以管理当前的状态类。

稍作整理,大概的代码会类似这样:

状态基类接口:

StateBase

{

voidEnter()=0;

voidLeave()=0;

voidProcess(Message*msg)=0;

};

状态机基类接口:

MachineBase

{

voidChangeState(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/WorldServerGateWay/WodlServerLoginServerLoginServerDNSServerWorldServerMgr

|||||

---------------------------------------------------------------------------------------------

|||

internet

|

clients

其中DNSServer负责带负载均衡的域名解析服务,返回LoginServer的IP地址给客户端。

WorldServerMgr维护当前大区内的世界服列表,LoginServer会从这里取世界列表发给客户端。

LoginServer处理玩家的登录及世界服选择请求。

GateWay/WorldServer为各个独立的世界服或者通过网关连接到后面的世界服。

在mangos的代码中,我们注意到登录服是从数据库中取的世界列表,而在wow官方服务器中,我们却会注意到,这个世界服列表并不是一开始就固定,而是动态生成的。

当每周一次的维护完成之后,我们可以很明显的看到这个列表生成的过程。

刚开始时,世界列表是空的,慢慢的,世界服会一个个加入进来,而这里如果有世界服当机,他会显示为离线,不会从列表中删除。

但是当下一次服务器再维护后,所有的世界服都不存在了,全部重新开始添加。

从上面的过程描述中,我们很容易想到利用一个临时的列表来保存世界服信息,这也是我们增加WorldServerMgr服务器的目的所在。

GateWay/WorldServer在启动时会自动向WorldServerMgr注册自己,这样就把自己所代表的游戏世界添加到世界列表中了。

类似的,如果DNSServer也可以让LoginServer自己去注册,这样在临时LoginServer时就不需要去改动DNSServer的配置文件了。

展开阅读全文
相关资源
猜你喜欢
相关搜索

当前位置:首页 > 经管营销 > 公共行政管理

copyright@ 2008-2022 冰豆网网站版权所有

经营许可证编号:鄂ICP备2022015515号-1