游戏服务器编程
本文作者:sodme
本文出处:http://blog.csdn.net/sodme
声明:本文可以不经作者同意任意转载、复制、传播,但任何对本文的引用都请保留作者、出处及本声明信息。谢谢!
常见的网络服务器,基本上是7*24小时运转的,对于网游来说,至少要求服务器要能连续工作一周以上的时间并保证不出现服务器崩溃这样的灾难性事件。事 实上,要求一个服务器在连续的满负荷运转下不出任何异常,要求它设计的近乎完美,这几乎是不太现实的。服务器本身可以出异常(但要尽可能少得出),但是, 服务器本身应该被设计得足以健壮,“小病小灾”打不垮它,这就要求服务器在异常处理方面要下很多功夫。
服务器的异常处理包括的内容非常广泛,本文仅就在网络封包方面出现的异常作一讨论,希望能对正从事相关工作的朋友有所帮助。
关于网络封包方面的异常,总体来说,可以分为两大类:一是封包格式出现异常;二是封包内容(即封包数据)出现异常。在封包格式的异常处理方面, 我们在最底端的网络数据包接收模块便可以加以处理。而对于封包数据内容出现的异常,只有依靠游戏本身的逻辑去加以判定和检验。游戏逻辑方面的异常处理,是 随每个游戏的不同而不同的,所以,本文随后的内容将重点阐述在网络数据包接收模块中的异常处理。
为方便以下的讨论,先明确两个概念(这两个概念是为了叙述方面,笔者自行取的,并无标准可言):
1、逻辑包:指的是在应用层提交的数据包,一个完整的逻辑包可以表示一个确切的逻辑意义。比如登录包,它里面就可以含有用户名字段和密码字段。尽管它看上去也是一段缓冲区数据,但这个缓冲区里的各个区间是代表一定的逻辑意义的。
2、物理包:指的是使用recv(recvfrom)或wsarecv(wsarecvfrom)从网络底层接收到的数据包,这样收到的一个数据包,能不能表示一个完整的逻辑意义,要取决于它是通过UDP类的“数据报协议”发的包还是通过TCP类的“流协议”发的包。
我们知道,TCP是流协议,“流协议”与“数据报协议”的不同点在于:“数据报协议”中的一个网络包本身就是一个完整的逻辑包,也就是说,在应 用层使用sendto发送了一个逻辑包之后,在接收端通过recvfrom接收到的就是刚才使用sendto发送的那个逻辑包,这个包不会被分开发送,也 不会与其它的包放在一起发送。但对于TCP而言,TCP会根据网络状况和neagle算法,或者将一个逻辑包单独发送,或者将一个逻辑包分成若干次发送, 或者会将若干个逻辑包合在一起发送出去。正因为TCP在逻辑包处理方面的这种粘合性,要求我们在作基于TCP的应用时,一般都要编写相应的拼包、解包代 码。
因此,基于TCP的上层应用,一般都要定义自己的包格式。TCP的封包定义中,除了具体的数据内容所代表的逻辑意义之外,第一步就是要确定以何种方式表示当前包的开始和结束。通常情况下,表示一个TCP逻辑包的开始和结束有两种方式:
1、以特殊的开始和结束标志表示,比如FF00表示开始,00FF表示结束。
2、直接以包长度来表示。比如可以用第一个字节表示包总长度,如果觉得这样的话包比较小,也可以用两个字节表示包长度。
下面将要给出的代码是以第2种方式定义的数据包,包长度以每个封包的前两个字节表示。我将结合着代码给出相关的解释和说明。
函数中用到的变量说明:
CLIENT_BUFFER_SIZE:缓冲区的长度,定义为:Const int CLIENT_BUFFER_SIZE=4096。
m_ClientDataBuf:数据整理缓冲区,每次收到的数据,都会先被复制到这个缓冲区的末尾,然后由下面的整理函数对这个缓冲区进行整理。它的定义是:char m_ClientDataBuf[2* CLIENT_BUFFER_SIZE]。
m_DataBufByteCount:数据整理缓冲区中当前剩余的未整理字节数。
GetPacketLen(const char*):函数,可以根据传入的缓冲区首址按照应用层协议取出当前逻辑包的长度。
GetGamePacket(const char*, int):函数,可以根据传入的缓冲区生成相应的游戏逻辑数据包。
AddToExeList(PBaseGamePacket):函数,将指定的游戏逻辑数据包加入待处理的游戏逻辑数据包队列中,等待逻辑处理线程对其进行处理。
DATA_POS:指的是除了包长度、包类型等这些标志型字段之外,真正的数据包内容的起始位置。
Bool SplitFun(const char* pData,const int &len)
{
PBaseGamePacket pGamePacket=NULL;
__int64 startPos=0, prePos=0, i=0;
int packetLen=0;
//先将本次收到的数据复制到整理缓冲区尾部
startPos = m_DataBufByteCount;
memcpy( m_ClientDataBuf+startPos, pData, len );
m_DataBufByteCount += len;
//当整理缓冲区内的字节数少于DATA_POS字节时,取不到长度信息则退出
//注意:退出时并不置m_DataBufByteCount为0
if (m_DataBufByteCount < DATA_POS+1)
return false;
//根据正常逻辑,下面的情况不可能出现,为稳妥起见,还是加上
if (m_DataBufByteCount > 2*CLIENT_BUFFER_SIZE)
{
//设置m_DataBufByteCount为0,意味着丢弃缓冲区中的现有数据
m_DataBufByteCount = 0;
//可以考虑开放错误格式数据包的处理接口,处理逻辑交给上层
//OnPacketError()
return false;
}
//还原起始指针
startPos = 0;
//只有当m_ClientDataBuf中的字节个数大于最小包长度时才能执行此语句
packetLen = GetPacketLen( pIOCPClient->m_ClientDataBuf );
//当逻辑层的包长度不合法时,则直接丢弃该包
if ((packetLen < DATA_POS+1) || (packetLen > 2*CLIENT_BUFFER_SIZE))
{
m_DataBufByteCount = 0;
//OnPacketError()
return false;
}
//保留整理缓冲区的末尾指针
__int64 oldlen = m_DataBufByteCount;
while ((packetLen <= m_DataBufByteCount) && (m_DataBufByteCount>0))
{
//调用拼包逻辑,获取该缓冲区数据对应的数据包
pGamePacket = GetGamePacket(m_ClientDataBuf+startPos, packetLen);
if (pGamePacket!=NULL)
{
//将数据包加入执行队列
AddToExeList(pGamePacket);
}
pGamePacket = NULL;
//整理缓冲区的剩余字节数和新逻辑包的起始位置进行调整
m_DataBufByteCount -= packetLen;
startPos += packetLen;
//残留缓冲区的字节数少于一个正常包大小时,只向前复制该包随后退出
if (m_DataBufByteCount < DATA_POS+1)
{
for(i=startPos; i<startPos+m_DataBufByteCount; ++i)
m_ClientDataBuf[i-startPos] = m_ClientDataBuf[i];
return true;
}
packetLen = GetPacketLen(m_ClientDataBuf + startPos );
//当逻辑层的包长度不合法时,丢弃该包及缓冲区以后的包
if ((packetLen<DATA_POS+1) || (packetLen>2*CLIENT_BUFFER_SIZE))
{
m_DataBufByteCount = 0;
//OnPacketError()
return false;
}
if (startPos+packetLen>=oldlen)
{
for(i=startPos; i<startPos+m_DataBufByteCount; ++i)
m_ClientDataBuf[i-startPos] = m_ClientDataBuf[i];
return true;
}
}//取所有完整的包
return true;
}
以上便是数据接收模块的处理函数,下面是几点简要说明:
1、用于拼包整理的缓冲区(m_ClientDataBuf)应该比recv中指定的接收缓冲区(pData)长度(CLIENT_BUFFER_SIZE)要大,通常前者是后者的2倍(2*CLIENT_BUFFER_SIZE)或更大。
2、为避免因为剩余数据前移而导致的额外开销,建议m_ClientDataBuf使用环形缓冲区实现。
3、为了避免出现无法拼装的包,我们约定每次发送的逻辑包,其单个逻辑包最大长度不可以超过CLIENT_BUFFER_SIZE的2倍。因为我们的整 理缓冲区只有2*CLIENT_BUFFER_SIZE这么长,更长的数据,我们将无法整理。这就要求在协议的设计上以及最终的发送函数的处理上要加上这 样的异常处理机制。
4、对于数据包过短或过长的包,我们通常的情况是置m_DataBufByteCount为0,即舍弃当前包的处理。如果此处不设置 m_DataBufByteCount为0也可,但该客户端只要发了一次格式错误的包,则其后继发过来的包则也将连带着产生格式错误,如果设置 m_DataBufByteCount为0,则可以比较好的避免后继的包受此包的格式错误影响。更好的作法是,在此处开放一个封包格式异常的处理接口 (OnPacketError),由上层逻辑决定对这种异常如何处置。比如上层逻辑可以对封包格式方面出现的异常进行计数,如果错误的次数超过一定的值,
则可以断开该客户端的连接。
5、建议不要在recv或wsarecv的函数后,就紧接着作以上的处理。当recv收到一段数据后,生成一个结构体或对象(它主要含有 data和len两个内容,前者是数据缓冲区,后者是数据长度),将这样的一个结构体或对象放到一个队列中由后面的线程对其使用SplitFun函数进行 整理。这样,可以最大限度地提高网络数据的接收速度,不至因为数据整理的原因而在此处浪费时间。
代码中,我已经作了比较详细的注释,可以作为拼包函数的参考,代码是从偶的应用中提取、修改而来,本身只为演示之用,所以未作调试,应用时需要你自己去完善。如有疑问,可以我的blog上留言提出。
版权声明:本文可以不经作者同意任意转载,但转载时烦请保留文章开始前两行的版权、作者及出处信息。
提示:阅读本文前,请先读此文了解文章背景:http://data.gameres.com/message.asp?TopicID=27236
让无数中国玩家为之瞩目的“魔兽世界”,随着一系列内测前期工作的逐步展开,正在一步步地走近中国玩家,但是,“魔兽”的服务器,却着实让我们为它捏了一把汗。
造成一个网游服务器当机的原因有很多,但主要有以下两种:一,服务器在线人数达到上限,服务器处理效率严重迟缓,造成当机;二,由于外挂或其它游戏作弊 工具导致的非正常数据包的出错,导致游戏服务器逻辑出现混乱,从而造成当机。在这里,我主要想说说后者如何尽可能地避免。
要避免以上 所说到的第二种情况,我们就应该遵循一个基本原则:在网游服务器的设计中,对于具有较强逻辑关系的处理单元,服务器端和客户端应该采用“互不信任原则”, 即:服务器端即使收到了客户端的数据包,也并不是立刻就认为客户端已经达到了某种功能或者状态,客户端到达是否达到了某种功能或者状态,还必须依靠服务器 端上记载的该客户端“以往状态”来判定,也就是说:服务器端的逻辑执行并不单纯地以“当前”的这一个客户端封包来进行,它还应该广泛参考当前封包的上下文 环境,对执行的逻辑作出更进一步地判定,同时,在单个封包的处理上,服务器端应该广泛考虑当前客户端封包所需要的“前置”封包,如果没有收到该客户端应该
发过来的“前置”封包,则当前的封包应该不进行处理或进行异常处理(如果想要性能高,则可以直接忽略该封包;如果想让服务器稳定,可以进行不同的异常处 理)。
之所以采用“互不信任”原则设计网游服务器,一个很重要的考虑是:防外挂。对于一个网络服务器(不仅仅是游戏服务器,泛指所有 服务器)而言,它所面对的对象既有属于自己系统内的合法的网络客户端,也有不属于自己系统内的非法客户端访问。所以,我们在考虑服务器向外开放的接口时, 就要同时考虑这两种情况:合法客户端访问时的逻辑走向以及非法客户端访问时的逻辑走向。举个简单的例子:一般情况下,玩家登录逻辑中,都是先向服务器发送 用户名和密码,然后再向服务器发送进入某组服务器的数据包;但在非法客户端(如外挂)中,则这些客户端则完全有可能先发进入某组服务器的数据包。当然,这
里仅仅是举个例子,也许并不妥当,但基本的意思我已经表达清楚了,即:你服务器端不要我客户端发什么你就信什么,你还得进行一系列的逻辑验证,以判定我当 前执行的操作是不是合法的。以这个例子中,服务器端可以通过以下逻辑执行验证功能:只有当客户端的用户名和密码通过验证后,该客户端才会进入在线玩家列表 中。而只有在线玩家列表中的成员,才可以在登陆服务器的引导下进入各分组服务器。
总之,在从事网游服务器的设计过程中,要始终不移地 坚持一个信念:我们的服务器,不仅仅有自己的游戏客户端在访问,还有其它很多他人写的游戏客户端在访问,所以,我们应该确保我们的服务器是足够强壮的,任 它风吹雨打也不怕,更不会倒。如果在开发实践中,没有很好地领会这一点或者未能将这一思路贯穿进开发之中,那么,你设计出来的服务器将是无比脆弱的。
当然,安全性和效率总是相互对立的。为了实现我们所说的“互不信任”原则,难免的,就会在游戏逻辑中加入很多的异常检测机制,但异常检测又是比较耗时 的,这就需要我们在效率和安全性方面作个取舍,对于特别重要的逻辑,我们应该全面贯彻“互不信任”原则,一步扣一步,步步为营,不让游戏逻辑出现一点漏 洞。而对于并非十分重要的场合,则完全可以采用“半信任”或者根本“不须信任”的原则进行设计,以尽可能地提高服务器效率。
本文只是对自己长期从事游戏服务器设计以来的感受加以总结,也是对魔兽的服务器有感而发。欢迎有相同感受的朋友或从事相同工作的朋友一起讨论。
本文作者:sodme 本文出处:http://blog.csdn.net/sodme
版权声明:本文可以不经作者同意任意转载,但转载时烦请保留文章开始前两行的版权、作者及出处信息。
QQ游戏于前几日终于突破了百万人同时在线的关口,向着更为远大的目标迈进,这让其它众多传统的棋牌休闲游戏平台黯然失色,相比之下,联众似乎 已经根本不是QQ的对手,因为QQ除了这100万的游戏在线人数外,它还拥有3亿多的注册量(当然很多是重复注册的)以及QQ聊天软件900万的同时在线 率,我们已经可以预见未来由QQ构建起来的强大棋牌休闲游戏帝国。
那么,在技术上,QQ游戏到底是如何实现百万人同时在线并保持游戏高效率的呢?
事实上,针对于任何单一的网络服务器程序,其可承受的同时连接数目是有理论峰值的,通过C++中对TSocket的定义类型:word,我们可以判定 这个连接理论峰值是65535,也就是说,你的单个服务器程序,最多可以承受6万多的用户同时连接。但是,在实际应用中,能达到一万人的同时连接并能保证 正常的数据交换已经是很不容易了,通常这个值都在2000到5000之间,据说QQ的单台服务器同时连接数目也就是在这个值这间。
如果要实现2000到5000用户的单服务器同时在线,是不难的。在windows下,比较成熟的技术是采用IOCP--完成端口。与完成端口相关的 资料在网上和CSDN论坛里有很多,感兴趣的朋友可以自己搜索一下。只要运用得当,一个完成端口服务器是完全可以达到2K到5K的同时在线量的。但,5K 这样的数值离百万这样的数值实在相差太大了,所以,百万人的同时在线是单台服务器肯定无法实现的。
要实现百万人同时在线,首先要实现一个比较完善的完成端口服务器模型,这个模型要求至少可以承载2K到5K的同时在线率(当然,如果你MONEY多, 你也可以只开发出最多允许100人在线的服务器)。在构建好了基本的完成端口服务器之后,就是有关服务器组的架构设计了。之所以说这是一个服务器组,是因 为它绝不仅仅只是一台服务器,也绝不仅仅是只有一种类型的服务器。
简单地说,实现百万人同时在线的服务器模型应该是:登陆服务器+大厅服务器+房间服务器。当然,也可以是其它的模型,但其基本的思想是一样的。下面,我将逐一介绍这三类服务器的各自作用。
登陆服务器:一般情况下,我们会向玩家开放若干个公开的登陆服务器,就如QQ登陆时让你选择的从哪个QQ游戏服务器登陆一样,QQ登陆时让玩家选择的 六个服务器入口实际上就是登陆服务器。登陆服务器主要完成负载平衡的作用。详细点说就是,在登陆服务器的背后,有N个大厅服务器,登陆服务器只是用于为当 前的客户端连接选择其下一步应该连接到哪个大厅服务器,当登陆服务器为当前的客户端连接选择了一个合适的大厅服务器后,客户端开始根据登陆服务器提供的信 息连接到相应的大厅上去,同时客户端断开与登陆服务器的连接,为其他玩家客户端连接登陆服务器腾出套接字资源。在设计登陆服务器时,至少应该有以下功
能:N个大厅服务器的每一个大厅服务器都要与所有的登陆服务器保持连接,并实时地把本大厅服务器当前的同时在线人数通知给各个登陆服务器,这其中包括:用 户进入时的同时在线人数增加信息以及用户退出时的同时在线人数减少信息。这里的各个大厅服务器同时在线人数信息就是登陆服务器为客户端选择某个大厅让其登 陆的依据。举例来说,玩家A通过登陆服务器1连接到登陆服务器,登陆服务器开始为当前玩家在众多的大厅服务器中根据哪一个大厅服务器人数比较少来选择一个 大厅,同时把这个大厅的连接IP和端口发给客户端,客户端收到这个IP和端口信息后,根据这个信息连接到此大厅,同时,客户端断开与登陆服务器之间的连
接,这便是用户登陆过程中,在登陆服务器这一块的处理流程。
大厅服务器:大厅服务器,是普通玩家看不到的服务器,它的连接IP和端口信息是登陆服务器通知给客户端的。也就是说,在QQ游戏的本地文件中,具体的 大厅服务器连接IP和端口信息是没有保存的。大厅服务器的主要作用是向玩家发送游戏房间列表信息,这些信息包括:每个游戏房间的类型,名称,在线人数,连 接地址以及其它如游戏帮助文件URL的信息。从界面上看的话,大厅服务器就是我们输入用户名和密码并校验通过后进入的游戏房间列表界面。大厅服务器,主要 有以下功能:一是向当前玩家广播各个游戏房间在线人数信息;二是提供游戏的版本以及下载地址信息;三是提供各个游戏房间服务器的连接IP和端口信息;四是
提供游戏帮助的URL信息;五是提供其它游戏辅助功能。但在这众多的功能中,有一点是最为核心的,即:为玩家提供进入具体的游戏房间的通道,让玩家顺利进 入其欲进入的游戏房间。玩家根据各个游戏房间在线人数,判定自己进入哪一个房间,然后双击服务器列表中的某个游戏房间后玩家开始进入游戏房间服务器。
游戏房间服务器:游戏房间服务器,具体地说就是如“斗地主1”,“斗地主2”这样的游戏房间。游戏房间服务器才是具体的负责执行游戏相关逻辑的服务 器。这样的游戏逻辑分为两大类:一类是通用的游戏房间逻辑,如:进入房间,离开房间,进入桌子,离开桌子以及在房间内说话等;第二类是游戏桌子逻辑,这个 就是各种不同类型游戏的主要区别之处了,比如斗地主中的叫地主或不叫地主的逻辑等,当然,游戏桌子逻辑里也包括有通用的各个游戏里都存在的游戏逻辑,比如 在桌子内说话等。总之,游戏房间服务器才是真正负责执行游戏具体逻辑的服务器。
这里提到的三类服务器,我均采用的是完成端口模型,每个服务器最多连接数目是5000人,但是,我在游戏房间服务器上作了逻辑层的限定,最多只允许 300人同时在线。其他两个服务器仍然允许最多5000人的同时在线。如果按照这样的结构来设计,那么要实现百万人的同时在线就应该是这样:首先是大 厅,1000000/5000=200。也就是说,至少要200台大厅服务器,但通常情况下,考虑到实际使用时服务器的处理能力和负载情况,应该至少准备 250台左右的大厅服务器程序。另外,具体的各种类型的游戏房间服务器需要多少,就要根据当前玩各种类型游戏的玩家数目分别计算了,比如斗地主最多是十万
人同时在线,每台服务器最多允许300人同时在线,那么需要的斗地主服务器数目就应该不少于:100000/300=333,准备得充分一点,就要准备 350台斗地主服务器。
除正常的玩家连接外,还要考虑到:
对于登陆服务器,会有250台大厅服务器连接到每个登陆服务器上,这是始终都要保持的连接;
而对于大厅服务器而言,如果仅仅有斗地主这一类的服务器,就要有350多个连接与各个大厅服务器始终保持着。所以从这一点看,我的结构在某些方面还存在着需要改进的地方,但核心思想是:尽快地提供用户登陆的速度,尽可能方便地让玩家进入游戏中。
本文作者:sodme
本文出处:http://blog.csdn.net/sodme
声明:本文可以不经作者同意任意转载、复制、引用。但任何对本文的引用,均须注明本文的作者、出处以及本行声明信息。
之前,我分析过QQ游戏(特指QQ休闲平台,并非QQ堂,下同)的通信架构(http://blog.csdn.net/sodme/archive/2005/06/12/393165.aspx),分析过魔兽世界的通信架构(http://blog.csdn.net/sodme/archive/2005/06/18/397371.aspx), 似乎网络游戏的通信架构也就是这些了,其实不然,在网络游戏大家庭中,还有一种类型的游戏我认为有必要把它的通信架构专门作个介绍,这便是如泡泡堂、QQ 堂类的休闲类竞技游戏。曾经很多次,被网友们要求能抽时间看看泡泡堂之类游戏的通信架构,这次由于被逼交作业,所以今晚抽了一点的时间截了一下泡泡堂的 包,正巧昨日与网友就泡泡堂类游戏的通信架构有过一番讨论,于是,将这两天的讨论、截包及思考总结于本文中,希望能对关心或者正在开发此类游戏的朋友有所 帮助,如果要讨论具体的技术细节,请到我的BLOG(http://blog.csdn.net/sodme)加我的MSN讨论..
总体来说,泡泡堂类游戏(此下简称泡泡堂)在大厅到房间这一层的通信架构,其结构与QQ游戏相当,甚至要比QQ游戏来得简单。所以,在房间这一层的通信架构上,我不想过多讨论,不清楚的朋友请参看我对QQ游戏通信架构的分析文章(http://blog.csdn.net/sodme/archive/2005/06/12/393165.aspx)。可以这么说,如果采用与QQ游戏相同的房间和大厅架构,是完全可以组建起一套可扩展的支持百万人在线的游戏系统的。也就是说,通过负载均衡+大厅+游戏房间对游戏逻辑的分摊,完全可以实现一个可扩展的百万人在线泡泡堂。
但是,泡泡堂与斗地主的最大不同点在于:泡泡堂对于实时性要求特别高。那么,泡泡堂是如何解决实时性与网络延迟以及大用户量之间矛盾的呢?
阅读以下文字前,请确认你已经完全理解TCP与UDP之间的不同点。
我们知道,TCP与UDP之间的最大不同点在于:TCP是可靠连接的,而UDP是无连接的。如果通信双方使用TCP协议,那么他们之前必须事先 通过监听+连接的方式将双方的通信管道建立起来;而如果通信双方使用的是UDP通信,则双方不用事先建立连接,发送方只管向目标地址上的目标端口发送 UDP包即可,不用管对方到底收没收到。如果要说形象点,可以用这样一句话概括:TCP是打电话,UDP是发电报。TCP通信,为了保持这样的可靠连接, 在可靠性上下了很多功夫,所以导致了它的通信效率要比UDP差很多,所以,一般地,在地实时性要求非常高的场合,会选择使用UDP协议,比如常见的动作射 击类游戏。
通过载包,我们发现泡泡堂中同时采用了TCP和UDP两种通信协议。并且,具有以下特点:
1.当玩家未进入具体的游戏地图时,仅有TCP通信存在,而没有UDP通信;
2.进入游戏地图后,TCP的通信量远远小于UDP的通信量
3.UDP的通信IP个数,与房间内的玩家成一一对应关系(这一点,应网友疑惑而加,此前已经证实)
以上是几个表面现象,下面我们来分析它的本质和内在。^&^
泡泡堂的游戏逻辑,简单地可以归纳为以下几个方面:
1.玩家移动
2.玩家埋地雷(如果你觉得这种叫法比较土,你也可以叫它:下泡泡,呵呵)
3.地雷爆炸出道具或者地雷爆炸困住另一玩家
4.玩家捡道具或者玩家消灭/解救一被困的玩家
与MMORPG一样,在上面的几个逻辑中,广播量最大的其实是玩家移动。为了保持玩家画面同步,其他玩家的每一步移动消息都要即时地发给其它玩家。
通常,网络游戏的逻辑控制,绝大多数是在服务器端的。有时,为了保证画面的流畅性,我们会有意识地减少服务器端的逻辑判断量和广播量,当然,这 个减少,是以“不危及游戏的安全运行”为前提的。到底如何在效率、流畅性和安全性之间作取舍,很多时候是需要经验积累的,效率提高的过程,就是逻辑不断优 化的过程。不过,有一个原则是可以说的,那就是:“关键逻辑”一定要放在服务器上来判断。那么,什么是“关键逻辑”呢?
拿泡泡堂来说,下面的这个逻辑,我认为就是关键逻辑:玩家在某处埋下一颗地雷,地雷爆炸后到底能不能炸出道具以及炸出了哪些道具,这个信息,需要服务器来给。那么,什么又是“非关键逻辑”呢?
“非关键逻辑”,在不同的游戏中,会有不同的概念。在通常的MMORPG中,玩家移动逻辑的判断,是算作关键逻辑的,否则,如果服务器端不对客 户端发过来的移动包进行判断那就很容易造成玩家的瞬移以及其它毁灭性的灾难。而在泡泡堂中,玩家移动逻辑到底应不应该算作关键逻辑还是值得考虑的。泡泡堂 中的玩家可以取胜的方法,通常是确实因为打得好而赢得胜利,不会因为瞬移而赢得胜利,因为如果外挂要作泡泡堂的瞬移,它需要考虑的因素和判断的逻辑太多 了,由于比赛进程的瞬息万变,外挂的瞬移点判断不一定就比真正的玩家来得准确,所在,在玩家移动这个逻辑上使用外挂,在泡泡堂这样的游戏中通常是得不偿失 的(当然,那种特别变态的高智能的外挂除外)。从目前我查到的消息来看,泡泡堂的外挂多数是一些按键精灵脚本,它的本质还不是完全的游戏机器人,并不是通 过纯粹的协议接管实现的外挂功能。这也从反面验证了我以上的想法。
说到这里,也许你已经明白了。是的!TCP通信负责“关键逻辑”,而UDP通信负责“非关键逻辑”,这里的“非关键逻辑”中就包含了玩家移动。 在泡泡堂中,TCP通信用于本地玩家与服务器之间的通信,而UDP则用于本地玩家与同一地图中的其他各玩家的通信。当本地玩家要移动时,它会同时向同一地 图内的所有玩家广播自己的移动消息,其他玩家收到这个消息后会更新自己的游戏画面以实现画面同步。而当本地玩家要在地图上放置一个炸弹时,本地玩家需要将 此消息同时通知同一地图内的其他玩家以及服务器,甚至这里,可以不把放置炸弹的消息通知给服务器,而仅仅通知其他玩家。当炸弹爆炸后,要拾取物品时才向服 务器提交拾取物品的消息。
那么,你可能会问,“地图上某一点是否存在道具”这个消息,服务器是什么时候通知给客户端的呢?这个问题,可以有两种解决方案:
1.客户端如果在放置炸弹时,将放置炸弹的消息通知给服务器,服务器可以在收到这个消息后,告诉客户端炸弹爆炸后会有哪些道具。但我觉得这种方案不好,因为这样作会增加游戏运行过程中的数据流量。
2.而这第2种方案就是,客户端进入地图后,游戏刚开始时,就由服务器将本地图内的各道具所在点的信息传给各客户端,这样,可以省去两方面的开 销:a.客户端放炸弹时,可以不通知服务器而只通知其它玩家;b.服务器也不用在游戏运行过程中再向客户端传递有关某点有道具的信息。
但是,不管采用哪种方案,服务器上都应该保留一份本地图内道具所在点的信息。因为服务器要用它来验证一个关键逻辑:玩家拾取道具。当玩家要在某点拾取道具时,服务器必须要判定此点是否有道具,否则,外挂可以通过频繁地发拾取道具的包而不断取得道具。
至于泡泡堂其它游戏逻辑的实现方法,我想,还是要依靠这个原则:首先判断这个逻辑是关键逻辑吗?如果不全是,那其中的哪部分是非关键逻辑呢?对 于非关键逻辑,都可以交由客户端之间(UDP)去自行完成。而对于关键逻辑,则必须要有服务器(TCP)的校验和认证。这便是我要说的。
以上仅仅是在理论上探讨关于泡泡堂类游戏在通信架构上的可能作法,这些想法是没有事实依据的,所有结论皆来源于对封包的分析以及个人经验,文章 的内容和观点可能跟真实的泡泡堂通信架构实现有相当大的差异,但我想,这并不是主要的,因为我的目的是向大家介绍这样的TCP和UDP通信并存情况下,如 何对游戏逻辑的进行取舍和划分。无论是“关键逻辑”的定性,还是“玩家移动”的具体实施,都需要开发者在具体的实践中进行总结和优化。此文全当是一个引子 罢,如有疑问,请加Msn讨论。
posted @ 2009-09-23 23:44 暗夜教父 阅读(142) | 评论 (0) | 编辑 收藏 负载均衡--大型在线系统实现的关键(上篇)(再谈QQ游戏百万人在线的技术实现)
本文作者:sodme
本文出处:http://blog.csdn.net/sodme
声明:本文可以不经作者同意任意转载,但任何对本文的引用都须注明作者、出处及此声明信息。谢谢!!
要了解此篇文章中引用的本人写的另一篇文章,请到以下地址:
http://blog.csdn.net/sodme/archive/2004/12/12/213995.aspx
以上的这篇文章是早在去年的时候写的了,当时正在作休闲平台,一直在想着如何实现一个可扩充的支持百万人在线的游戏平台,后来思路有了,就写了那篇总结。文章的意思,重点在于阐述一个百万级在线的系统是如何实施的,倒没真正认真地考察过QQ游戏到底是不是那样实现的。
近日在与业内人士讨论时,提到QQ游戏的实现方式并不是我原来所想的那样,于是,今天又认真抓了一下QQ游戏的包,结果确如这位兄弟所言,QQ 游戏的架构与我当初所设想的那个架构相差确实不小。下面,我重新给出QQ百万级在线的技术实现方案,并以此展开,谈谈大型在线系统中的负载均衡机制的设 计。
从QQ游戏的登录及游戏过程来看,QQ游戏中,也至少分为三类服务器。它们是:
第一层:登陆/账号服务器(Login Server),负责验证用户身份、向客户端传送初始信息,从QQ聊天软件的封包常识来看,这些初始信息可能包括“会话密钥”此类的信息,以后客户端与后续服务器的通信就使用此会话密钥进行身份验证和信息加密;
第二层:大厅服务器(估且这么叫吧, Game Hall Server),负责向客户端传递当前游戏中的所有房间信息,这些房间信息包括:各房间的连接IP,PORT,各房间的当前在线人数,房间名称等等。
第三层:游戏逻辑服务器(Game Logic Server),负责处理房间逻辑及房间内的桌子逻辑。
从静态的表述来看,以上的三层结构似乎与我以前写的那篇文章相比并没有太大的区别,事实上,重点是它的工作流程,QQ游戏的通信流程与我以前的设想可谓大相径庭,其设计思想和技术水平确实非常优秀。具体来说,QQ游戏的通信过程是这样的:
1.由Client向Login Server发送账号及密码等登录消息,Login Server根据校验结果返回相应信息。可以设想的是,如果Login Server通过了Client的验证,那么它会通知其它Game Hall Server或将通过验证的消息以及会话密钥放在Game Hall Server也可以取到的地方。总之,Login Server与Game Hall Server之间是可以共享这个校验成功消息的。一旦Client收到了Login Server返回成功校验的消息后,Login Server会主动断开与Client的连接,以腾出socket资源。Login Server的IP信息,是存放在QQGameconfigQQSvrInfo.ini里的。
2.Client收到Login Server的校验成功等消息后,开始根据事先选定的游戏大厅入口登录游戏大厅,各个游戏大厅Game Hall Server的IP及Port信息,是存放在QQGameDirconfig.ini里的。Game Hall Server收到客户端Client的登录消息后,会根据一定的策略决定是否接受Client的登录,如果当前的Game Hall Server已经到了上限或暂时不能处理当前玩家登录消息,则由Game Hall Server发消息给Client,以让Client重定向到另外的Game Hall Server登录。重定向的IP及端口信息,本地没有保存,是通过数据包或一定的算法得到的。如果当前的Game Hall Server接受了该玩家的登录消息后,会向该Client发送房间目录信息,这些信息的内容我上面已经提到。目录等消息发送完毕后,Game Hall Server即断开与Client的连接,以腾出socket资源。在此后的时间里,Client每隔30分钟会重新连接Game Hall Server并向其索要最新的房间目录信息及在线人数信息。
3.Client根据列出的房间列表,选择某个房间进入游戏。根据我的抓包结果分析,QQ游戏,并不是给每一个游戏房间都分配了一个单独的端口 进行处理。在QQ游戏里,有很多房间是共用的同一个IP和同一个端口。比如,在斗地主一区,前50个房间,用的都是同一个IP和Port信息。这意味着, 这些房间,在QQ游戏的服务器上,事实上,可能是同一个程序在处理!!!QQ游戏房间的人数上限是400人,不难推算,QQ游戏单个服务器程序的用户承载 量是2万,即QQ的一个游戏逻辑服务器程序最多可同时与2万个玩家保持TCP连接并保证游戏效率和品质,更重要的是,这样可以为腾讯省多少money 呀!!!哇哦!QQ确实很牛。以2万的在线数还能保持这么好的游戏品质,确实不容易!QQ游戏的单个服务器程序,管理的不再只是逻辑意义上的单个房间,而 可能是许多逻辑意义上的房间。其实,对于服务器而言,它就是一个大区服务器或大区服务器的一部分,我们可以把它理解为一个庞大的游戏地图,它实现的也是分 块处理。而对于每一张桌子上的打牌逻辑,则是有一个统一的处理流程,50个房间的50*100张桌子全由这一个服务器程序进行处理(我不知道QQ游戏的具 体打牌逻辑是如何设计的,我想很有可能也是分区域的,分块的)。当然,以上这些只是服务器作的事,针对于客户端而言,客户端只是在表现上,将一个个房间单 独罗列了出来,这样作,是为便于玩家进行游戏以及减少服务器的开销,把这个大区中的每400人放在一个集合内进行处理(比如聊天信息,“向400人广播” 和“向2万人广播”,这是完全不同的两个概念)。
4.需要特别说明的一点。进入QQ游戏房间后,直到点击某个位置坐下打开另一个程序界面,客户端的程序,没有再创建新的socket,而仍然使 用原来大厅房间客户端跟游戏逻辑服务器交互用的socket。也就是说,这是两个进程共用的同一个socket!不要小看这一点。如果你在创建桌子客户端 程序后又新建了一个新的socket与游戏逻辑服务器进行通信,那么由此带来的玩家进入、退出、逃跑等消息会带来非常麻烦的数据同步问题,俺在刚开始的时 候就深受其害。而一旦共用了同一个socket后,你如果退出桌子,服务器不涉及释放socket的问题,所以,这里就少了很多的数据同步问题。关于多个 进程如何共享同一个socket的问题,请去google以下内容:WSADuplicateSocket。
以上便是我根据最新的QQ游戏抓包结果分析得到的QQ游戏的通信流程,当然,这个流程更多的是客户端如何与服务器之间交互的,却没有涉及到服务器彼此之间是如何通信和作数据同步的。关于服务器之间的通信流程,我们只能基于自己的经验和猜想,得出以下想法:
1.Login Server与Game Hall Server之前的通信问题。Login Server是负责用户验证的,一旦验证通过之后,它要设法让Game Hall Server知道这个消息。它们之前实现信息交流的途径,我想可能有这样几条:a. Login Server将通过验证的用户存放到临时数据库中;b. Login Server将验证通过的用户存放在内存中,当然,这个信息,应该是全局可访问的,就是说所有QQ的Game Hall Server都可以通过服务器之间的数据包通信去获得这样的信息。
2.Game Hall Server的最新房间目录信息的取得。这个信息,是全局的,也就是整个游戏中,只保留一个目录。它的信息来源,可以由底层的房间服务器逐级报上来,报给谁?我认为就如保存的全局登录列表一样,它报给保存全局登录列表的那个服务器或数据库。
3.在QQ游戏中,同一类型的游戏,无法打开两上以上的游戏房间。这个信息的判定,可以根据全局信息来判定。
以上关于服务器之间如何通信的内容,均属于个人猜想,QQ到底怎么作的,恐怕只有等大家中的某一位进了腾讯之后才知道了。呵呵。不过,有一点是 可以肯定的,在整个服务器架构中,应该有一个地方是专门保存了全局的登录玩家列表,只有这样才能保证玩家不会重复登录以及进入多个相同类型的房间。
在前面的描述中,我曾经提到过一个问题:当登录当前Game Hall Server不成功时,QQ游戏服务器会选择让客户端重定向到另位的服务器去登录,事实上,QQ聊天服务器和MSN服务器的登录也是类似的,它也存在登录重定向问题。
那么,这就引出了另外的问题,由谁来作这个策略选择?以及由谁来提供这样的选择资源?这样的处理,便是负责负载均衡的服务器的处理范围了。由QQ游戏的通信过程分析派生出来的针对负责均衡及百万级在线系统的更进一步讨论,将在下篇文章中继续。
在此,特别感谢网友tilly及某位不便透露姓名的网友的讨论,是你们让我决定认真再抓一次包探个究竟。
posted @ 2009-09-23 23:43 暗夜教父 阅读(116) | 评论 (0) | 编辑 收藏 闪客帝国-flash多人网络游戏的实现的讨论 一直以来,flash就是我非常喜爱的平台,因为他简单,完整,但是功能强大,
很适合游戏软件的开发,
只不过处理复杂的算法和海量数据的时候,
速度慢了一些,
但是这并不意味着flash不能做,
我们需要变通的方法去让flash做不善长的事情,
这个贴子用来专门讨论用flash作为客户端来开发网络游戏,
持续时间也不会很长,在把服务器端的源代码公开完以后,
就告一段落,
注意,仅仅用flash作为客户端,
服务器端,我们使用vc6,
我将陆续的公开服务器端的源代码和大家共享,
并且将讲解一些网络游戏开发的原理,
希望对此感兴趣的朋友能够使用今后的资源或者理论开发出完整的网络游戏。
我们从简单到复杂,
从棋牌类游戏到动作类的游戏,
从2个人的游戏到10个人的游戏,
因为工作忙的关系,我所做的一切仅仅起到抛砖引玉的作用,
希望大家能够热情的讨论,为中国的flash事业垫上一块砖,添上一片瓦。
现在的大型网络游戏(mmo game)都是基于server/client体系结构的,
server端用c(windows下我们使用vc.net+winsock)来编写,
客户端就无所谓,
在这里,我们讨论用flash来作为客户端的实现,
实践证明,flash的xml socket完全可以胜任网络传输部分,
在别的贴子中,我看见有的朋友谈论msn中的flash game
他使用msn内部的网络接口进行传输,
这种做法也是可以的,
我找很久以前对于2d图形编程的说法,"给我一个打点函数,我就能创造整个游戏世界",
而在网络游戏开发过程中,"给我一个发送函数和一个接收函数,我就能创造网络游戏世界."
我们抽象一个接口,就是网络传输的接口,
对于使用flash作为客户端,要进行网络连接,
一个网络游戏的客户端,
可以简单的抽象为下面的流程
1.与远程服务器建立一条长连接
2.用账号密码登陆
3.循环
接收消息
发送消息
4.关闭
我们可以直接使用flash 的xml socket,也可以使用类似msn的那种方式,
这些我们先不管,我们先定义接口,
Connect( "127.0.0.1", 20000 ); 连接远程服务器,建立一条长连接
Send( data, len ); 向服务器发送一条消息
Recv( data, len ); 接收服务器传来的消息
项目开发的基本硬件配置
一台普通的pc就可以了,
安装好windows 2000和vc6就可以了,
然后连上网,局域网和internet都可以,
接下去的东西我都简化,不去用晦涩的术语,
既然是网络,我们就需要网络编程接口,
服务器端我们用的是winsock 1.1,使用tcp连接方式,
[tcp和udp]
tcp可以理解为一条连接两个端子的隧道,提供可靠的数据传输服务,
只要发送信息的一方成功的调用了tcp的发送函数发送一段数据,
我们可以认为接收方在若干时间以后一定会接收到完整正确的数据,
不需要去关心网络传输上的细节,
而udp不保证这一点,
对于网络游戏来说,tcp是普遍的选择。
[阻塞和非阻塞]
在通过socket发送数据时,如果直到数据发送完毕才返回的方式,也就是说如果我们使用send( buffer, 100.....)这样的函数发送100个字节给别人,我们要等待,直到100个自己发送完毕,程序才往下走,这样就是阻塞的,
而非阻塞的方式,当你调用send(buffer,100....)以后,立即返回,此时send函数告诉你发送成功,并不意味着数据已经向目的地发送完 毕,甚至有可能数据还没有开始发送,只被保留在系统的缓冲里面,等待被发送,但是你可以认为数据在若干时间后,一定会被目的地完整正确的收到,我们要充分 的相信tcp。
阻塞的方式会引起系统的停顿,一般网络游戏里面使用的都是非阻塞的方式,
[有状态服务器和无状态服务器]
在c/s体系中,如果server不保存客户端的状态,称之为无状态,反之为有状态,
在这里要强调一点,
我们所说的服务器不是一台具体的机器,
而是指服务器应用程序,
一台具体的机器或者机器群组可以运行一个或者多个服务器应用程序,
我们的网络游戏使用的是有状态服务器,
保存所有玩家的数据和状态,
一些有必要了解的理论和开发工具
[开发语言]
vc6
我们首先要熟练的掌握一门开发语言,
学习c++是非常有必要的,
而vc是windows下面的软件开发工具,
为什么选择vc,可能与我本身使用vc有关,
而且网上可以找到许多相关的资源和源代码,
[操作系统]
我们使用windows2000作为服务器的运行环境,
所以我们有必要去了解windows是如何工作的,
同时对它的编程原理应该熟练的掌握
[数据结构和算法]
要写出好的程序要先具有设计出好的数据结构和算法的能力,
好的算法未必是繁琐的公式和复杂的代码,
我们要找到又好写有满足需求的算法,
有时候,最笨的方法同时也是很好的方法,
很多程序员沉迷于追求精妙的算法而忽略了宏观上的工程,
花费了大量的精力未必能够取得好的效果,
举个例子,
我当年进入游戏界工作,学习老师的代码,
发现有个函数,要对画面中的npc位置进行排序,
确定哪个先画,那个后画,
他的方法太“笨”,
任何人都会想到的冒泡,
一个一个去比较,没有任何的优化,
我当时想到的算法就有很多,
而且有一大堆优化策略,
可是,当我花了很长时间去实现我的算法时,
发现提升的那么一点效率对游戏整个运行效率而言几乎是没起到什么作用,
或者说虽然算法本身快了几倍,
可是那是多余的,老师的算法虽然“笨”,
可是他只花了几十行代码就搞定了,
他的时间花在别的更需要的地方,
这就是他可以独自完成一个游戏,
而我可以把一个函数优化100倍也只能打杂的原因
[tcp/ip的理论]
推荐数据用tcp/ip进行网际互连,tcp/ip详解,
这是两套书,共有6卷,
都是国外的大师写的,
可以说是必读的,
网络传