1。服务器架构
2。通信模块架构及技术难点
3。服务器程序框架
4。数据存储及DB接口
5。服务器逻辑
6。相关工具,patch,gmtools,logtools,monitor
《快乐西游》http://joyxy.the9.com 的服务器框架
Client <--> GameServer <--> GroupServer <--> Database
(可惜这里不能贴图,只能简单的文字说明,但愿不要引起误解)
客户经过验证直接连线到游戏服务器(一个游戏服务器称为一线),5-10个GameServer形成一组,受一个GroupServer管理,加上一个
帐号服务器,形成一个区。一个完整的区包含10(GameServer)+1(GroupServer)+1(AccountServer)
+1(DB)共13台机器,能负载5000-10000人同时在线。
这种框架的典型特点是区与区之间帐号不通用,线与线之间世界是独立的世界,即你在一线的武器铺,我在二线的武器铺我们是看不到对方的,我们处于一个完全
相同的世界副本中。
九城几年前代理的游戏《奇迹》也是这种架构,只是它更多一个层次,叫做服务器组。
从程序的角度来看就是 一个游戏世界由一个进程来维持。
《魔兽世界》http://wowchina.com 的服务器框架
Client <-->GateServer <--> GameServer <--> Database
客户经过验证之后连线到GateServer,GateServer再连接GameServer,一个游戏世界由多个GameServer支撑,可能这
个GameServer管几个地图,那个GameServer管另外几个地图,
另外一个GameServer则负责副本地图等等,这就是为什么有时候铁炉堡的人集体掉线但是达纳苏斯的都没事,或者MC的集体掉线而在燃烧平原做任务
的一点影响都没有,那都是因为负责那块地图的GameServer崩溃了。当然崩溃有时候也会引起错误漫延,导致不得不全服(所有
GameServer)重启。
从程序的角度来看就是 一个游戏世界由多个进程来维持。
(注:本人没有真正接触到wow的服务器,仅凭借自己看到的现象来推测服务器的框架,快乐西游服务器是我负责设计的,所以能说的很明确。)
一个世界由单一进程来维持,做起来相对简单,玩家切换世界跟下线之后重新登录基本没有区别,无需处理很多边界问题。问题就在于一个世界容纳的人数有限,
这种即时战斗的rpg,一个进程能支持到1000人算很好了(普通2G的志强CPU的服务器)。
多进程支撑一个世界的好处是游戏世界可以做的很大,容纳很多人一起游戏,提供的交互性更好,但是游戏程序的复杂性也会大很多。玩家角色在两个服务器负责
的边界区域来回游走或者交易时,同步问题比较复杂。曾有过游戏(忘记了是什么游戏了)就在这个同步上出了问题导致大量物品复制。当然也不只是地图的管理
在不同的进程,有的游戏还把不同的逻辑运算分散到不同的进程,比如游走处理放在一个进程,任务处理放在另外一个进程等,这样同样是存在公用数据的同步问
题。Wow可能就用了这种,因为在wow中曾碰到过一个任务交了以后领到任务物品,服务器重启后再上来任务可以再交一次领到同样的任务物品,当然也可能
只是任务和数据存储方面的bug。
因为多进程维持一个世界能给玩家带来更好的游戏体验,现在是越来越流行,新的游戏几乎都是这种方式。
《快乐西游》的网络通信模块完全是我手工打造的。
据我所知,windows平台下最高效的网络模型是IOCP即完成端口,linux平台下则是poll.
因为日常的编程是在windows平台上用visual studio完成的,而目标运行平台是linux,所以我提供的网络通信库两个平台都能跑。因
为一开始就定了运行平台是linux,因此没有深入研究iocp,用select简单的实现了一下,程序员自己调试时能正常使用就行了。
当然也有很多借用第三方通信库的,比如ACE等。想想当初自己为了实现windows和linux两个平台都能跑,做了很多#ifdef #else,
如果用ACE就完全不用管这些了,ACE把这些麻烦事都做掉了。ACE还宣称虽然跨平台,但是几乎无损效率。在一般的概念里,要想通用自然就意味着要牺
牲某些效率。不过万事无绝对,那些大牛们搞出来的东西必然有它的优势,并且ACE的历史相当的悠久,比stl的历史悠久,比linux的历史也悠久。我
现在也在了解ACE的相关内容,或许以后做网络通信模块就用它了。
通信模块说简单也简单,无非就是一个接收,一个发送;说复杂也很复杂,很多细枝末节的问题,处理不好都是致命的。
网络游戏的通信有如下一些特点:长时间连接,上行(客户端到服务器)流量基本稳定,下行流量则严重依赖游戏逻辑状况,响应速度要求很高(ping在
150以下才能保证比较好的流畅性)。
一般的web服务,大多一个客户连上来则用一个线程或者进程来专门与之通信,这样简单直接而且有效,客户端断开则线程或者进程结束或者放回池中。因为客
户的连接是相对短期并且客户线程之间基本相互无交互。
基于长时间连接、客户之间交互频繁这两点,网络游戏的处理方法不能这样做,如果为每一个连接开一个线程,当线程数量大到一定程度后线程间的切换与同步带
来的消耗是我们无法接受的。
如果不能为每个客户提供单独的线程来服务,自然就是一个线程服务多个客户了。
最简单的是一个服务所有客户,这样就不存在同步问题了。但是一个线程服务全部客户弊端也是很明显的,客户数量多了以后响应速度会达不到要求,为了保证速
度则不得不限制客户数上限,另外单一线程也无法更好利用到系统资源,服务器一般都是多CPU,至少是超线程的,但一线程就完全浪费了这一优势。
既然One vs all 不好,那就只好 1 vs N或者 M vs N,就是说一个线程固定服务多个客户或者是一组线程(或者说一个线程池)服务
一组客户,不管如何,线程同步问题是无法避免了。这就是网络通信模块的一大技术难点-线程同步。
在《快乐西游》中采用的是简单的一个线程服务固定的一组客户的方式。因为是自己手工打造的,要我自己完成线程调度实现线程池我自觉不可能做得很好,所以
采用了简单的方式。
接下来的问题是选择阻塞模式还是非阻塞模式。
阻塞模式的好处是简单,函数返回事情肯定做完了(返回错误也可以看作是做完了,因为出错处理是统一的)。问题是一旦堵塞则线程被占,这个线程没办法再拿
去做别的事情。这个是难以接受的。为此我在《快乐西游》的网络通信模块中用的是非阻塞模式。
非阻塞模式的好处是不管情况如何函数总能及时返回,线程不会被长期占用,问题是函数返回后,事情可能只做了一部分,对没做掉的部分要做处理,让它下次接
着上次没做完的地方继续做,这样实现起来比较复杂,尤其各种出错情况交织其中的时候。非阻塞模式还有一个问题,就是线程空闲的问题,一旦线程没事情可以
做的时候我们必须让它停下来不要消耗cpu资源,这个也是比较麻烦的事情。有事情的时候要及时唤醒,无事的时候要及时休息,但是如果要用条件变量等方式
唤醒和等待,也不是无代价的,烦啊。我写程序的原则是,尽量往简单里做。所以我用了一个sleep,当线程无事可做的时候sleep 10毫秒,然后检
查是否有事可做,没有再sleep 10毫秒。这样做最大的好处是简单,但是存在两个问题,一是长时间无事可做的时候依然会消耗一点cpu资源(因为是
即时战斗rpg这种情况会比较少出现),另外一个是突然有事情做的时候最多会引入10毫秒的延迟,平均延迟5毫秒。This is a
problem。这是通信模块中的第二个难点,我没有把它解决的很好。
通信模块中的第三个难点是如何减少数据复制。
客户端与服务器之间的数据交换不可能直接用逻辑层的数据结构,因为逻辑运算所用的数据结构往往太大,并且很多运算出来的中间值,网络传递要尽量只传递那
些必不可少的数据,这样就必然存在数据的复制,从逻辑数据结构复制到网络通信数据结构,既然是必然要复制,那也没什么问题。
存在问题的地方是这个通信数据结构从逻辑模块到网络通信模块的时候,因为它们在不同的线程中工作,所以这个数据会在线程间传递,必须保证最终发送时依然
有效。
这样一来,要么逻辑模块传递给网络通信模块时后者把它复制一份,要么逻辑模块自己new出来,将指针交给网络通信模块,并在最终发送完成以后
delete掉。
前一种方法比较符合常规,但是数据要被复制一次;后一种方法则可以省掉复制,但是造成了new和delete的不对称使用,当然我们可以用智能指针等方
式解决,不是太大的问题。后一种方式的另外一个问题是如果大同小异的数据要发往多个客户端时,逻辑层使用起来比较麻烦,要重复new很多次并且复制数据
很多次,不向前一种那一可以用一个数据实例,只需要修改不同的字段,然后用同样的方法交给网络通信模块即可。
是多一次复制让使用方便呢,还是要求更复杂的使用规则而减少一次memcpy呢,取舍很难。
To be or not to be, it's a problem.
网络通信模块的另外一个技术难点是反外挂。
外挂有很多种,按键精灵类的,修改出入封包的,直接修改属性或者参数的,不一而足。
有些是减轻玩家重复劳动的,可以称为良性外挂,更多的破坏游戏平衡性的,甚至会毁了游戏。几乎每个成功的网络游戏都有很多外挂。最夸张的要数石器时
代,RO等游戏的全脱机外挂。完全不用开客户端,只需运行外挂程序,自动做你要做的事情,比人亲自操作还好。
搞笑的是到最后为了查脱机外挂,不得不用gm在游戏里面问玩家问题,不能正常回答的就被认为是脱机外挂,后来外挂制作者根据gm常问的问题提供了自动应
答功能。
外挂能做到这种程度,自然是通信协议被完全破解。
因为客户端程序在玩家机器上执行,所以从理论上讲协议完全不被破解是不可能的。网络游戏的破解与反破解,就像博弈,我们是在不可能胜利的一方。
既然是这样我们要做的是什么,能做的是什么呢。
我们要做的是让自己不要输掉,能做的就是加大破解的难度,提供快速应对的方法。
加大破解难度可以从以下几个方面来做:
A.给程序加壳。如果不能脱掉壳,则无法准确跟踪程序运行的流程,也无法做静态代码分析。
B.通信协议加密。加密的协议能阻挡简单的通过截获网络封包来破解协议的企图。
C.嵌入动态核心代码。即让一些核心代码在运行时从服务器加载,这样能对抗静态代码分析。
D.同时运行监控程序或者别的方式反调试和反hook。如果不能调试或者向客户端注入代码,很难了解程序运行时到底在做什么。
可能还有一些其他的手段,目的无非是加大破解的难度。但是有句话说得好,道高一尺,魔高一丈。这些手段都是能加大难度,但无法从根本上解决问题。难道我
们真的束手无策了么。
换个角度思考问题,会有不同的收获。
为什么会有外挂,为什么会有人来制作外挂?
因为玩家觉得游戏中玩的不爽,需要外挂来帮忙让自己更爽,所以会有外挂。
因为有需求,就有市场,有了市场,自然就有了利益,有了利益,自然就有人来制作外挂。
要想没有外挂,有两种可能,没人使用外挂,或者没人制作。
如果能做到外挂一出,立刻修改协议,让其无法使用,则其无法出售牟利,久而久之制作者会知难而退。如果我们做到破解协议需要两周的时间,而我们每周更换
一次协议,那就等于破解者也无能为力。所以说加大破解难度,做到快速应对,很大程度上是对付外挂制作者的。当然还有非技术的方法,起诉外挂制作者,把他
们抓起来。 :)
这些都是从环节上解决问题,下面说说从根源上解决问题的办法。
我们让玩家在游戏中没有什么不爽的重复劳动,岂不是从根源上解决问题了么。
理论上是,但是游戏制作无法尽善尽美,另外就是有些玩家的欲望是永无止境的,他总想要破坏规则,达到他想要境界。对这样的玩家,就该像法律处理罪犯一
样。
这么说来最好的反外挂方法是两点:一是把游戏做好,让玩家玩得爽;二是做好记录,及时发现破坏规则使用外挂的玩家,立即把它驱逐出游戏(封号,清除角色
等)。
有点跑题了。
对于上面提到的加大难度方法中,网络通信模块要做的是加密通信协议。加密实现起来不难,关键是越是复杂的加密耗用的资源越多,当然破解难度也越大,我们
要在效率和破解难度之间寻找平衡点。另外加密的协议只能对抗封包拦截者。因为加密的封包你自己最终还是要解密的,因为你自己要用。如果在解密之后被截
获,那再强的加密也是白搭。防止加密之前和解密之后被拦截的方法最本质的方法是不要有单点。我们结构化的编程如果提供一个加密函数,编译出来是一段代
码,每次加密都会跳转到此段代码的入口,这就是单点。一旦被人找到,所谓的加密就废了。避免单点的办法是代码都内联和编译打乱。 :)
关于通信模块的具体包装,可参见另外一篇《关于网络游戏通信模块设计》
如下的类是我常用的循环体的简短直观描述。
class Circulator
{
public:
//初始化
virtual int Initialize();
//释放
virtual void Release();
//主循环体
virtual void Run();
//响应网络封包的输入
virtual OnPacket(PACKET* pPacket);
//响应时间的流逝
virtual OnUpdate(unsigned int nElapsed);
//发送封包的函数,用来输出结果
virtual SendPacket(PACKET* pPacket);
}
Initialize()和Release()基本上不用说,只在启动和结束的时候执行一次,用来做一些预备和资源释放的工作。
Run()就是循环体的入口,里面基本上是如下一个样子
void Circulator::Run()
{
while(true)
{
//从网络层获取封包
NetworkWrapper::GetPacket();
//处理之
OnPacket();
if( time_elapsed >= HeartBeat_Interval )
{
//响应时间流逝
OnUpdate(time_elapsed);
}
}
}
接下来说说OnPacket(),这个函数处理所有封包,当然不是做所有的事情,这里仿佛是闸门,闸门后面不可能直接是要灌溉的农田,而是一排整理的渠
道,将出闸的水分流到各个渠道,最终缓缓流入农田。要是闸门后面直接是农田,那么结果就不是灌溉,而是泄洪了。程序也是一样,如果这里直接针对每个网络
消息作处理,这个函数将无比庞大而凌乱,逻辑无法清晰的分开,这对大型的游戏项目来说是灾难性的。
啰嗦了这么多,其实只是想说明OnPacket()只是负责分流工作,将不同的消息交与相关的模块去处理。
其内容大致如下:
void Circulator::OnPacket(PACKET* pPacket)
{
switch(pPacket->GetPacketType())
{
case PACKET_TYPE_Login:
LoginModule.OnPacket(pPacket);
break;
case PACKET_TYPE_Map:
MapModule.OnPacket(pPacket);
break;
case PACKET_TYPE_Mail:
MailModule.OnPacket(pPacket);
break;
...
}
}
这里用的是简单的switch-case,也有人用Command模式,也或者直接把处理函数绑定在Packet中,各有利弊,不一一详述。
既然上面提到了LoginModule和MapModule以及MailModule等,再来说说另外一个比较重要的东西:逻辑模块。我喜欢把一些相关
的逻辑处理组织在一起做成一个个模块,既高效的完成功能,又尽量相互独立,降低耦合。下面给出一个模块的基类的简单描述来勾勒其工作的方式。
class ModuleBase
{
public:
virtual void OnPacket(PACKET* pPacket);
virtual void OnUpdate(unsigned int nElapsed);
void RaiseEvent(EVENT* pEvent);
void OnEvent(EVENT* pEvent);
}
前面两个函数不用细说,一看就知道,就是前面提到的闸门后面的渠道,用来处理分流进来的各种输入。后面一个函数RaiseEvent(),其作用是什么
呢。故名思义,发起事件。我们知道,如果每个模块都能这么简单的包装完全相互独立,那我们干嘛还要用单线程,给每个模块一个线程不是能很大程度上提高处
理能力么。事实是各模块之间不但要公用玩家信息、地图信息、公会信息等等数据,还要相互之间关联,不可能完全独立。但是我也不希望他们之间相互直接引
用,那样会造成头文件包含灾难和逻辑混乱。为此做了EVENT,来提供一种方式让他们可以相互沟通,但不至于耦合。举个例子,玩家登录我们假定是
LoginModule来处理的,但是公会模块也关心玩家上线的事情,可能要为这个玩家准备相关的数据。我们可以直接在LoginModule的
OnLoginOK()里面直接调用GuildModule.OnPlayerLogin(),那如果邮件模块也关心这个事情,那么我们还得加入
MailModule.OnPlayerLogin(),这就是我前面提到的强耦合的方式。用EVENT的方式是在OnLoginOK()
中,RaiseEvent(EVENT_Login),这个函数做什么呢,是把这个事件交给Circulator,让它来处理,Circulator的
处理也很简单,就是根据自己记录的哪些模块关心此事件,就调用它的OnEvent方法来让其有机会响应。这里用到了一个简单的解耦合的方法,也是编程中
常用的方法,解除了各个Module之间的耦合性,各模块只和Circulator之间有耦合,而Circulator是唯一的循环体,和它的耦合是不
可避免的。当然我们可以用同样的注册Packet类型和逻辑模块的方式来降低Circulator和各具体模块之间的耦合,让Circulator只和
ModuleBase之间发生耦合(当然也有个代价问题值得考虑,因为网络封包相当的多,即OnPacket函数的调用相当的频繁,其分流效率一定要
高。很明显switch-case不是一个最高效的方式,但是用来说明服务器程序组织架构很适合:)。
说完了OnPacket,其实OnUpdate也不需要再说了,因为他们的分流方式是一样的,只不过最终的具体模块中他们做的事情不一样而以。如果用抽
象的眼光来看,时间流逝也可以看作是一种输入,它和网络封包这类的输入并没有本质的不同,各模块的功能也都是接受一定的输入,计算之,产生输出。其实输
出也可以把他们统一管理起来。如果输出被其他的方式接管,那么模块的功能就可以归纳为一个词--计算服务。各模块所做的事情就像是一架香肠机,在入口丢
进去一头猪,在出口整齐的摆出香肠。:)和香肠机有所区别的是,模块提供计算服务的时候需要有些环境数据,比如玩家信息、地图信息等。也可以说功能模块
需要数据服务。那正好,我们做一些模块来完成数据服务,不同的数据服务有不同的提供者。比如玩家数据我们可以用一个PlayerMgr来管理并且提供个
各功能模块,地图数据我们可以用MapMgr来完成。
对于PlayerMgr可以是一个player list,也可以是一个map,当然只要能完成所需要的功能,用什么方法是不要紧的,面向对象的方法就
是封装实现细节。我前面有篇文章提到一个小技巧来高效的组织网游服务器中的数据,文章题目是《小技巧,大作用》。
提到数据服务,还有一种数据服务也要说说,那就是数据存储服务。
《快乐西游》的数据库我选择的是MySQL,主要是基于几点:免费、效率高、功能简单够用。严格来讲MySQL不能算是数据库,因为其没有提供事务功
能,只能算是一个提供SQL的文件系统。(新版本的MySQL好像已经实现了事务功能)功能简单可能也正是它高效率的原因。因为它提供了我所需要的所有
功能,够用就好。
在大学毕业之前曾帮一家公司做过MIS(管理信息系统),用过Oraclehe和MS SQL Server。也正是那个时候学到的SQL,学到的数据
库的一些基本知识。当时已经有很好的图形工具来建立数据库和表结构,十分方便直观,但是指导我的一个长者坚持让我用脚本来完成这些工作。我很不解,放着
这么好的可视化工具不用,非要自己去抠一些规则细节来写脚本是何苦?得到的回答是:你以后就明白其中的好处了。是的,很快我就明白了数据库脚本的好处。
移植、迁移、重建时脚本是如此的容易,而图形化的工具则要重复劳动并且无法确保自己不遗漏东西。当然,现在的图形化工具都提供导出脚本的功能了,但是我
依然改不了自己写脚本的习惯。
旧事不提了,言归正传。网络游戏的数据相对于大型企业的数据来讲简直就是小儿科,表结构简单,需要的查询功能也少,但是有一个重要的要求就是查询频度高
并且要求查询效率高,这些特点也是我选择MySQL的重要原因。
《快乐西游》的数据库接口可以分成两个部分,一个是数据库访问接口,一个是程序的查询管理。前者我采用的MySQL的C语言API实现,封装为一个类,
其核心就是一个ExecuteSQL函数,其实也是对MySQL提供的C语言接口的一些简单的封装,没有太多需要细说的地方。查询管理我也封装了一个名
为QueryMgr的类。
因为查询数据的函数调用不可能即刻返回,主线程又不可能在这里等着(否则什么事都不要做了:),所以需要有个查询管理器来管理这些查询及其返回的结果。
查询管理器有个有个后台线程来等待查询的结果,并将结果还给发起查询请求的主线程,示例代码如下:
class Query //查询管理器与主线程交换信息的数据结构或者说通信协议
{
public:
int nQueryID;//查询ID,用来标志不同的查询
void *pData;//查询附带的数据
void *pTage;//查询附加标志,查询发起者在查询返回时需要的一些发起时的数据区分。
int nResult;//查询结果
}
class QueryMgr
{
public:
int Initialize();//初始化一些东西,并启动后台线程,等待在查询队列上
void AddQuery(Query* pQuery);//向查询队列中加入一个查询
Query* GetQuery();//从查询队列中取出一个等待完成的查询
Query* GetQueryFinished();//从已经完成的查询队列中取出查询,供主线程调用
void FinishQuery(Query* pQuery);//查询完成,加入到完成队列
void ThreadProc()//线程函数
{
while ( bWorking )
{
Query* pQuery = GetQuery();
if( pQuery != NULL)
{
pQuery->nResult = ProcessQuery(pQuery);
FinishQuery(pQuery);
}
}
}
int ProcessQuery(Query* pQuery)//处理查询,也就是执行相关的查询语句并得到结果
{
switch(pQuery->nQueryID)
{
case ....
//执行查询并记录结果
break;
}
}
protected:
ThreadSafeQueue queue_;
ThreadSafeQueue queueFinished_;
}
主线程的OnUpdate()中加入如下一段代码
Query* pQueryFinished(NULL);
while( pQueryFinished= QueryMgr->GetQueryFinished() ) != NULL )
{
switch ( pQueryFinished->nQueryID)
{
...
}
}
大致情况如上。这样做的好处是简单明了,想做什么查询只需要调用QueryMgr->AddQuery()即可,然后定时去调用QueryMgr-
>GetQueryFinished()得到结果处理之。如果需要,可以把用法改的更漂亮一点,把这些switch-case都用回调函数的方式替代。
为Query类添加一个Exec(CallbackFunction func)函数,一个SetQueryMgr(QueryMgr*)函数(如果只
使用一个QueryMgr则可以把QueryMgr做成单体,这样SetQueryMgr()函数都不需要了)。Exec()函数调用QueryMgr
的AddQuery()把自己和自己的回调函数加入到对列中(QueryMgr也需要做相应的修改支持回调函数功能),当完成查询之后调用该回调函数执
行后续的功能。
《快乐西游》中的GameServer直接使用我上面提供的接口访问数据库,因为有多个GameServer可能同时访问同一个数据库,这中间就需要有
个同步问题:即数据更新后可能在没有被写入数据库之前,数据库又被读取了。同步的问题在测试之初并没有解决,所以当时曾出现过因此引起物品复制问题(同
步实现之后就不再有此引起的复制问题,更多的物品复制问题源自于程序逻辑漏洞而非数据库接口问题)。
为了解决数据库同步问题,曾想到过一个方案:在GameServer和Database之间添加一个中间层。所有的数据访问由这个中间层来执
行,GameServer只和这个中间层来交换数据,就算数据的修改没有及时提交给数据库,但是中间层内的数据已经被更新。其实这种方式也是有同步问题
的,只是不容易显现而已。其实此方案最大的好处是能统一组织数据,提供高效率的数据访问,减少数据库读写次数。
先来说说GM 工具
GM工具本身也是一个小的管理信息系统(MIS),有用户管理、数据修改流程、操作日志等等,数据最终都保存在数据库中。GM工具本身的逻辑相对简单,
这里不细说,只谈谈实现采用的结构。这个MIS可以采用c/s结构也可以采用b/s结构,《奇迹》采用的更简单的方式:tool程序直接访问数据库,每
个GM启动自己的程序连接到需要操作的数据库中。《快乐西游》的则采用的是b/s结构,GM通过浏览器连接服务器,服务器上的服务程序来负责完成数据库
的访问。b/s的好处是GM工具的更新不需要更新每个人的客户端而只需要更新一下服务器程序就好了,并且安全性也相对较高。《快乐西游》的GM工具服务
器采用的是jsp,好处前面已经说了,弊端就是因为游戏服务器采用的是C++,所以数据结构没法直接通用,另外由于游戏数据结构本身设计的一些缺陷,导
致有些数据结构要在java语言中实现相关的数据解释和分析,如果直接使用C++语言实现,就可以直接拿游戏服务器的相关代码来使用。鉴于GM工具的使
用者数量相对较少,并且安全性也比较容易控制,所以b/s的优势并不十分明显。到底采用c/s还是b/s,It is a problem.
CS工具主要功能包括:发送游戏公告,响应玩家的呼叫接受玩家的抱怨,在线回答玩家的问题,直接解决玩家碰到的常见问题(人物卡住等),警告不遵守游戏
规则的玩家等等。这些功能在有些游戏中是和GM工具集成在一起的。这些功能本身实现起来也没有多大难度,也没什么好细说的。值得一说的也是一个结构问
题。为了均衡负载又能提高单个客服人员的服务对象数量,最好是采用c/s结构(因为这里没有太多数据结构转换问题,主要是一些聊天对话和一些简单指令的
发送,b/s结构也是不错的选择)。
直接使用游戏客户端+特殊的帐号并将部分的功能做在游戏服务器中也是较常见的做法,只是这样做统筹协调服务人员和服务压力的能力比较低,压力大的服务器
可能一个服务人员不够,而压力小的服务器一个服务人员又不够,并且绝大部分玩家的问题都是雷同的,同样的问题交给不同的服务人员来回复即存在重复劳动也
可能因为回复的不一致而降低服务质量。用c/s结构将玩家请求集合起来经过简单处理再分配给不同的客服人员,这样能提高客服人员的工作效率也能提高客服
人员的利用率。
《快乐西游》的Patch方式是把每个版本的补丁打成一个包,玩家每次升级的时候下载然后解开即可。实现起来简单明了,对经常玩游戏的玩家来说基本没什
么问题,只是那些不时常玩游戏的人如果一次更新多个版本,则可能因为多个补丁包中包含同样的文件(比如游戏主程序)而无谓的下载多次,浪费时间。
后来经过考虑,觉得有个更好的方式,试述如下:补丁服务器上保留一个最新的完整的客户端版本,任何玩家上来,对比所有文件,哪个需要更新就下载哪个,这
样不管经过了多少次更新,玩家总能一次得到最新的版本。为了减少下载量,可以将每个文件分别压缩。为了减少每个玩家比较文件花费的时间,可以做一个文件
列表,列表中存放所有文件的md5校验码,只需要比较文件列表的内容即可知道是否需要更新了。可能有些项目文件比较多,文件列表的内容也不少,为了进一
步减少日常登录时比较版本的时间,可以对文件列表再做一个md5校验,每次首先比较这个32字节的内容即可知道文件列表内容有没有发生变化,如果有变化
再下载文件列表来比较并处理之,这样就两全其美了。
另外因为很多玩家都是在网吧玩网络游戏,网吧的机器大都有还原系统,玩家无法自己更新,那么让网吧管理员一个机器一个机器的从网站下载更新游戏比较烦,
而且有些玩家为了避免版本更新的下载高峰,希望提前得到手动补丁,所以最好能提前提供手动补丁给玩家下载。另外一个减小下载压力的方式是使用bt,让已
经下载的玩家分担下载压力。
自动patch程序可能自身也需要更新自己,《快乐西游》的patch程序实现的方式是先检查自己的版本,如果自己需要更新则把自己复制到另外的目录然
后运新之,之后再下载新版本覆盖掉原位置的自己,然后关掉自己运行新下载的版本。其实很多网络游戏都有一个加载程序(launch),可以利用
launch来检查patch程序的版本并更新之,这样就避免了些许问题并且利于调试patch程序。
Launch程序往往实现的功能是显示各组服务器及其连接速度,让玩家选择自己喜爱的服务器后启动游戏程序。主要是一个Ping各服务器然后显示
ping值的功能。
手动补丁包的功能无非就是把自身包含的新文件解压缩到安装目录,制作时可以把需要更新的内容压缩打包作为程序的资源编译在可执行文件中。在生成新的手动
补丁包的时候可以采用替换旧补丁包资源文件的方式来完成,避免每次都要编译手动补丁程序,也便于自动化生成手动补丁程序。
还有个工具不是每个游戏都有提供,就是修复工具,可能有些文件损坏后无法正常游戏,而重新下载安装客户端的代价又太大,repair工具是个很体贴玩家
的做法。如果按照我上面改进后的patch方式,要实现repair工具十分简单,逐个比较文件是否校验码相同,不同则下载更新之。
制作游戏补丁包是一个需要很细心的活。《快乐西游》的补丁就出过些小小问题,比如更新之后玩家的默认选区发生了变化等,虽然无伤大雅但也不应该。我为了
方便补丁的制作,避免人的疏忽造成不必要的麻烦,专门作了一个工具,比较两个目录下的所有文件,将不同的部分找出,并打包到一个zip文件中,然后自动
根据当前的补丁版本号生成新的补丁配置文件等,使用起来还算方便。近来做的新游戏中本来想再次用它,结果发现源代码找不到了,只好重新做一个。结合新的
需求,此次的工具在制作patch方面功能更强大,能自动将补丁文件集合起来打成一个包并生成校验码便于传输,另外还能自动生成手动补丁程序(当然需要
实现做一个手动补丁程序的模板)。
该准备准备休息了,明天还要上班,to be continued ...
说了这么多log生成的事情,再来说log分析的事。其实如果log生成的好,分析自然简单,无非是些索引啊查找啊数据比对之类的东西,没有太多太难的
东西。只是因为log分析涉及大量文件的读取,大量数据的查找,对算法的效率要求比较高,不然慢的像蜗牛的话那些log组的GM会哭的。:) 《天外》
又叫做MO的(九城代理的一款回合制网游)log分析工具是b/s结构的,估计其log是记录在数据库中的,也或者是分析工具做得不好,分析的时候慢的
不得了。
接下来说说服务器监控工具。
服务器在线上运行的时候,需要有人时刻监控其状态。是否运行正常?是否cpu负荷太高?是否内存吃紧?是否延迟太厉害?不可能每个服务器都开个控制台打
top在那里跑着。因此需要有相关的监控工具。
《快乐西游》的服务器监控工具是我独立开发的。包括两个部分,一个是每台服务器上都跑一个Agent程序,另外一部分是跑在监控机上的Monitor程
序。Agent实际上是一个TCP服务器,接受Monitor的连接,并在Monitor要求的时候把自己收集的服务器数据(包括cpu占用情况、内存
占用情况、服务器程序的运行情况、在线玩家数等)发送给Monitor。Monitor则负责定时采集这些信息来显示在屏幕上,还能设定一些警戒线,当
服务器状态出现问题时及时报警引起值班人员的注意。Monitor提供一些常用的管理服务器的功能,比如关闭、重启服务器程序、发送公告等等,当然这些
都是通过Agent来完成的。《快乐西游》服务器程序的稳定性比较差,有时经常发生崩溃,但是基本上总是能在很短的时间就重新起来,这都是Agent自
动完成的。如果要手工操作的话,负责维护的技术人员只怕要天天失眠了。就算是这样,我还是多次在凌晨2点被电话吵醒。因为总有些意外事情是监控程序和监
控人员都搞不定的。问题如果能完全被预计到那也就不成为问题了。
最后来说说数据备份的问题。
数据备份很重要,无需多言。网络游戏的备份主要是两部分,数据库备份和log文件备份。不同的数据库有不同的备份方法。《快乐西游》采用的是主从数据库
的方式,可以任意备份(即任意时刻都可以对从数据库进行完整备份而不影响游戏运行),需要的时候跑一下脚本就好。《快乐西游》的log文件是产生在游戏
服务器上的,每次停机维护的时候会被拷贝集中到一个磁盘整列中以便分析,3个月以上的数据会被压缩备份到其他的设备中。其中涉及的工具其实就是一些脚
本。脚本其实能干很多事情。linux操作系统提供了丰富的零散的功能,脚本就像胶水一样,把它们按照我们想要的方式粘起来,完成我们需要的组合功能。
大型的集成工具的好处是功能多使用方便,缺点是往往用户无法扩展;脚本组合细小功能的好处是灵活,想要做什么基本上都可以办到。程序员都该掌握一门脚本
语言。