游戏源码里的server怎么安装

系统发生错误
文档【】不存在或已删除!
等待时间: 64606人阅读
本文作者: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_DataBufByteC&&
&&& memcpy( m_ClientDataBuf+startPos, pData, len );
&&& m_DataBufByteCount +=&&&
&&& //当整理缓冲区内的字节数少于DATA_POS字节时,取不到长度信息则退出
 //注意:退出时并不置m_DataBufByteCount为0
&&& if (m_DataBufByteCount & DATA_POS+1)
&&& //根据正常逻辑,下面的情况不可能出现,为稳妥起见,还是加上
&&& if (m_DataBufByteCount && 2*CLIENT_BUFFER_SIZE)
&&&&&&& //设置m_DataBufByteCount为0,意味着丢弃缓冲区中的现有数据
&&&&&&& m_DataBufByteCount = 0;
  //可以考虑开放错误格式数据包的处理接口,处理逻辑交给上层
  //OnPacketError()
&&&& //还原起始指针
& && startPos = 0;
&&&& //只有当m_ClientDataBuf中的字节个数大于最小包长度时才能执行此语句
&&& packetLen = GetPacketLen( pIOCPClient-&m_ClientDataBuf );
&&& //当逻辑层的包长度不合法时,则直接丢弃该包
&&& if ((packetLen & DATA_POS+1) || (packetLen & 2*CLIENT_BUFFER_SIZE))
&&&&&&& m_DataBufByteCount = 0;
  //OnPacketError()
&&& //保留整理缓冲区的末尾指针
&&& __int64 oldlen = m_DataBufByteC&
&&& while ((packetLen &= m_DataBufByteCount) && (m_DataBufByteCount&0))
&&&&&&& //调用拼包逻辑,获取该缓冲区数据对应的数据包
&&&&&&& pGamePacket = GetGamePacket(m_ClientDataBuf+startPos, packetLen);&
&&&&&&& if (pGamePacket!=NULL)
&&&&&&&&&&& //将数据包加入执行队列
&&&&&&&&&&& AddToExeList(pGamePacket);
&&&&&&& pGamePacket = NULL;
  //整理缓冲区的剩余字节数和新逻辑包的起始位置进行调整
&&&&&&& m_DataBufByteCount -= packetL
&&&&&&& startPos += packetL&
&&&&&&& //残留缓冲区的字节数少于一个正常包大小时,只向前复制该包随后退出
&&&&&&& if (m_DataBufByteCount & DATA_POS+1)
&&&&&&&&&&& for(i=startP i&startPos+m_DataBufByteC ++i)
&&&&&&&&&&&&&&& m_ClientDataBuf[i-startPos] = m_ClientDataBuf[i];
&&&&&&&&&&&
&&&&&&&&packetLen = GetPacketLen(m_ClientDataBuf + startPos );
&&&&&&&& //当逻辑层的包长度不合法时,丢弃该包及缓冲区以后的包
&&&&&&& if ((packetLen&DATA_POS+1) || (packetLen&2*CLIENT_BUFFER_SIZE))
&&&&&&&&&&& m_DataBufByteCount = 0;
&&&   //OnPacketError()
&&&&&&&&&&&
&&&&&&&& if (startPos+packetLen&=oldlen)
&&&&&&&&&&& for(i=startP i&startPos+m_DataBufByteC ++i)
&&&&&&&&&&&&&&& m_ClientDataBuf[i-startPos] = m_ClientDataBuf[i];&&&&&&&&&&
&&&&&&&&&&&
&&&& }//取所有完整的包
  以上便是数据接收模块的处理函数,下面是几点简要说明:
  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上留言提出。
posted @&&暗夜教父 阅读(97) |&&|&&
本文作者:sodme 本文出处:
版权声明:本文可以不经作者同意任意转载,但转载时烦请保留文章开始前两行的版权、作者及出处信息。
提示:阅读本文前,请先读此文了解文章背景:
  让无数中国玩家为之瞩目的“魔兽世界”,随着一系列内测前期工作的逐步展开,正在一步步地走近中国玩家,但是,“魔兽”的服务器,却着实让我们为它捏了一把汗。
造成一个网游服务器当机的原因有很多,但主要有以下两种:一,服务器在线人数达到上限,服务器处理效率严重迟缓,造成当机;二,由于外挂或其它游戏作弊 工具导致的非正常数据包的出错,导致游戏服务器逻辑出现混乱,从而造成当机。在这里,我主要想说说后者如何尽可能地避免。
  要避免以上 所说到的第二种情况,我们就应该遵循一个基本原则:在网游服务器的设计中,对于具有较强逻辑关系的处理单元,服务器端和客户端应该采用“互不信任原则”, 即:服务器端即使收到了客户端的数据包,也并不是立刻就认为客户端已经达到了某种功能或者状态,客户端到达是否达到了某种功能或者状态,还必须依靠服务器 端上记载的该客户端“以往状态”来判定,也就是说:服务器端的逻辑执行并不单纯地以“当前”的这一个客户端封包来进行,它还应该广泛参考当前封包的上下文 环境,对执行的逻辑作出更进一步地判定,同时,在单个封包的处理上,服务器端应该广泛考虑当前客户端封包所需要的“前置”封包,如果没有收到该客户端应该
发过来的“前置”封包,则当前的封包应该不进行处理或进行异常处理(如果想要性能高,则可以直接忽略该封包;如果想让服务器稳定,可以进行不同的异常处 理)。
  之所以采用“互不信任”原则设计网游服务器,一个很重要的考虑是:防外挂。对于一个网络服务器(不仅仅是游戏服务器,泛指所有 服务器)而言,它所面对的对象既有属于自己系统内的合法的网络客户端,也有不属于自己系统内的非法客户端访问。所以,我们在考虑服务器向外开放的接口时, 就要同时考虑这两种情况:合法客户端访问时的逻辑走向以及非法客户端访问时的逻辑走向。举个简单的例子:一般情况下,玩家登录逻辑中,都是先向服务器发送 用户名和密码,然后再向服务器发送进入某组服务器的数据包;但在非法客户端(如外挂)中,则这些客户端则完全有可能先发进入某组服务器的数据包。当然,这
里仅仅是举个例子,也许并不妥当,但基本的意思我已经表达清楚了,即:你服务器端不要我客户端发什么你就信什么,你还得进行一系列的逻辑验证,以判定我当 前执行的操作是不是合法的。以这个例子中,服务器端可以通过以下逻辑执行验证功能:只有当客户端的用户名和密码通过验证后,该客户端才会进入在线玩家列表 中。而只有在线玩家列表中的成员,才可以在登陆服务器的引导下进入各分组服务器。
  总之,在从事网游服务器的设计过程中,要始终不移地 坚持一个信念:我们的服务器,不仅仅有自己的游戏客户端在访问,还有其它很多他人写的游戏客户端在访问,所以,我们应该确保我们的服务器是足够强壮的,任 它风吹雨打也不怕,更不会倒。如果在开发实践中,没有很好地领会这一点或者未能将这一思路贯穿进开发之中,那么,你设计出来的服务器将是无比脆弱的。
当然,安全性和效率总是相互对立的。为了实现我们所说的“互不信任”原则,难免的,就会在游戏逻辑中加入很多的异常检测机制,但异常检测又是比较耗时 的,这就需要我们在效率和安全性方面作个取舍,对于特别重要的逻辑,我们应该全面贯彻“互不信任”原则,一步扣一步,步步为营,不让游戏逻辑出现一点漏 洞。而对于并非十分重要的场合,则完全可以采用“半信任”或者根本“不须信任”的原则进行设计,以尽可能地提高服务器效率。
  本文只是对自己长期从事游戏服务器设计以来的感受加以总结,也是对魔兽的服务器有感而发。欢迎有相同感受的朋友或从事相同工作的朋友一起讨论。
