来自:http://www.libing.net.cn/read.php/1724.htmphp
这里讨论的游戏服务器架构大概是目前国内乃至世界上的网游通用的一种架构了:
http://bbs.gameres.com/showthread.asp?threadid=93775
做者:qinglan
有段时间没有研究技术了,此次正好看到了新版的mangos,较之之前我看的版本有了比较大的完善,因而再次浏览了下他的代码,也借此机会整理下我在游戏服务器开发方面的一些心得,与你们探讨。
另外因为为避免与公司引发一些没必要要的纠纷,我所描述的全都是经过google可以找到的资料,因此也能够认为我下面的内容都是网上所找资料的整理合集。在平时的开发中我也搜索过相关的中文网页,不多有讲游戏服务器相关技术的,你们的讨论主要仍是集中在3D相关技术,因此也但愿我将开始的这几篇文章可以起到抛砖引玉的做用,潜水的兄弟们也都上来透透气。
要描述一项技术或是一个行业,通常都会从其最古老的历史开始提及,我本也想按着这个套路走,无奈本人乃一八零后小辈,没有经历过那些苦涩的却使人羡慕的单机游戏开发,也没有响当当的拿的出手的优秀做品,因此也就只能就我所了解的一些技术作些简单的描述。一来算是敦促本身对知识作个梳理,二来与你们探讨的过程也可以找到我以前学习的不足和理解上的错误,最后呢,有可能的话也跟业内的同行们混个脸熟,哪天要是想换个工做了也好有我的帮忙介绍下。最后的理由有些俗了。
关于游戏开发,正如云风在其blog上所说,游戏项目始终只是个小工程,另外开发时间仍是个很重要的问题,因此软件工程的思想及方法在大部分的游戏公司中并不怎么受欢迎。固然这也只是从我我的一些肤浅的了解所得,可能不够充分。从游戏开发的程序团队的人员构成上也可看出来,基本只能算做是小开发团队。有些工做室性质的开发团队,那就更简单了。
我所了解的早些的开发团队,其成员间没有什么严格的分工,你们凭兴趣自由选择一些模块来负责,完成了再去负责另外一模块,有其余同事的工做须要接手或协助的也会当即转入。因此游戏开发人员基本都是多面手,从网络到数据库,从游戏逻辑到图形图象,每一项都有所了解,并能实际应用。或者说都具备很是强的学习能力,在接手一项新的任务后能在很短的时间内对该领域的技术迅速掌握并消化,并且还能现炒现卖。固然,这也与早期2D游戏的技术要求相对比较简单,游戏逻辑也没有如今这般复杂有关。而更重要的多是,都是被逼出来的吧!:)
好了,闲话少说,下一篇,也就是第一篇了,主题为,服务器结构探讨。
服务器结构探讨 -- 最简单的结构
所谓服务器结构,也就是如何将服务器各部分合理地安排,以实现最初的功能需求。因此,结构本无所谓正确与错误;固然,优秀的结构更有助于系统的搭建,对系统的可扩展性及可维护性也有更大的帮助。
好的结构不是一蹴而就的,并且每一个设计者心中的那把尺都不相同,因此这个优秀结构的定义也就没有定论。在这里,咱们不打算对现有游戏结构作评价,而是试着从头开始搭建一个咱们须要的MMOG结构。
对于一个最简单的游戏服务器来讲,它只须要可以接受来自客户端的链接请求,而后处理客户端在游戏世界中的移动及交互,也即游戏逻辑处理便可。若是咱们把这两项功能集成到一个服务进程中,则最终的结构很简单:
client ----- server
嗯,太简单了点,这样也敢叫服务器结构?好吧,如今咱们来往里面稍稍加点东西,让它看起来更像是服务器结构一些。
通常来讲,咱们在接入游戏服务器的时候都会要提供一个账号和密码,验证经过后才能进入。关于为何要提供用户名和密码才能进入的问题咱们这里不打算作过多讨论,云风曾对此也提出过相似的疑问,并给出了只用一个标识串就能进入的设想,有兴趣的能够去看看他们的讨论。但无论是采用何种方式进入,照目前看来咱们的服务器起码得提供一个账号验证的功能。
咱们把观察点先集中在一个大区内。在大多数状况下,一个大区内都会有多组游戏服,也就是多个游戏世界可供选择。简单点来实现,咱们彻底能够抛弃这个大区的概念,认为一个大区也就是放在同一个机房的多台服务器组,各服务器组间没有什么关系。这样,咱们可为每组服务器单独配备一台登陆服。最后的结构图应该像这样:
loginServer gameServer
| /
| /
client
该结构下的玩家操做流程为,先选择大区,再选择大区下的某台服务器,即某个游戏世界,点击进入后开始账号验证过程,验证成功则进入了该游戏世界。可是,若是玩家想要切换游戏世界,他只能先退出当前游戏世界,而后进入新的游戏世界从新进行账号验证。
早期的游戏大都采用的是这种结构,有些游戏在实现时采用了一些技术手段使得在切换游戏服时不须要再次验证账号,但总体结构仍是未作改变。
该结构存在一个服务器资源配置的问题。由于登陆服处理的逻辑相对来讲比较简单,就是将玩家提交的账号和密码送到数据库进行验证,和生成会话密钥发送给游戏服和客户端,操做完成后链接就会当即断开,并且玩家在之后的游戏过程当中不会再与登陆服打任何交道。这样处理短链接的过程使得系统在大多数状况下都是比较空闲的,可是在某些时候,因为请求比较密集,好比开新服的时候,登陆服的负载又会比较大,甚至会处理不过来。
另外在实际的游戏运营中,有些游戏世界很火爆,而有些游戏世界却很是冷清,甚至没有多少人玩的状况也是很常见的。因此,咱们可否更合理地配置登陆服资源,使得整个大区内的登陆服能够共享就成了下一步改进的目标。
服务器结构探讨 -- 登陆服的负载均衡
回想一下咱们在玩wow时的操做流程:运行wow.exe进入游戏后,首先就会要求咱们输入用户名和密码进行验证,验证成功后才会出来游戏世界列表,以后是排队进入游戏世界,开始游戏...
能够看到跟前面的描述有个很明显的不一样,那就是要先验证账号再选择游戏世界。这种结构也就使得登陆服不是固定配备给个游戏世界,而是全区共有的。
咱们能够试着从实际需求的角度来考虑一下这个问题。正如咱们以前所描述过的那样,登陆服在大多数状况下都是比较空闲的,也许咱们的一个拥有20个游戏世界的大区仅仅使用10台或更少的登陆服便可知足需求。而当在开新区的时候,或许要配备40台登陆服才能应付那如潮水般涌入的玩家登陆请求。因此,登陆服在设计上应该能知足这种动态增删的需求,咱们能够在任什么时候候为大区增长或减小登陆服的部署。
固然,在这里也不会存在要求添加太多登陆服的状况。仍是拿开新区的状况来讲,即便新增长登陆服知足了玩家登陆的请求,游戏世界服的承载能力依然有限,玩家同样只能在排队系统中等待,或者是进入到游戏世界中致使你们都卡。
另外,当咱们在增长或移除登陆服的时候不该该须要对游戏世界服有所改动,也不会要求重启世界服,固然也不该该要求客户端有什么更新或者修改,一切都是在背后自动完成。
最后,有关数据持久化的问题也在这里考虑一下。通常来讲,使用现有的商业数据库系统比本身手工技术先进要明智得多。咱们须要持久化的数据有玩家的账号及密码,玩家建立的角色相关信息,另外还有一些游戏世界全局共有数据也须要持久化。
好了,需求已经提出来了,如今来考虑如何将其实现。
对于负载均衡来讲,已有了成熟的解决方案。通常最经常使用,也最简单部署的应该是基于DNS的负载均衡系统了,其经过在DNS中为一个域名配置多个IP地址来实现。最新的DNS服务已实现了根据服务器系统状态来实现的动态负载均衡,也就是实现了真正意义上的负载均衡,这样也就有效地解决了当某台登陆服当机后,DNS服务器不能当即作出反应的问题。固然,若是找不到这样的解决方案,本身从头打造一个也并不难。并且,经过DNS来实现的负载均衡已经包含了所作的修改对登陆服及客户端的透明。
而对于数据库的应用,在这种结构下,登陆服及游戏世界服都会须要链接数据库。从数据库服务器的部署上来讲,能够将账号和角色数据都放在一个中心数据库中,也可分为两个不一样的库分别来处理,基到从物理上分到两台不一样的服务器上去也行。
可是对于不一样的游戏世界来讲,其角色及游戏内数据都是互相独立的,因此通常状况下也就为每一个游戏世界单独配备一台数据库服务器,以减轻数据库的压力。因此,总体的服务器结构应该是一个大区有一台账号数据库服务器,全部的登陆服都链接到这里。而每一个游戏世界都有本身的游戏数据库服务器,只容许本游戏世界内的服务器链接。
最后,咱们的服务器结构就像这样:
大区服务器
/ | \
/ | \
登陆服1 登陆服2 世界服1 世界服2
\ | | |
\ | | |
账号数据库 DBS DBS
这里既然讨论到了大区及账号数据库,因此顺带也说一下关于激活大区的概念。wow中一共有八个大区,咱们想要进入某个大区游戏以前,必须到官网上激活这个区,这是为何呢?
通常来讲,在各个大区账号数据库之上还有一个总的账号数据库,咱们能够称它为中心数据库。好比咱们在官网上注册了一个账号,这时账号数据是只保存在中心数据库上的。而当咱们要到一区去建立角色开始游戏的时候,在一区的账号数据库中并无咱们的账号数据,因此,咱们必须先到官网上作一次激活操做。这个激活的过程也就是从中心库上把咱们的账号数据拷贝到所要到的大区账号数据库中。
服务器结构探讨 -- 简单的世界服实现
讨论了这么久咱们一直都尚未进入游戏世界服务器内部,如今就让咱们来窥探一下里面的结构吧。
对于如今大多数MMORPG来讲,游戏服务器要处理的基本逻辑有移动、聊天、技能、物品、任务和生物等,另外还有地图管理与消息广播来对其余高级功能作支撑。如纵队、好友、公会、战场和副本等,这些都是经过基本逻辑功能组合或扩展而成。
在全部这些基础逻辑中,与咱们要讨论的服务器结构关系最紧密的当属地图管理方式。决定了地图的管理方式也就决定了咱们的服务器结构,咱们仍然先从最简单的实现方式开始提及。
回想一下咱们曾战斗过无数个夜晚的暗黑破坏神,整个暗黑的世界被分为了若干个独立的小地图,当咱们在地图间穿越时,通常都要通过一个叫作传送门的装置。世界中有些地图间虽然在地理上是直接相连的,但咱们发现其游戏内部的逻辑倒是彻底隔离的。能够这样认为,一块地图就是一个独立的数据处理单元。
既然如此,咱们就把每块地图都看成是一台独立的服务器,他提供了在这块地图上游戏时的全部逻辑功能,至于内部结构如何划分咱们暂不理会,先把他看成一个黑盒子吧。
当两我的合做作一件事时,咱们能够以对等的关系相互协商着来作,并且通常也都不会有什么问题。当人数增长到三个时,咱们对等的合做关系可能会有些复杂,由于咱们每一个人都同时要与另两我的合做协商。正如俗语所说的那样,三个和尚可能会碰到没水喝的状况。当人数继续增长,状况就变得不那么简单了,咱们得须要一个管理者来对咱们的工做进行分工、协调。游戏的地图服务器之间也是这么回事。
通常来讲,咱们的游戏世界不可能会只有一块或者两块小地图,那瓜熟蒂落的,也就须要一个地图管理者。先称它为游戏世界的中心服务器吧,毕竟是管理者嘛,你们都以它为中心。
中心服务器主要维护一张地图ID到地图服务器地址的映射表。当咱们要进入某张地图时,会从中心服上取得该地图的IP和port告诉客户端,客户端主动去链接,这样进入他想要去的游戏地图。在整个游戏过程当中,客户端始终只会与一台地图服务器保持链接,当要切换地图的时候,在获取到新地图的地址后,会先与当前地图断开链接,再进入新的地图,这样保证玩家数据在服务器上只有一份。
咱们来看看结构图是怎样的:
中心服务器
/ \ \
/ \ \
登陆服 地图1 地图2 地图n
\ | / /
\ | / /
客户端
很简单,不是吗。可是简单并不表示功能上会有什么损失,简单也更不能表示游戏不能赚钱。早期很多游戏也确实采用的就是这种简单结构。
服务器结构探讨 -- 继续世界服
都已经看出来了,这种每切换一次地图就要从新链接服务器的方式实在是不够优雅,并且在实际游戏运营中也发现,地图切换致使的卡号,复制装备等问题很是多,这里彻底就是一个事故多发地段,如何避免这种频繁的链接操做呢?
最直接的方法就是把那个图倒转过来就好了。客户端只须要链接到中心服上,全部到地图服务器的数据都由中心服来转发。很完美的解决方案,不是吗?
这种结构在实际的部署中也遇到了一些挑战。对于通常的MMORPG服务器来讲,单台服务器的承载量平均在2000左右,若是你的服务器很不幸地只能带1000人,不要紧,很多游戏都是如此;若是你的服务器上跑了3000多玩家依然比较流畅,那你能够自豪地告诉你的策划,多设计些大量消耗服务器资源的玩法吧,好比大型国战、公会战争等。
2000人,彷佛咱们的策划朋友们不大愿意接受这个数字。咱们将地图服务器分开来原来也是想将负载分开,以多带些客户端,如今要全部的链接都从中心服上转发,那链接数又遇到单台服务器的可最大承载量的瓶颈了。
这里有必要再解释下这个数字。我知道,有人必定会说,才带2000人,那是你水平不行,我随便写个TCP服务器均可带个五六千链接。问题偏偏在于你是随便写的,而MMORPG的服务器是复杂设计的。若是一个演示socket API用的echo服务器就能知足MMOG服务器的需求,那写服务器该是件多么惬意的事啊。
但咱们所遇到的事实是,服务器收到一个移动包后,要向周围全部人广播,而不是echo服务器那样简单的回应;服务器在收到一个链接断开通知时要向不少人通知玩家退出事件,并将该玩家的资料写入数据库,而不是echo服务器那样什么都不须要作;服务器在收到一个物品使用请求包后要作一系列的逻辑判断以检查玩家有没有做弊;服务器上还启动着不少定时器用来更新游戏世界的各类状态......
其实这么一比较,咱们也看出资源消耗的所在了:服务器上大量的复杂的逻辑处理。再回过头来看看咱们想要实现的结构,咱们既想要有一个惟一的入口,使得客户端不用频繁改变链接,又但愿这个惟一入口的负载不会太大,以至于接受不了多少链接。
仔细看一看这个需求,咱们想要的仅仅只是一台管理链接的服务器,并不打算让他承担太多的游戏逻辑。既然如此,那五六千个链接也还有知足咱们的要求。至少在如今来讲,一个游戏世界内,也就是一组服务器内同时有五六千个在线的玩家仍是件让人很兴奋的事。事实上,在大多数游戏的大部分时间里,这个数字也是很让人眼红的。
什么?你说梦幻、魔兽还有史先生的那个什么征途远不止这么点人了!噢,我说的是大多数,是大多数,不包括那些明星。你知道大陆如今有多少游戏在运营吗?或许你又该说,咱们不应在一开始就把本身的目标定的过低!好吧,咱们仍是先不谈这个。
继续咱们的结构讨论。通常来讲,咱们把这台负责链接管理的服务器称为网关服务器,由于内部的数据都要经过这个网关才能出去,不过从这台服务器提供的功能来看,称其为反向代理服务器可能更合适。咱们也不在这个名字上纠缠了,就按你们通用的叫法,仍是称他为网关服务器吧。
网关以后的结构咱们依然能够采用以前描述的方案,只是,彷佛并无必要为每个地图都开一个独立的监听端口了。咱们能够试着对地图进行一些划分,由一个Master Server来管理一些更小的Zone Server,玩家经过网关链接到Master Server上,而实际与地图有关的逻辑是分派给更小的Zone Server去处理。
最后的结构看起来大概是这样的:
Zone Server Zone Server
\ /
\ /
Master Server Master Server
/ \ /
/ \ /
Gateway Server \ /
| \ \ /
| \ \ /
| Center Server
|
|
Client
服务器结构探讨 -- 最终的结构
若是咱们就此打住,可能立刻就会有人要嗤之以鼻了,就这点古董级的技术也敢出来现。好吧,咱们仍是把以前留下的问题拿出来解决掉吧。
通常来讲,当某一部分能力达不到咱们的要求时,最简单的解决方法就是在此多投入一点资源。既然想要更多的链接数,那就再加一台网关服务器吧。新增长了网关服后须要在大区服上作相应的支持,或者再简单点,有一台主要的网关服,当其负载较高时,主动将新到达的链接重定向到其余网关服上。
而对于游戏服来讲,有一台仍是多台网关服是没有什么区别的。每一个表明客户端玩家的对象内部都保留一个表明其链接的对象,消息广播时要求每一个玩家对象使用本身的链接对象发送数据便可,至于链接是在什么地方,那是彻底透明的。固然,这只是一种简单的实现,也是普通使用的一种方案,若是后期想对消息广播作一些优化的话,那可能才须要多考虑一下。
既然说到了优化,咱们也稍稍考虑一下如今结构下可能采用的优化方案。
首先是当前的Zone Server要作的事情太多了,以致于他都处理不了多少链接。这其中最消耗系统资源的当属生物的AI处理了,尤为是那些复杂的寻路算法,因此咱们能够考虑把这部分AI逻辑独立出来,由一台单独的AI服务器来承担。
而后,咱们能够试着把一些与地图数据无关的公共逻辑放到Master Server上去实现,这样Zone Server上只保留了与地图数据紧密相关的逻辑,如生物管理,玩家移动和状态更新等。
还有聊天处理逻辑,这部分与游戏逻辑没有任何关联,咱们也彻底能够将其独立出来,放到一台单独的聊天服务器上去实现。
最后是数据库了,为了减轻数据库的压力,提升数据请求的响应速度,咱们能够在数据库以前创建一个数据库缓存服务器,将一些经常使用数据缓存在此,服务器与数据库的通讯都要经过这台服务器进行代理。缓存的数据会定时的写入到后台数据库中。
好了,作完这些优化咱们的服务器结构大致也就定的差很少了,暂且也再也不继续深刻,更细化的内容等到各个部分实现的时候再探讨。
比如咱们去看一场晚会,舞台上演员们按着预约的节目单有序地上演着,但这就是整场晚会的所有吗?显然不止,在幕后还有太多太多的人在忙碌着,甚至在晚会前和晚会后都有。咱们的游戏服务器也如此。
在以前描述的部分就如同舞台上的演员,是咱们能直接看到的,幕后的工做人员咱们也来认识一下。
现实中有警察来维护秩序,游戏中也如此,这就是咱们常说的GM。GM能够采用跟普通玩家同样的拉入方式来进入游戏,固然权限会比普通玩家高一些,也能够提供一台GM服务器专门用来处理GM命令,这样能够有更高的安全性,GM服通常接在中心服务器上。
在以时间收费的游戏中,咱们还须要一台计费的服务器,这台服务器通常接在网关服务器上,注册玩家登陆和退出事件以记录玩家的游戏时间。
任何为用户提供服务的地方都会有日志记录,游戏服务器固然也不例外。从记录玩家登陆的时间,地址,机器信息到游戏过程当中的每一项操做均可以做为日志记录下来,以备查错及数据挖掘用。至于搜集玩家机器资料所涉及到的法律问题不是咱们该考虑的。
差很少就这么多了吧,接下来咱们会按照这个大体的结构来详细讨论各部分的实现。
服务器结构探讨 -- 一点杂谈
再强调一下,服务器结构本无所谓好坏,只有是否适合本身。咱们在前面探讨了一些在如今的游戏中见到过的结构,并尽我所知地分析了各自存在的一些问题和能够作的一些改进,但愿其中没有谬误,若是能给你们也带来些启发那天然更好。
忽然发现本身一旦罗嗦起来还真是没完没了。接下来先说说我在开发中遇到过的一些困惑和一基础问题探讨吧,这些问题可能有人与我同样,也曾遇到过,或者正在被困扰中,而所要探讨的这些基础问题向来也是争论比较多的,咱们也不评价其中的好与坏,只作简单的描述。
首先是服务器操做系统,linux与windows之争随处可见,其实在大多数状况下这不是咱们所能决定的,彷佛各大公司也基本都有了本身的传统,如网易的freebsd,腾讯的linux等。若是真有权利去选择的话,选本身最熟悉的吧。
决定了OS也就基本上肯定了网络IO模型,windows上的IOCP和linux下的epool,或者直接使用现有的网络框架,如ACE和asio等,其余还有些商业的网络库在国内的使用好像没有见到,不符合中国国情嘛。:)
而后是网络协议的选择,之前的选择大多倾向于UDP,为了可靠传输通常本身都会在上面实现一层封装,而如今更普通的是直接采用自己就很可靠的TCP,或者TCP与UDP的混用。早期选择UDP的主要缘由仍是带宽限制,如今宽带普通的状况下TCP比UDP多出来的一点点开销与开发的便利性相比已经不算什么了。固然,若是已有了成熟的可靠UDP库,那也能够继续使用着。
还有消息包格式的定义,这个曾在云风的blog上展开过激烈的争论。消息包格式定义包括三段,包长、消息码和包体,争论的焦点在于应该是消息码在前仍是包长在前,咱们也把这个看成是信仰问题吧,有兴趣的去云风的blog上看看,论论。
另外早期有些游戏的包格式定义是以特殊字符做分隔的,这样一个好处是其中某个包出现错误后咱们的游戏还能继续。但实际上,我以为这是彻底没有必要的,真要出现这样的错误,直接断开这个客户端的链接可能更安全。并且,以特殊字符作分隔的消息包定义还加大了一点点网络数据量。
最后是一个纯技术问题,有关socket链接数的最大限制。开始学习网络编程的时候我犯过这样的错误,觉得port的定义为unsigned short,因此想固然的认为服务器的最大链接数为65535,这会是一个硬性的限制。而实际上,一个socket描述符在windows上的定义是unsigned int,所以要有限制那也是四十多亿,放心好了。
在服务器上port是监听用的,想象这样一种状况,web server在80端口上监听,当一个链接到来时,系统会为这个链接分配一个socket句柄,同时与其在80端口上进行通信;当另外一个链接到来时,服务器仍然在80端口与之通讯,只是分配的socket句柄不同。这个socket句柄才是描述每一个链接的惟一标识。按windows网络编程第二版上的说法,这个上限值配置影响。
好了,废话说完了,下一篇,咱们开始进入登陆服的设计吧。
登陆服的设计 -- 功能需求
正如咱们在前面曾讨论过的,登陆服要实现的功能至关简单,就是账号验证。为了便于描述,咱们暂不引入那些讨论过的优化手段,先以最简单的方式实现,另外也将基本以mangos的代码做为参考来进行描述。
想象一下账号验证的实现方法,最容易的那就是把用户输入的明文用账号和密码直接发给登陆服,服务器根据账号从数据库中取出密码,与用户输入的密码相比较。
这个方法存在的安全隐患实在太大,明文的密码传输太容易被截获了。那咱们试着在传输以前先加一下密,为了服务器能进行密码比较,咱们应该采用一个可逆的加密算法,在服务器端把这个加密后的字串还原为原始的明文密码,而后与数据库密码进行比较。既然是一个可逆的过程,那外挂制做者总有办法知道咱们的加密过程,因此,这个方法仍不够安全。
哦,若是咱们只是但愿密码不可能被还原出来,那还不容易吗,使用一个不可逆的散列算法就好了。用户在登陆时发送给服务器的是明文的账号和经散列后的不可逆密码串,服务器取出密码后也用一样的算法进行散列后再进行比较。好比,咱们就用使用最普遍的md5算法吧。噢,不要管那个王小云的什么论文,若是我真有那么好的运气,早中500w了,还用在这考虑该死的服务器设计吗?
彷佛是一个很完美的方案,外挂制做者再也偷不到咱们的密码了。慢着,外挂偷密码的目的是什么?是为了能用咱们的账号进游戏!若是咱们老是用一种固定的算法来对密码作散列,那外挂只须要记住这个散列后的字串就好了,用这个作密码就能够成功登陆。
嗯,这个问题好解决,咱们不要用固定的算法进行散列就是了。只是,问题在于服务器与客户端采用的散列算法得出的字串必须是相同的,或者是可验证其是否匹配的。很幸运的是,伟大的数学字们早就为咱们准备好了不少优秀的这类算法,并且经理论和实践都证实他们也确实是足够安全的。
这其中之一是一个叫作SRP的算法,全称叫作Secure Remote Password,即安全远程密码。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
{
void putMessage(Message*);
Message* getMessage();
}
网络IO,数据库IO线程把整理好的消息包都加入到主逻辑循环线程的这个消息队列中便返回。有关消息队列的实现和线程间消息的传递在ACE中有比较彻底的代码实现及描述,还有一些使用示例,是个很好的参考。
这样的话,咱们的主循环就很清晰了,从主线程的消息队列中取消息,处理消息,再取下一条消息......
服务器公共组件实现 -- 消息队列
既然说到了消息队列,那咱们继续来稍微多聊一点吧。
咱们所能想到的最简单的消息队列可能就是使用stl的list来实现了,即消息队列内部维护一个list和一个互斥锁,putMessage时将message加入到队列尾,getMessage时从队列头取一个message返回,同时在getMessage和putMessage以前都要求先获取锁资源。
实现虽然简单,但功能是绝对知足需求的,只是性能上可能稍稍有些不尽如人意。其最大的问题在频繁的锁竞争上。
对于如何减小锁竞争次数的优化方案,Ghost Cheng提出了一种。提供一个队列容器,里面有多个队列,每一个队列均可固定存放必定数量的消息。网络IO线程要给逻辑线程投递消息时,会从队列容器中取一个空队列来使用,直到将该队列填满后再放回容器中换另外一个空队列。而逻辑线程取消息时是从队列容器中取一个有消息的队列来读取,处理完后清空队列再放回到容器中。
这样便使得只有在对队列容器进行操做时才须要加锁,而IO线程和逻辑线程在操做本身当前使用的队列时都不须要加锁,因此锁竞争的机会大大减小了。
这里为每一个队列设了个最大消息数,看来好像是打算只有当IO线程写满队列时才会将其放回到容器中换另外一个队列。那这样有时也会出现IO线程未写满一个队列,而逻辑线程又没有数据可处理的状况,特别是当数据量不多时可能会很容易出现。Ghost Cheng在他的描述中没有讲到如何解决这种问题,但咱们能够先来看看另外一个方案。
这个方案与上一个方案基本相似,只是再也不提供队列容器,由于在这个方案中只使用了两个队列,arthur在他的一封邮件中描述了这个方案的实现及部分代码。两个队列,一个给逻辑线程读,一个给IO线程用来写,当逻辑线程读完队列后会将本身的队列与IO线程的队列相调换。因此,这种方案下加锁的次数会比较多一些,IO线程每次写队列时都要加锁,逻辑线程在调换队列时也须要加锁,但逻辑线程在读队列时是不须要加锁的。
虽然看起来锁的调用次数是比前一种方案要多不少,但实际上大部分锁调用都是不会引发阻塞的,只有在逻辑线程调换队列的那一瞬间可能会使得某个线程阻塞一下。另外对于锁调用过程自己来讲,其开销是彻底能够忽略的,咱们所不能忍受的仅仅是由于锁调用而引发的阻塞而已。
两种方案都是很优秀的优化方案,但也都是有其适用范围的。Ghost Cheng的方案由于提供了多个队列,可使得多个IO线程能够总工程师的,互不干扰的使用本身的队列,只是还有一个遗留问题咱们还不了解其解决方法。arthur的方案很好的解决了上一个方案遗留的问题,但由于只有一个写队列,因此当想要提供多个IO线程时,线程间互斥地写入数据可能会增大竞争的机会,固然,若是只有一个IO线程那将是很是完美的。linux