posted @&&暗夜教父 阅读(122) |&&|&&
本文作者:sodme 本文出处:
版权声明:本文可以不经作者同意任意转载,但转载时烦请保留文章开始前两行的版权、作者及出处信息。
  QQ游戏于前几日终于突破了百万人同时在线的关口,向着更为远大的目标迈进,这让其它众多传统的棋牌休闲游戏平台黯然失色,相比之下,联众似乎 已经根本不是QQ的对手,因为QQ除了这100万的游戏在线人数外,它还拥有3亿多的注册量(当然很多是重复注册的)以及QQ聊天软件900万的同时在线 率,我们已经可以预见未来由QQ构建起来的强大棋牌休闲游戏帝国。
那么,在技术上,QQ游戏到底是如何实现百万人同时在线并保持游戏高效率的呢?
事实上,针对于任何单一的网络服务器程序,其可承受的同时连接数目是有理论峰值的,通过C++中对TSocket的定义类型:word,我们可以判定 这个连接理论峰值是65535,也就是说,你的单个服务器程序,最多可以承受6万多的用户同时连接。但是,在实际应用中,能达到一万人的同时连接并能保证 正常的数据交换已经是很不容易了,通常这个值都在之间,据说QQ的单台服务器同时连接数目也就是在这个值这间。
如果要实现用户的单服务器同时在线,是不难的。在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人的同时在线。如果按照这样的结构来设计,那么要实现百万人的同时在线就应该是这样:首先是大 厅,0=200。也就是说,至少要200台大厅服务器,但通常情况下,考虑到实际使用时服务器的处理能力和负载情况,应该至少准备 250台左右的大厅服务器程序。另外,具体的各种类型的游戏房间服务器需要多少,就要根据当前玩各种类型游戏的玩家数目分别计算了,比如斗地主最多是十万
人同时在线,每台服务器最多允许300人同时在线,那么需要的斗地主服务器数目就应该不少于:=333,准备得充分一点,就要准备 350台斗地主服务器。
除正常的玩家连接外,还要考虑到:
对于登陆服务器,会有250台大厅服务器连接到每个登陆服务器上,这是始终都要保持的连接;
而对于大厅服务器而言,如果仅仅有斗地主这一类的服务器,就要有350多个连接与各个大厅服务器始终保持着。所以从这一点看,我的结构在某些方面还存在着需要改进的地方,但核心思想是:尽快地提供用户登陆的速度,尽可能方便地让玩家进入游戏中。
posted @&&暗夜教父 阅读(108) |&&|&&
本文作者:sodme
本文出处:
声明:本文可以不经作者同意任意转载、复制、引用。但任何对本文的引用,均须注明本文的作者、出处以及本行声明信息。
  之前,我分析过QQ游戏(特指QQ休闲平台,并非QQ堂,下同)的通信架构(),分析过魔兽世界的通信架构(),
似乎网络游戏的通信架构也就是这些了,其实不然,在网络游戏大家庭中,还有一种类型的游戏我认为有必要把它的通信架构专门作个介绍,这便是如泡泡堂、QQ 堂类的休闲类竞技游戏。曾经很多次,被网友们要求能抽时间看看泡泡堂之类游戏的通信架构,这次由于被逼交作业,所以今晚抽了一点的时间截了一下泡泡堂的 包,正巧昨日与网友就泡泡堂类游戏的通信架构有过一番讨论,于是,将这两天的讨论、截包及思考总结于本文中,希望能对关心或者正在开发此类游戏的朋友有所 帮助,如果要讨论具体的技术细节,请到我的BLOG()加我的MSN讨论..
  总体来说,泡泡堂类游戏(此下简称泡泡堂)在大厅到房间这一层的通信架构,其结构与QQ游戏相当,甚至要比QQ游戏来得简单。所以,在房间这一层的通信架构上,我不想过多讨论,不清楚的朋友请参看我对QQ游戏通信架构的分析文章()。可以这么说,如果采用与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 @&&暗夜教父 阅读(142) |&&|&&
本文作者:sodme
本文出处:
声明:本文可以不经作者同意任意转载,但任何对本文的引用都须注明作者、出处及此声明信息。谢谢!!
  要了解此篇文章中引用的本人写的另一篇文章,请到以下地址:
以上的这篇文章是早在去年的时候写的了,当时正在作休闲平台,一直在想着如何实现一个可扩充的支持百万人在线的游戏平台,后来思路有了,就写了那篇总结。文章的意思,重点在于阐述一个百万级在线的系统是如何实施的,倒没真正认真地考察过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信息,是存放在QQGame\config\QQSvrInfo.ini里的。
  2.Client收到Login Server的校验成功等消息后,开始根据事先选定的游戏大厅入口登录游戏大厅,各个游戏大厅Game Hall Server的IP及Port信息,是存放在QQGame\Dirconfig.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 @&&暗夜教父 阅读(116) |&&|&&
一直以来,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.用账号密码登陆
我们可以直接使用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不保存客户端的状态,称之为无状态,反之为有状态,
在这里要强调一点,
我们所说的服务器不是一台具体的机器,
而是指服务器应用程序,
一台具体的机器或者机器群组可以运行一个或者多个服务器应用程序,
我们的网络游戏使用的是有状态服务器,
保存所有玩家的数据和状态,
一些有必要了解的理论和开发工具
[开发语言]
我们首先要熟练的掌握一门开发语言,
学习c++是非常有必要的,
而vc是windows下面的软件开发工具,
为什么选择vc,可能与我本身使用vc有关,
而且网上可以找到许多相关的资源和源代码,
[操作系统]
我们使用windows2000作为服务器的运行环境,
所以我们有必要去了解windows是如何工作的,
同时对它的编程原理应该熟练的掌握
[数据结构和算法]
要写出好的程序要先具有设计出好的数据结构和算法的能力,
好的算法未必是繁琐的公式和复杂的代码,
我们要找到又好写有满足需求的算法,
有时候,最笨的方法同时也是很好的方法,
很多程序员沉迷于追求精妙的算法而忽略了宏观上的工程,
花费了大量的精力未必能够取得好的效果,
举个例子,
我当年进入游戏界工作,学习老师的代码,
发现有个函数,要对画面中的npc位置进行排序,
确定哪个先画,那个后画,
他的方法太“笨”,
任何人都会想到的冒泡,
一个一个去比较,没有任何的优化,
我当时想到的算法就有很多,
而且有一大堆优化策略,
可是,当我花了很长时间去实现我的算法时,
发现提升的那么一点效率对游戏整个运行效率而言几乎是没起到什么作用,
或者说虽然算法本身快了几倍,
可是那是多余的,老师的算法虽然“笨”,
可是他只花了几十行代码就搞定了,
他的时间花在别的更需要的地方,
这就是他可以独自完成一个游戏,
而我可以把一个函数优化100倍也只能打杂的原因
[tcp/ip的理论]
推荐数据用tcp/ip进行网际互连,tcp/ip详解,
这是两套书,共有6卷,
都是国外的大师写的,
可以说是必读的,
网络传输中的“消息”
消息是个很常见的术语,
在windows中,消息机制是个十分重要的概念,
我们在网络游戏中,也使用了消息这样的机制,
一般我们这么做,
一个数据块,头4个字节是消息名,后面接2个字节的数据长度,
再后面就是实际的数据
为什么使用消息??
我们来看看例子,
在游戏世界,
一个玩家想要和别的玩家聊天,
那么,他输入好聊天信息,
客户端生成一条聊天消息,
并把聊天的内容打包到消息中,
然后把聊天消息发送给服务器,
请求服务器把聊天信息发送给另一个玩家,
服务器接收到一条消息,
此刻,服务器并不知道当前的数据是什么东西,
对于服务器来讲,这段数据仅仅来自于网络通讯的底层,
不加以分析的话,没有任何的信息,
因为我们的通讯是基于消息机制的,
我们认为服务器接收到的任何数据都是基于消息的数据方式组织的,
4个字节消息名,2字节长度,这个是不会变的,
通过消息名,服务器发现当前数据是一条聊天数据,
通过长度把需要的数据还原,校验,
然后把这条消息发送给另一个玩家,
大家注意,消息是变长的,
关于消息的解释完全在于服务器和客户端的应用程序,
可以认为与网络传输低层无关,
比如一条私聊消息可能是这样的,
MsgID:4 byte
Length:2 byte
TargetPlayerID:2 byte
String:anybyte & 256
一条移动消息可能是这样的,
MsgID:4 byte
Length:2 byte
TargetPlayerID:2 byte
TargetPosition:4 byte (x,y)
编程者可以自定义消息的内容以满足不同的需求
队列是一个很重要的数据结构,
比如说消息队列,
服务器或者客户端,
发送的消息不一定是立即发送的,
而是等待一个适当时间,
或者系统规定的时间间隔以后才发送,
这样就需要创建一个消息队列,以保存发送的消息,
消息队列的大小可以按照实际的需求创建,
队列又可能会满,
当队列满了,可以直接丢弃消息,
如果你觉得这样不妥,
也可以预先划分一个足够大的队列,
可以使用一个系统全局的大的消息队列,
也可以为每个对象创建一个消息队列,
这个我们的一个数据队列的实现,
开发工具vc.net,使用了C++的模板,
关于队列的算法和基础知识,我就不多说了,
DataBuffer.h
#ifndef __DATABUFFER_H__
#define __DATABUFFER_H__
#include &windows.h&
#include &assert.h&
#include &g_assert.h&
#include &stdio.h&
#ifndef HAVE_BYTE
#endif // HAVE_BYTE
//数据队列管理类
template &const int _max_line, const int _max_size&
class DataBufferTPL
bool Add( byte *data ) // 加入队列数据
G_ASSERT_RET( data, false );
m_ControlStatus =
if( IsFull() )&
//assert( false );
memcpy( m_s_ptr, data, _max_size );
NextSptr();
m_NumData++;
m_ControlStatus =
bool Get( byte *data ) // 从队列中取出数据
G_ASSERT_RET( data, false );
m_ControlStatus =
if( IsNull() )&
memcpy( data, m_e_ptr, _max_size );
NextEptr();
m_NumData--;
m_ControlStatus =
bool CtrlStatus() // 获取操作成功结果
return m_ControlS
int GetNumber() // 获得现在的数据大小
return m_NumD
DataBufferTPL()
m_NumData = 0;
m_start_ptr = m_DataTeam[0];
m_end_ptr = m_DataTeam[_max_line-1];
m_s_ptr = m_start_
m_e_ptr = m_start_
~DataBufferTPL()
m_NumData = 0;
m_s_ptr = m_start_
m_e_ptr = m_start_
bool IsFull() // 是否队列满
G_ASSERT_RET( m_NumData &=0 && m_NumData &= _max_line, false );
if( m_NumData == _max_line )&
bool IsNull() // 是否队列空
G_ASSERT_RET( m_NumData &=0 && m_NumData &= _max_line, false );
if( m_NumData == 0 )
void NextSptr() // 头位置增加
assert(m_start_ptr);
assert(m_end_ptr);
assert(m_s_ptr);
assert(m_e_ptr);
m_s_ptr += _max_
if( m_s_ptr & m_end_ptr )
m_s_ptr = m_start_
void NextEptr() // 尾位置增加
assert(m_start_ptr);
assert(m_end_ptr);
assert(m_s_ptr);
assert(m_e_ptr);
m_e_ptr += _max_
if( m_e_ptr & m_end_ptr )
m_e_ptr = m_start_
byte m_DataTeam[_max_line][_max_size]; //数据缓冲
int m_NumD //数据个数
bool m_ControlS //操作结果
byte *m_start_ //起始位置
byte *m_end_ //结束位置
byte *m_s_ //排队起始位置
byte *m_e_ //排队结束位置
//////////////////////////////////////////////////////////////////////////
// 放到这里了!
//ID自动补位列表模板,用于自动列表,无间空顺序列表。
template &const int _max_count&
class IDListTPL
// 清除重置
void Reset()&
for(int i=0;i&_max_i++)
m_dwList[i] = G_ERROR;
m_counter = 0;
int MaxSize() const { return _max_ }
int Count() const { return m_ }
const DWORD operator[]( int iIndex ) {&
G_ASSERTN( iIndex &= 0 && iIndex & m_counter );
return m_dwList[ iIndex ];&
bool New( DWORD dwID )
G_ASSERT_RET( m_counter &= 0 && m_counter & _max_count, false );
//ID 唯一性,不能存在相同ID
if ( Find( dwID ) != -1 )&
m_dwList[m_counter] = dwID;
m_counter++;
// 没有Assert的加入ID功能
bool Add( DWORD dwID )
if( m_counter &0 || m_counter &= _max_count )&
//ID 唯一性,不能存在相同ID
if ( Find( dwID ) != -1 )&
m_dwList[m_counter] = dwID;
m_counter++;
bool Del( int iIndex )
G_ASSERT_RET( iIndex &=0 && iIndex & m_counter, false );
for(int k=iIk&m_counter-1;k++)
m_dwList[k] = m_dwList[k+1];
m_dwList[k] = G_ERROR;
m_counter--;
int Find( DWORD dwID )
for(int i=0;i&m_i++)
if( m_dwList[i] == dwID )&
return -1;
IDListTPL():m_counter(0)&
for(int i=0;i&_max_i++)
m_dwList[i] = G_ERROR;
virtual ~IDListTPL()&
DWORD m_dwList[_max_count];
//////////////////////////////////////////////////////////////////////////
#endif //__DATABUFFER_H__
我们采用winsock作为网络部分的编程接口,
接下去编程者有必要学习一下socket的基本知识,
不过不懂也没有关系,我提供的代码已经把那些麻烦的细节或者正确的系统设置给弄好了,
编程者只需要按照规则编写游戏系统的处理代码就可以了,
这些代码在vc6下编译通过,
是通用的网络传输底层,
这里是socket部分的代码,
我们需要安装vc6才能够编译以下的代码,
因为接下去我们要接触越来越多的c++,
所以,大家还是去看看c++的书吧,
// socket.h
#ifndef _socket_h
#define _socket_h
#pragma once
//定义最大连接用户数目 ( 最大支持 512 个客户连接 )
#define MAX_CLIENTS 512
//#define FD_SETSIZE MAX_CLIENTS
#pragma comment( lib, &wsock32.lib& )
#include &winsock.h&
class CSocketCtrl
void SetDefaultOpt();
CSocketCtrl(): m_sockfd(INVALID_SOCKET){}
BOOL StartUp();
BOOL ShutDown();
BOOL IsIPsChange();
BOOL CanWrite();
BOOL HasData();
int Recv( char* pBuffer, int nSize, int nFlag );
int Send( char* pBuffer, int nSize, int nFlag );
BOOL Create( UINT uPort );
BOOL Create(void);
BOOL Connect( LPCTSTR lpszHostAddress, UINT nHostPort );
void Close();
BOOL Listen( int nBackLog );
BOOL Accept( CSocketCtrl& sockCtrl );
BOOL RecvMsg( char *sBuf );
int SendMsg( char *sBuf,unsigned short stSize );
SOCKET GetSockfd(){ return m_ }
BOOL GetHostName( char szHostName[], int nNameLength );
protected:
static DWORD m_dwConnectO
static DWORD m_dwReadO
static DWORD m_dwWriteO
static DWORD m_dwAcceptO
static DWORD m_dwReadB
static DWORD m_dwWriteB
// socket.cpp
#include &stdio.h&
#include &msgdef.h&
#include &socket.h&
// 吊线时间
#define ALL_TIMEOUT 120000
DWORD CSocketCtrl::m_dwConnectOut = 60000;
DWORD CSocketCtrl::m_dwReadOut = ALL_TIMEOUT;
DWORD CSocketCtrl::m_dwWriteOut = ALL_TIMEOUT;
DWORD CSocketCtrl::m_dwAcceptOut = ALL_TIMEOUT;
DWORD CSocketCtrl::m_dwReadByte = 0;
DWORD CSocketCtrl::m_dwWriteByte = 0;
// 接收数据
BOOL CSocketCtrl::RecvMsg( char *sBuf )
if( !HasData() )
return FALSE;
int nbRead = this-&Recv( (char*)&header, sizeof( header ), MSG_PEEK );
if( nbRead == SOCKET_ERROR )
return FALSE;
if( nbRead & sizeof( header ) )
this-&Recv( (char*)&header, nbRead, 0 );
printf( &\ninvalid msg, skip %ld bytes.&, nbRead );
return FALSE;
if( this-&Recv( (char*)sBuf, header.stLength, 0 ) != header.stLength )
return FALSE;
return TRUE;
// 发送数据
int CSocketCtrl::SendMsg( char *sBuf,unsigned short stSize )
static char sSendBuf[ 4000 ];
memcpy( sSendBuf,&stSize,sizeof(short) );
memcpy( sSendBuf + sizeof(short),sBuf,stSize );
if( (sizeof(short) + stSize) != this-&Send( sSendBuf,stSize+sizeof(short),0 ) )
return -1;
return stS
// 启动winsock
BOOL CSocketCtrl::StartUp()
WSADATA wsaD
WORD wVersionRequested = MAKEWORD( 1, 1 );
int err = WSAStartup( wVersionRequested, &wsaData );
if ( err != 0 )&
return FALSE;
return TRUE;
// 关闭winsock
BOOL CSocketCtrl::ShutDown()
WSACleanup();
return TRUE;
// 得到主机名
BOOL CSocketCtrl::GetHostName( char szHostName[], int nNameLength )
if( gethostname( szHostName, nNameLength ) != SOCKET_ERROR )
return TRUE;
return FALSE;
BOOL CSocketCtrl::IsIPsChange()
return FALSE;
static int iIPNum = 0;
char sHost[300];
hostent *pH
if( gethostname(sHost,299) != 0 )
return FALSE;
pHost = gethostbyname(sHost);
psHost = pHost-&h_addr_list[i++];
if( psHost == 0 )
}while(1);
if( iIPNum != i )
return TRUE;
return FALSE;
// socket是否可以写
BOOL CSocketCtrl::CanWrite()
tout.tv_sec = 0;
tout.tv_usec = 0;
FD_ZERO(&set);
FD_SET(m_sockfd,&set);
e=::select(0,NULL,&set,NULL,&tout);
if(e==SOCKET_ERROR) return FALSE;
if(e&0) return TRUE;
return FALSE;
// socket是否有数据
BOOL CSocketCtrl::HasData()
tout.tv_sec = 0;
tout.tv_usec = 0;
FD_ZERO(&set);
FD_SET(m_sockfd,&set);
e=::select(0,&set,NULL,NULL,&tout);
if(e==SOCKET_ERROR) return FALSE;
if(e&0) return TRUE;
return FALSE;
int CSocketCtrl::Recv( char* pBuffer, int nSize, int nFlag )
return recv( m_sockfd, pBuffer, nSize, nFlag );
int CSocketCtrl::Send( char* pBuffer, int nSize, int nFlag )
return send( m_sockfd, pBuffer, nSize, nFlag );
BOOL CSocketCtrl::Create( UINT uPort )
m_sockfd=::socket(PF_INET,SOCK_STREAM,0);
if(m_sockfd==INVALID_SOCKET) return FALSE;
SOCKADDR_IN SockA
memset(&SockAddr,0,sizeof(SockAddr));
SockAddr.sin_family = AF_INET;
SockAddr.sin_addr.s_addr = INADDR_ANY;
SockAddr.sin_port = ::htons( uPort );
if(!::bind(m_sockfd,(SOCKADDR*)&SockAddr, sizeof(SockAddr)))&
SetDefaultOpt();
return TRUE;
return FALSE;
void CSocketCtrl::Close()
::closesocket( m_sockfd );
m_sockfd = INVALID_SOCKET;
BOOL CSocketCtrl::Connect( LPCTSTR lpszHostAddress, UINT nHostPort )
if(m_sockfd==INVALID_SOCKET) return FALSE;
SOCKADDR_IN sockA
memset(&sockAddr,0,sizeof(sockAddr));
LPSTR lpszAscii=(LPSTR)lpszHostA
sockAddr.sin_family=AF_INET;
sockAddr.sin_addr.s_addr=inet_addr(lpszAscii);
if(sockAddr.sin_addr.s_addr==INADDR_NONE)
lphost = ::gethostbyname(lpszAscii);
if(lphost!=NULL)
sockAddr.sin_addr.s_addr = ((IN_ADDR *)lphost-&h_addr)-&s_
else return FALSE;
sockAddr.sin_port = htons((u_short)nHostPort);
int r=::connect(m_sockfd,(SOCKADDR*)&sockAddr,sizeof(sockAddr));
if(r!=SOCKET_ERROR) return TRUE;
e=::WSAGetLastError();
if(e!=WSAEWOULDBLOCK) return FALSE;
tout.tv_sec = 0;
tout.tv_usec = 100000;
while( n& CSocketCtrl::m_dwConnectOut)
FD_ZERO(&set);
FD_SET(m_sockfd,&set);
e=::select(0,NULL,&set,NULL, &tout);
if(e==SOCKET_ERROR) return FALSE;
if(e&0) return TRUE;
if( IsIPsChange() )
return FALSE;
n += 100;
return FALSE;
// 设置监听socket
BOOL CSocketCtrl::Listen( int nBackLog )
if( m_sockfd == INVALID_SOCKET ) return FALSE;
if( !listen( m_sockfd, nBackLog) ) return TRUE;
return FALSE;
// 接收一个新的客户连接
BOOL CSocketCtrl::Accept( CSocketCtrl& ms )
if( m_sockfd == INVALID_SOCKET ) return FALSE;
if( ms.m_sockfd != INVALID_SOCKET ) return FALSE;
tout.tv_sec = 0;
tout.tv_usec = 100000;
while(n& CSocketCtrl::m_dwAcceptOut)
//if(stop) return FALSE;
FD_ZERO(&set);
FD_SET(m_sockfd,&set);
e=::select(0,&set,NULL,NULL, &tout);
if(e==SOCKET_ERROR) return FALSE;
n += 100;
if( n&= CSocketCtrl::m_dwAcceptOut ) return FALSE;
ms.m_sockfd=accept(m_sockfd,NULL,NULL);
if(ms.m_sockfd==INVALID_SOCKET) return FALSE;
ms.SetDefaultOpt();
return TRUE;
BOOL CSocketCtrl::Create(void)
m_sockfd=::socket(PF_INET,SOCK_STREAM,0);
if(m_sockfd==INVALID_SOCKET) return FALSE;
SOCKADDR_IN SockA
memset(&SockAddr,0,sizeof(SockAddr));
SockAddr.sin_family = AF_INET;
SockAddr.sin_addr.s_addr = INADDR_ANY;
SockAddr.sin_port = ::htons(0);
//if(!::bind(m_sock,(SOCKADDR*)&SockAddr, sizeof(SockAddr)))&
SetDefaultOpt();
return TRUE;
return FALSE;
// 设置正确的socket状态,
// 主要是主要是设置非阻塞异步传输模式
void CSocketCtrl::SetDefaultOpt()
ling.l_onoff=1;
ling.l_linger=0;
setsockopt( m_sockfd, SOL_SOCKET, SO_LINGER, (char *)&ling, sizeof(ling));
setsockopt( m_sockfd, SOL_SOCKET, SO_REUSEADDR, 0, 0);
int bKeepAlive = 1;
setsockopt( m_sockfd, SOL_SOCKET, SO_KEEPALIVE, (char*)&bKeepAlive, sizeof(int));
BOOL bNoDelay = TRUE;
setsockopt( m_sockfd, IPPROTO_TCP, TCP_NODELAY, (char*)&bNoDelay, sizeof(BOOL));
unsigned long nonblock=1;
::ioctlsocket(m_sockfd,FIONBIO,&nonblock);
今天晚上写了一些测试代码,
想看看flash究竟能够承受多大的网络数据传输,
我在flash登陆到服务器以后,
每隔3毫秒就发送100次100个字符的串 &6789& 给flash,
然后在flash里面接收数据的函数里面统计数据,
var g_nTotalRecvByte = 0;
var g_time = new Date();&
var g_nStartTime = g_time.getTime();
var g_nCounter = 0;
mySocket.onData=function(xmlDoc)
g_nTotalRecvByte += xmlDoc.
// 每接收超过1k字节的数据,输出一次信息,
if( g_nTotalRecvByte-g_nCounter & 1024 )
g_time = new Date();
var nPassedTime = g_time.getTime()-g_nStartT
trace( &花费时间:&+nPassedTime+&毫秒& );
g_nCounter = g_nTotalRecvB
trace( &接收总数:&+g_nTotalRecvByte+&字节& );
trace( &接收速率:&+g_nTotalRecvByte*1000/nPassedTime+&字节/秒& );
结果十分令我意外,
这是截取的一段调试信息,
花费时间:6953毫秒
接收总数:343212字节
接收速率:8988字节/秒
花费时间:7109毫秒
接收总数:344323字节
接收速率:534字节/秒
花费时间:7109毫秒
接收总数:345434字节
接收速率:3878字节/秒
花费时间:8125毫秒
接收总数:400984字节
接收速率:0769字节/秒
花费时间:8125毫秒
接收总数:402095字节
接收速率:6154字节/秒
花费时间:8125毫秒
接收总数:403206字节
接收速率:1538字节/秒
我检查了几遍源程序,没有发现逻辑错误,
如果程序没有问题的话,
那么我们得出的结论是,flash的xml socket每秒可以接收至少40K的数据,
这还没有计算xmlSocket.onData事件的触发,调试代码、信息输出占用的时间。
比我想象中快了一个数量级,
flash网络游戏我们可以继续往下走了,
有朋友问到lag的问题,
问得很好,不过也不要过于担心,
lag的产生有的是因为网络延迟,
有的是因为服务器负载过大,
对于游戏的设计者和开发者来说,
首先要从设计的角度来避免或者减少lag产生的机会,
如果lag产生了,
也不要紧,找到巧妙的办法骗过玩家的眼睛,
这也有很多成熟的方法了,
比如航行预测法,路径插值等等,
都可以产生很好的效果,
还有最后的绝招,就是提高服务器的配置和网络带宽,
从我开发网络游戏这段时间的经验来看,
我们的服务器是vc开发的,
普通pc跑几百个玩家,几百个怪物是没有问题的,
又作了一个flash发送的测试,
网络游戏的特点是,
出去的信息比较少,
进来的信息比较多,
这个很容易理解,
人操作游戏的速度是很有限的,
控制指令的产生也是随机的,
但是多人游戏的话,
因为人多,信息的流量也就区域均匀分布了,
在昨天接收数据的基础上,
我略加修改,
我在_root.enterFrame写了如下代码,
_root.onEnterFrame = function()&
for( i = 0; i & 10; i++ )
mySocket.send( ConvertToMsg( &89& ) );
服务器端要做的是,
把所有从flash客户端收到的信息原封不动的返回来,
这样,我又可以通过昨天onData里面的统计算法来从侧面估算出flash发送数据的能力,
这里是输出的数据
花费时间:30531毫秒
接收总数:200236字节
接收速率:5468字节/秒
花费时间:30937毫秒
接收总数:201290字节
接收速率:6811字节/秒
花费时间:31140毫秒
接收总数:202344字节
接收速率:9904字节/秒
花费时间:31547毫秒
接收总数:203398字节
接收速率:7208字节/秒
可以看出来,发送+接收同时做,
发送速率至少可以达到5k byte/s
有一点要注意,要非常注意,
不能让flash的网络传输满载,
所谓满载就是flash在阻塞运算的时候,
不断的有数据从网络进来,
而flash又无法在预计的时间内处理我这些信息,
或者flash发送数据过于频繁,
导致服务器端缓冲溢出导致错误,
对于5k的传输速率,
已经足够了,
因为我也想不出来有什么产生这么大的数据量,
而且如果产生了这么大的数据量,
也就意味着服务器每时每刻都要处理所有的玩家发出的海量数据,
还要把这些海量数据转发给其他的玩家,
已经引起数据爆炸了,
所以,5k的上传从设计阶段就要避免的,
我想用flash做的网络游戏,
除了动作类游戏可能需要恒定1k以内的上传速率,
其他的200个字节/秒以内就可以了,
使用于Flash的消息结构定义
我们以前讨论过,
通过消息来传递信息,
消息的结构是
struct msg
short nL // 2 byte
DWORD dwId; // 4 byte
但是在为flash开发的消息中,
不能采用这种结构,
首先Flash xmlSocket只传输字符串,
从xmlSocket的send,onData函数可以看出来,
发出去的,收进来的都应该是字符串,
而在服务器端是使用vc,java等高级语言编写的,
消息中使用的是二进制数据块,
显然,简单的使用字符串会带来问题,
所以,我们需要制定一套协议,
就是无论在客户端还是服务器端,
都用统一的字符串消息,
通过解析字符串的方式来传递信息,
我想这就是flash采用xml document来传输结构化信息的理由之一,
xml document描述了一个完整的数据结构,
而且全部使用的是字符串,
原来是这样,怪不得叫做xml socket,
本来socket和xml完全是不同的概念,
flash偏偏出了个xml socket,
一开始令我费解,
现在,渐渐理解其中奥妙。
Flash Msg结构定义源代码和相关函数
在服务器端,我们为flash定义了一种msg结构,
使用语言,vc6
#define MSGMAXSIZE 512
struct MsgHeader
MsgHeader():stLength( 0 ){}
struct Msg
short GetLength(){ return header.stL }
// flash 消息
struct MsgToFlashublic Msg
// 一个足够大的缓冲,但是不会被整个发送,
char szString[MSGMAXSIZE];
// 计算设置好内容后,内部会计算将要发送部分的长度,
// 要发送的长度=消息头大小+字符串长度+1
void SetString( const char* pszChatString )
if( strlen( pszChatString ) & MSGMAXSIZE-1 )
strcpy( szString, pszChatString );
header.stLength = sizeof( header )+
(short)strlen( pszChatString )+1;
在发往flash的消息中,整个处理过后MsgToFlash结构将被发送,
实践证明,在flash 客户端的xmlSocket onData事件中,
接收到了正确的消息,消息的内容是MasToFlash的szString字段,
是一个字符串,
比如在服务器端,
msg.SetString( &move player0 to 100 100& );
SendMsg( msg,............. );
那么,在我们的flash客户端的onData( xmlDoc )中,
我们trace( xmlDoc )
move player0 to 100 100
然后是flash发送消息到服务器,
我们强调flash只发送字符串,
这个字符串无论是否内部拥有有效数据,
服务器都应该首先把消息收下来,
那就要保证发送给服务器的消息遵循统一的结构,
在flash客户端中,
我们定义一个函数,
这个函数把一个字符串转化为服务器可以识别的消息,
补充:现在我们约定字符串长度都不大于97个字节长度,
var num_table = new array( &0&,&1&,&2&,&3&,&4&,&5&,&6&,&7&,&8&,&9& );
function ConvertToMsg( str )
var l = str.length+3;&
var t = &&;
if( l & 10 )
t = num_table[Math.floor(l/10)]+num_table[Number(l%10)]+
t = num_table[0]+num_table[l]+
var msg = ConvertToMsg( &client login& );
我们trace( msg );
15client login
为什么是这个结果呢?
15是消息的长度,
头两个字节是整个消息的长度的asc码,意思是整个消息有15个字节长,
然后是信息client login,
最后是一个0(c语言中的字符串结束符)
当服务器收到15client login,
他首先把15给分析出来,
把&15&字符串转化为15的数字,
然后,根据15这个长度把后面的client login读出来,
这样,网络传输的底层就完成了,
client login的处理就交给逻辑层,
谢谢大家的支持,
很感谢斑竹把这个贴子置顶,
我写这文章的过程也是我自己摸索的过程,
文章可以记录我一段开发的历史,
一个思考分析的历程,
有时候甚至作为日志来写,
由于我本身有杂务在身,
所以贴子的更新有点慢,
请大家见谅,
我喜爱flash,
虽然我在帝国中,但我并不能称之为闪客,
因为我制作flash的水平实在很低,
但是我想设计开发出让其他人能更好的使用flash的工具,
前阵子我开发了Match3D,
一个可以把三维动画输出成为swf的工具,
而且实现了swf渲染的实时三维角色动画,
这可以说是我真正推出的第一个flash第三方软件,
其实这以前,
我曾经开发过几个其他的flash第三方软件,
都中途停止了,
因为不实用或者市场上有更好的同类软件,
随着互联网的发展,
flash的不断升级,
我的flash第三方软件目光渐渐的从美术开发工具转移到网络互连,
web应用上面来,
如今已经到了2004版本,
flash的种种新特性让我眼前发光,
我最近在帝国的各个板块看了很多贴子,
分析里面潜在的用户需求,
总结了以下的几个我认为比较有意义的选题,
可能很片面,
flash源代码保护,主要是为了抵御asv之类的软件进行反编译和萃取
flash与远端数据库的配合,应该出现一个能够方便快捷的对远程数据库进行操作的方法或者控件,
flash网际互连,我认为flash网络游戏是一块金子,
这里我想谈谈flash网络游戏,
我要谈的不仅仅是技术,而是一个概念,
用flash网络游戏,
我本身并不想把flash游戏做成rpg或者其他剧烈交互性的游戏,
而是想让flash实现那些节奏缓慢,玩法简单的游戏,
把网络的概念带进来,
你想玩游戏的时候,登上flash网络游戏的网站,
选择你想玩的网络游戏,
因为现在几乎所有上网的电脑都可以播放swf,
所以,我们几乎不用下载任何插件,
输入你的账号和密码,
就可以开始玩了,
我觉得battle.net那种方式很适合flash,
开房间或者进入别人开的房间,
然后2个人或者4个人就可以交战了,
这种游戏可以是棋类,这是最基本的,
用户很广泛,
我脑海中的那种是类似与宠物饲养的,
就像当年的电子宠物,
每个玩家都可以到服务器认养宠物,
然后在线养成宠物,
还可以邀请别的玩家进行宠物比武,
看谁的宠物厉害,
就这样简简单单的模式,
配合清新可爱的画面,
趣味的玩法,
加入网络的要素,
也许可以取得以想不到的效果,
今天就说到这里吧,
想法那么多,要实现的话还有很多路要走,
希望大家多多支持,积极参与,
让我们的想法不仅仅停留于纸上。
非常抱歉,
都很长时间没有回贴了,
因为手头项目的原因,
几乎没有时间做flash multiplayer的研究,
很感谢大家的支持,
现在把整个flash networking的源代码共享出来,
大家可以任意的使用,
其实里面也没有多少东西,
相信感兴趣的朋友还是可以从中找到一些有用的东西,
这一次的源代码做的事情很简单,
服务器运行,
客户端登陆到服务器,
然后客户端不断的发送字符串给服务器,
服务器收到后,在发还给客户端,
客户端统计一些数据,
posted @&&暗夜教父 阅读(160) |&&|&&
原文:/freshmen/783449.html
&&& 要想在修改游戏中做到百战百胜,是需要相当丰富的计算机知识的。有很多计算机高手就是从玩游戏,修改游戏中,逐步对计算机产生浓厚的兴趣,逐步成长起来 的。不要在羡慕别人能够做到的,因为别人能够做的你也能够!我相信你们看了本教程后,会对游戏有一个全新的认识,呵呵,因为我是个好老师!(别拿鸡蛋砸我 呀,救命啊!#¥%……*)
&&& 不过要想从修改游戏中学到知识,增加自己的计算机水平,可不能只是靠修改游戏呀! 要知道,修改游戏只是一个验证你对你所了解的某些计算机知识的理解程度的场所,只能给你一些发现问题、解决问题的机会,只能起到帮助你提高学习计算机的兴 趣的作用,而决不是学习计算机的捷径。
&&& 一:什么叫外挂?
&&& 现在的网络游戏多是基于Internet上客户/服务器模式,服务端程序运行在游戏服务器上,游戏的设计者在其中创造一个庞大的游戏空间,各地的玩家可以通过运行客户端程序同时登录到游戏中。简单地说,网络游戏实际上就是由游戏开发商
提供一个游戏环境,而玩家们就是在这个环境中相对自由和开放地进行游戏操作。那么既然在网络游戏中有了服务器这个概念,我们以前传统的修改游戏方法就显得 无能为力了。记得我们在单机版的游戏中,随心所欲地通过内存搜索来修改角色的各种属性,这在网络游戏中就没有任何用处了。因为我们在网络游戏中所扮演角色 的各种属性及各种重要资料都存放在服务器上,在我们自己机器上(客户端)只是显示角色的状态,所以通过修改客户端内存里有关角色的各种属性是不切实际的。 那么是否我们就没有办法在网络游戏中达到我们修改的目的?回答是&否&.我们知道Internet客户/服务器模式的通讯一般采用TCP/IP通信协议,数据交换是通过IP数据包的传输来实现的,一般来说我们客户端向服务器发出某些请求,比如移动、战斗等指令都是通过封包的形式和服务器交换数
据。那么我们把本地发出消息称为SEND,意思就是发送数据,服务器收到我们SEND的消息后,会按照既定的程序把有关的信息反馈给客户端,比如,移动的 坐标,战斗的类型。那么我们把客户端收到服务器发来的有关消息称为RECV.知道了这个道理,接下来我们要做的工作就是分析客户端和服务器之间往来的数据 (也就是封包),这样我们就可以提取到对我们有用的数据进行修改,然后模拟服务器发给客户端,或者模拟客户端发送给服务器,这样就可以实现我们修改游戏的 目的了。
&&& 目前除了修改游戏封包来实现修改游戏的目的,我们也可以修改客户端的有关程序来达到我们的要求。我们知道目前各个服务器的运算能力是有限的,特别在游戏 中,游戏服务器要计算游戏中所有玩家的状况几乎是不可能的,所以有一些运算还是要依靠我们客户端来完成,这样又给了我们修改游戏提供了一些便利。比如我们 可以通过将客户端程序脱壳来发现一些程序的判断分支,通过跟踪调试我们可以把一些对我们不利的判断去掉,以此来满足我们修改游戏的需求。 在下几个章节中,我们将给大家讲述封包的概念,和修改跟踪客户端的有关知识。大家准备好了吗?
&&& 游戏数据格式和存储:
&&& 在进行我们的工作之前,我们需要掌握一些关于计算机中储存数据方式的知识和游戏中储存数据的特点。本章节是提供给菜鸟级的玩家看的,如果你是高手就可以跳 过了,如果,你想成为无坚不摧的剑客,那么,这些东西就会花掉你一些时间;如果,你只想作个江湖的游客的话,那么这些东西,了解与否无关紧要。是作剑客, 还是作游客,你选择吧!
&&& 现在我们开始!首先,你要知道游戏中储存数据的几种格式,这几种格式是:字节(BYTE)、字(WORD)和双字(DOUBLE WORD),或者说是8位、16位和32位储存方式。字节也就是8位方式能储存0~255的数字;字或说是16位储存方式能储存0~65535的数;双字 即32位方式能储存0~的数。
&&& 为何要了解这些知识呢?在游戏中各种参数的最大值是不同的,有些可能100左右就够了,比如,金庸群侠传中的角色的等级、随机遇敌个数等等。而有些却需要 大于255甚至大于65535,象金庸群侠传中角色的金钱值可达到数百万。所以,在游戏中各种不同的数据的类型是不一样的。在我们修改游戏时需要寻找准备 修改的数据的封包,在这种时候,正确判断数据的类型是迅速找到正确地址的重要条件。
&&& 在计算机中数据以字节为基本的储存单位,每个字节被赋予一个编号,以确定各自的位置。这个编号我们就称为地址。
&&& 在需要用到字或双字时,计算机用连续的两个字节来组成一个字,连续的两个字组成一个双字。而一个字或双字的地址就是它们的低位字节的地址。 现在我们常用的Windows&9x操作系统中,地址是用一个32位的二进制数表示的。而在平时我们用到内存地址时,总是用一个8位的16进制数来表示它。
&&& 二进制和十六进制又是怎样一回事呢?
&&& 简单说来,二进制数就是一种只有0和1两个数码,每满2则进一位的计数进位法。同样,16进制就是每满十六就进一位的计数进位法。16进制有0——F十六 个数字,它为表示十到十五的数字采用了A、B、C、D、E、F六个数字,它们和十进制的对应关系是:A对应于10,B对应于11,C对应于12,D对应于 13,E对应于14,F对应于15.而且,16进制数和二进制数间有一个简单的对应关系,那就是;四位二进制数相当于一位16进制数。比如,一个四位的二 进制数1111就相当于16进制的F,1010就相当于A.了解这些基础知识对修改游戏有着很大的帮助,下面我就要谈到这个问题。由于在计算机中数据是以
二进制的方式储存的,同时16进制数和二进制间的转换关系十分简单,所以大部分的修改工具在显示计算机中的数据时会显示16进制的代码,而且在你修改时也 需要输入16进制的数字。你清楚了吧?
&&& 在游戏中看到的数据可都是十进制的,在要寻找并修改参数的值时,可以使用Windows提供的计算器来进行十进制和16进制的换算,我们可以在开始菜单里的程序组中的附件中找到它。
&&& 现在要了解的知识也差不多了!不过,有个问题在游戏修改中是需要注意的。在计算机中数据的储存方式一般是低位数储存在低位字节,高位数储存在高位字节。比如,十进制数41715转换为16进制的数为A2F3,但在计算机中这个数被存为F3A2.
&&& 看了以上内容大家对数据的存贮和数据的对应关系都了解了吗? 好了,接下来我们要告诉大家在游戏中,封包到底是怎么一回事了,来!大家把袖口卷起来,让我们来干活吧!
&&& 二:什么是封包?
&&& 怎么截获一个游戏的封包?怎么去检查游戏服务器的ip地址和端口号? Internet用户使用的各种信息服务,其通讯的信息最终均可以归结为以IP包为单位的信息传送,IP包除了包括要传送的数据信息外,还包含有信息要发 送到的目的IP地址、信息发送的源IP地址、以及一些相关的控制信息。当一台路由器收到一个IP数据包时,它将根据数据包中的目的IP地址项查找路由表,根据查找的结果将此IP数据包送往对应端口。下一台IP路由器收到此数据包后继续转发,直至发到目的地。路由器之间可以通过路由协议来进行路由信息的交换,从而更新路由表。
&&& 那么我们所关心的内容只是IP包中的数据信息,我们可以使用许多监听网络的工具来截获客户端与服务器之间的交换数据,下面就向你介绍其中的一种工具:WPE. WPE使用方法:执行WPE会有下列几项功能可选择:
&&& SELECT GAME选择目前在记忆体中您想拦截的程式,您只需双击该程式名称即可。
&&& TRACE追踪功能。用来追踪撷取程式送收的封包。WPE必须先完成点选欲追踪的程式名称,才可以使用此项目。 按下Play键开始撷取程式收送的封包。您可以随时按下 | | 暂停追踪,想继续时请再按下 | | .按下正方形可以停止撷取封包并且显示所有已撷取封包内容。若您没按下正方形停止键,追踪的动作将依照OPTION里的设定值自动停止。如果您没有撷取到 资料,试试将OPTION里调整为Winsock Version 2.WPE 及 Trainers 是设定在显示至少16 bits 颜色下才可执行。
&&& FILTER过滤功能。用来分析所撷取到的封包,并且予以修改。
&&& SEND PACKET送出封包功能。能够让您送出假造的封包。
&&& TRAINER MAKER制作修改器。
&&& OPTIONS设定功能。让您调整WPE的一些设定值。
&&& FILTER的详细教学
&&& - 当FILTER在启动状态时 ,ON的按钮会呈现红色。- 当您启动FILTER时,您随时可以关闭这个视窗。FILTER将会留在原来的状态,直到您再按一次 on / off 钮。- 只有FILTER启用钮在OFF的状态下,才可以勾选Filter前的方框来编辑修改。- 当您想编辑某个Filter,只要双击该Filter的名字即可。
&&& NORMAL MODE:
&&& 范例:
&&& 当您在 Street Fighter Online ﹝快打旋风线上版﹞游戏中,您使用了两次火球而且击中了对方,这时您会撷取到以下的封包:SEND-&
21 06 01 04 SEND-&
87 00 67 FF A4 AA 11 22 00 00 00 00 SEND-&
11 09 11 09 SEND-&
C1 10 00 00 FF 52 44 SEND-&
C1 10 00 00 66
52 44您的第一个火球让对方减了16滴﹝16 = 10h﹞的生命值,而您观察到第4跟第5个封包的位置4有10h的值出现,应该就是这里了。
&&& 您观察10h前的0A 09 C1在两个封包中都没改变,可见得这3个数值是发出火球的关键。
&&& 因此您将0A 09 C1 10填在搜寻列﹝SEARCH﹞,然后在修改列﹝MODIFY﹞的位置4填上FF.如此一来,当您再度发出火球时,FF会取代之前的10,也就是攻击力为255的火球了!
&&& ADVANCED MODE:范例: 当您在一个游戏中,您不想要用真实姓名,您想用修改过的假名传送给对方。在您使用TRACE后,您会发现有些封包里面有您的名字出现。假设您的名字是 Shadow,换算成16进位则是﹝53 68 61 64 6F 77﹞;而您打算用moon﹝6D6F 6F 6E 20 20﹞来取代他。1) SEND-&
21 06 01 042) SEND-&
99 53 68 61 64 6F 77 00 01 05 3) SEND-& 0000
03 84 11 09 11 094) SEND-&
C1 10 00 53 68 61 64 6F 77 00 11 5) SEND-&
C1 10 00 00 66 52 44但是您仔细看,您的名字在每个封包中并不是出现在相同的位置上- 在第2个封包里,名字是出现在第4个位置上- 在第4个封包里,名字是出现在第6个位置上在这种情况下,您就需要使用ADVANCED MODE- 您在搜寻列﹝SEARCH﹞填上:53 68 61 64 6F 77 ﹝请务必从位置1开始填﹞-
您想要从原来名字Shadow的第一个字母开始置换新名字,因此您要选择从数值被发现的位置开始替代连续数值﹝from the position of the chain found﹞.- 现在,在修改列﹝MODIFY﹞000的位置填上:6D 6F 6F 6E 20 20 ﹝此为相对应位置,也就是从原来搜寻栏的+001位置开始递换﹞- 如果您想从封包的第一个位置就修改数值,请选择﹝from the beginning of the packet﹞了解一点TCP/IP协议常识的人都知道,互联网是
将信息数据打包之后再传送出去的。每个数据包分为头部信息和数据信息两部分。头部信息包括数据包的发送地址和到达地址等。数据信息包括我们在游戏中相关操 作的各项信息。那么在做截获封包的过程之前我们先要知道游戏服务器的IP地址和端口号等各种信息,实际上最简单的是看看我们游戏目录下,是否有一个 SERVER.INI的配置文件,这个文件里你可以查看到个游戏服务器的IP地址,比如金庸群侠传就是如此,那么除了这个我们还可以在DOS下使用 NETSTAT这个命令, NETSTAT命令的功能是显示网络连接、路由表和网络接口信息,可以让用户得知目前都有哪些网络连接正在运作。或者你可以使用木马客星等工具来查看网络
连接。工具是很多的,看你喜欢用哪一种了。
&&& NETSTAT命令的一般格式为:NETSTAT [选项]命令中各选项的含义如下:-a 显示所有socket,包括正在监听的。-c 每隔1秒就重新显示一遍,直到用户中断它。
&&& -i 显示所有网络接口的信息。-n 以网络IP地址代替名称,显示出网络连接情形。-r 显示核心路由表,格式同&route -e&.-t 显示TCP协议的连接情况。-u 显示UDP协议的连接情况。-v 显示正在进行的工作。
&&& 三:怎么来分析我们截获的封包?
&&& 首先我们将WPE截获的封包保存为文本文件,然后打开它,这时会看到如下的数据(这里我们以金庸群侠传里PK店小二客户端发送的数据为例来讲解):第一个 文件:SEND-&
0D 22 7E 6B E4 17 13 13 12 13 12 13 67 1BSEND-&
DD 34 12 12 12 12 17 12 0E 12 12 12 9BSEND-&
1E F1 29 06 17 12 3B 0E 17 1ASEND-& 0000 E6
56 1B C0 68 12 12 12 5ASEND-&
02 C8 13 C9 7E 6B E4 17 10 35 27 13 12 12SEND-&
17 C9 12第二个文件:SEND-&
68 47 1B 0E 81 72 76 76 77 76 77 76 02 7ESEND-&
07 1C 77 77 77 77 72 77 72 77 77 77 6DSEND-&
63 72 77 5E 6B 72 F3SEND-&
7E A5 21 77 77 77 3FSEND-&
67 AD 76 CF 1B 0E 81 72 75 50 42 76 77 77SEND-&
72 AC 77我们发现两次PK店小二的数据格式一样,但是内容却不相同,我们是PK的同一个NPC,为什么会不同呢? 原来金庸群侠传的封包是经过了加密运算才在网路上传输的,那么我们面临的问题就是如何将密文解密成明文再分析了。
&&& 因为一般的数据包加密都是异或运算,所以这里先讲一下什么是异或。 简单的说,异或就是&相同为0,不同为1&(这是针对二进制按位来讲的),举个例子,异或,我们按位对比,得到异或结果是0011,计 算的方法是:0001的第4位为0,0010的第4位为0,它们相同,则异或结果的第4位按照&相同为0,不同为1&的原则得到0,0001的第3位为 0,0010的第3位为0,则异或结果的第3位得到0,0001的第2位为0,0010的第2位为1,则异或结果的第2位得到1,0001的第1位为 1,0010的第1位为0,则异或结果的第1位得到1,组合起来就是0011.异或运算今后会遇到很多,大家可以先熟悉熟悉,熟练了对分析很有帮助的。
&&& 下面我们继续看看上面的两个文件,按照常理,数据包的数据不会全部都有值的,游戏开发时会预留一些字节空间来便于日后的扩充,也就是说数据包里会存在一些&00&的字节,观察上面的文件,我们会发现文件一里很多&12&,文件二里很多&77&,那么这是不是代表我们说的&00&呢?推理到这里,我们就开始行动吧!
&&& 我们把文件一与&12&异或,文件二与&77&异或,当然用手算很费事,我们使用&M2M 1.0 加密封包分析工具&来计算就方便多了。得到下面的结果:第一个文件:1 SEND-&
1F 30 6C 79 F6 05 01 01 00 01 00 01 75 09SEND-&
CF 26 00 00 00 00 05 00 1C 00 00 00 892 SEND-&
0C E3 3B 13 05 00 29 1C 05 083 SEND-&
09 D2 7A 00 00 00 484 SEND-&
10 DA 01 DB 6C 79 F6 05 02 27 35 01 00 005 SEND-&
05 DB 00第二个文件:1 SEND-&
1F 30 6C 79 F6 05 01 01 00 01 00 01 75 09SEND-&
70 6B 00 00 00 00 05 00 05 00 00 00 1A2 SEND-& 0000
F4 44 0C E3 3B 13 05 00 29 1C 05 843 SEND-&
09 D2 56 00 00 00 484 SEND-&
10 DA 01 B8 6C 79 F6 05 02 27 35 01 00 005 SEND-&
05 DB 00哈,这一下两个文件大部分都一样啦,说明我们的推理是正确的,上面就是我们需要的明文!
&&& 接下来就是搞清楚一些关键的字节所代表的含义,这就需要截获大量的数据来分析。
&&& 首先我们会发现每个数据包都是&F4 44&开头,第3个字节是变化的,但是变化很有规律。我们来看看各个包的长度,发现什么没有?对了,第3个字节就是包的长度! 通过截获大量的数据包,我们判断第4个字节代表}

我要回帖

更多推荐

版权声明:文章内容来源于网络,版权归原作者所有,如有侵权请点击这里与我们联系,我们将及时删除。

点击添加站长微信