1、游戏外挂的原理外挂现在分为好多种,比如模拟键盘的,鼠标的,修改数据包的,还有修改本地内存的,但好像没有修改服务器内存的哦,呵呵。其实修改服务器也是有办法的,只是技术太高一般人没有办法入手而已。(比如请GM去夜总会、送礼、收黑钱等等办法都可以修改服务器数据,哈哈)修改游戏无非是修改一下本地内存的数据,或者截获API函数等等。这里我把所能想到的方法都作一个介绍,希望大家能做出很好的外挂来使游戏厂商更好的完善自己的技术。我见到一篇文章是讲魔力宝贝的理论分析,写得不错,大概是那个样子。下来我就讲解一下技术方面的东西,以作引玉之用。2技术分析部分2.1模拟键盘或鼠标的响应我们一般使用:UINTSendInput(UINTnInputs, //countofinputeventsPINPUTpInputs, //arrayofinputeventsintcbSize //sizeofstructure);API函数。第一个参数是说明第二个参数的矩阵的维数的,第二个参数包含了响应事件,这个自己填充就可以,最后是这个结构的大小,非常简单,这是最简单的方法模拟键盘鼠标了,呵呵。注意,这个函数还有个替代函数:VOIDkeybd_event(BYTEbVk, //虚拟键码BYTEbScan, //扫描码DWORDdwFlags, ULONG_PTRdwExtraInfo //附加键状态);与VOIDmouse_event(DWORDdwFlags, //motionandclickoptionsDWORDdx, //horizontalpositionorchangeDWORDdy, //verticalpositionorchangeDWORDdwData, //wheelmovementULONG_PTRdwExtraInfo //application-definedinformation);这两个函数非常简单了,我想那些按键精灵就是用的这个吧。上面的是模拟键盘,下面的是模拟鼠标的。这个仅仅是模拟部分,要和游戏联系起来我们还需要找到游戏的窗口才行,或者包含快捷键,就象按键精灵的那个激活键一样,我们可以用GetWindow函数来枚举窗口,也可以用Findwindow函数来查找制定的窗口(注意,还有一个FindWindowEx),FindwindowEx可以找到窗口的子窗口,比如按钮,等什么东西。当游戏切换场景的时候我们可以用FindWindowEx来确定一些当前窗口的特征,从而判断是否还在这个场景,方法很多了,比如可以GetWindowInfo来确定一些东西,比如当查找不到某个按钮的时候就说明游戏场景已经切换了,等等办法。有的游戏没有控件在里面,这是对图像做坐标变换的话,这种方法就要受到限制了。这就需要我们用别的办法来辅助分析了。至于快捷键我们要用动态连接库实现了,里面要用到hook技术了,这个也非常简单。大家可能都会了,其实就是一个全局的hook对象然后SetWindowHook就可以了,回调函数都是现成的,而且现在网上的例子多如牛毛。这个实现在外挂中已经很普遍了。如果还有谁不明白,那就去看看MSDN查找SetWindowHook就可以了。不要低估了这个动态连接库的作用,它可以切入所有的进程空间,也就是可以加载到所有的游戏里面哦,只要用对, 你会发现很有用途的。这个需要你复习一下Win32编程的基础知识了。呵呵,赶快去看书吧。2.2截获消息有些游戏的响应机制比较简单,是基于消息的,或者用什么定时器的东西。这个时候你就可以用拦截消息来实现一些有趣的功能了。我们拦截消息使用的也是hook技术,里面包括了键盘消息,鼠标消息,系统消息,日志等,别的对我们没有什么大的用处,我们只用拦截消息的回调函数就可以了,这个不会让我写例子吧。其实这个和上面的一样,都是用SetWindowHook来写的,看看就明白了很简单的。至于拦截了以后做什么就是你的事情了,比如在每个定时器消息里面处理一些我们的数据判断,或者在定时器里面在模拟一次定时器,那么有些数据就会处理两次,呵呵。后果嘛,不一定是好事情哦,呵呵,不过如果数据计算放在客户端的游戏就可以真的改变数据了,呵呵,试试看吧。用途还有很多,自己想也可以想出来的,呵呵。2.3拦截Socket包这个技术难度要比原来的高很多。首先我们要替换WinSock.DLL或者WinSock32.DLL,我们写的替换函数要和原来的函数一致才行,就是说它的函数输出什么样的,我们也要输出什么样子的函数,而且参数,参数顺序都要一样才行,然后在我们的函数里面调用真正的WinSock32.DLL里面的函数就可以了。首先:我们可以替换动态库到系统路径。其次:我们应用程序启动的时候可以加载原有的动态库,用这个函数LoadLibary然后定位函数入口用GetProcAddress函数获得每个真正Socket函数的入口地址。当游戏进行的时候它会调用我们的动态库,然后从我们的动态库中处理完毕后才跳转到真正动态库的函数地址,这样我们就可以在里面处理自己的数据了,应该是一切数据。呵呵,兴奋吧,拦截了数据包我们还要分析之后才能进行正确的应答,不要以为这样工作就完成了,还早呢。等分析完毕以后我们还要仿真应答机制来和服务器通信,一个不小心就会被封号。分析数据才是工作量的来源呢,游戏每次升级有可能加密方式会有所改变,因此我们写外挂的人都是亡命之徒啊,被人愚弄了还不知道。2.4截获API上面的技术如果可以灵活运用的话我们就不用截获API函数了,其实这种技术是一种补充技术。比如我们需要截获Socket以外的函数作为我们的用途,我们就要用这个技术了,其实我们也可以用它直接拦截在Socket中的函数,这样更直接。现在拦截API的教程到处都是,我就不列举了,我用的比较习惯的方法是根据输入节进行拦截的,这个方法可以用到任何一种操作系统上,比如Windows98/2000等,有些方法不是跨平台的,我不建议使用。这个技术大家可以参考《Windows核心编程》里面的545页开始的内容来学习,如果是Win98系统可以用“Windows系统奥秘”那个最后一章来学习。网络游戏通讯模型初探[ 文章导读 ]本文就将围绕三个主题来给大家讲述一下网络游戏的网络互连实现方法序言网络游戏,作为游戏与网络有机结合的产物,把玩家带入了新的娱乐领域。网络游戏在中国开始发展至今也仅有3,4年的历史,跟已经拥有几十年开发历史的单机游戏相比,网络游戏还是非常年轻的。当然,它的形成也是根据历史变化而产生的可以说没有互联网的兴起,也就没有网络游戏的诞生。作为新兴产物,网络游戏的开发对广大开发者来说更加神秘,对于一个未知领域,开发者可能更需要了解的是网络游戏与普通单机游戏有何区别,网络游戏如何将玩家们连接起来,以及如何为玩家提供一个互动的娱乐环境。本文就将围绕这三个主题来给大家讲述一下网络游戏的网络互连实现方法。网络游戏与单机游戏说到网络游戏,不得不让人联想到单机游戏,实际上网络游戏的实质脱离不了单机游戏的制作思想,网络游戏和单机游戏的差别大家可以很直接的想到:不就是可以多人连线吗?没错,但如何实现这些功能,如何把网络连线合理的融合进单机游戏,就是我们下面要讨论的内容。在了解网络互连具体实现之前,我们先来了解一下单机与网络游戏它们各自的运行流程,只有了解这些,你才能深入网络游戏开发的核心。现在先让我们来看一下普通单机游戏的简化执行流程:Initialize()//初始化模块{ 初始化游戏数据; }Game()//游戏循环部分{ 绘制游戏场景、人物以及其它元素;获取用户操作输入;switch(用户输入数据){ case移动: { 处理人物移动; }break;case攻击: { 处理攻击逻辑: }break;...其它处理响应;...default: break; }游戏的NPC等逻辑AI处理; }Exit()//游戏结束{ 释放游戏数据;离开游戏; }我们来说明一下上面单机游戏的流程。首先,不管是游戏软件还是其他应用软件,初始化部分必不可少,这里需要对游戏的数据进行初始化,包括图像、声音以及一些必备的数据。接下来,我们的游戏对场景、人物以及其他元素进行循环绘制,把游戏世界展现给玩家,同时接收玩家的输入操作,并根据操作来做出响应,此外,游戏还需要对NPC以及一些逻辑AI进行处理。最后,游戏数据被释放,游戏结束。网络游戏与单机游戏有一个很显著的差别,就是网络游戏除了一个供操作游戏的用户界面平台(如单机游戏)外,还需要一个用于连接所有用户,并为所有用户提供数据服务的服务器,从某些角度来看,游戏服务器就像一个大型的数据库,提供数据以及数据逻辑交互的功能。让我们来看看一个简单的网络游戏模型执行流程:客户机:Login()//登入模块{ 初始化游戏数据; 获取用户输入的用户和密码; 与服务器创建网络连接; 发送至服务器进行用户验证; ... 等待服务器确认消息; ... 获得服务器反馈的登入消息; if(成立) 进入游戏; else 提示用户登入错误并重新接受用户登入; }Game()//游戏循环部分{ 绘制游戏场景、人物以及其它元素; 获取用户操作输入; 将用户的操作发送至服务器; ... 等待服务器的消息; ... 接收服务器的反馈信息; switch(服务器反馈的消息数据) { case本地玩家移动的消息: { if(允许本地玩家移动) 客户机处理人物移动; else 客户机保持原有状态; } break; case其他玩家/NPC的移动消息: { 根据服务器的反馈信息进行其他玩家或者NPC的移动处理; } break; case新玩家加入游戏: { 在客户机中添加显示此玩家; } break; case玩家离开游戏: { 在客户机中销毁此玩家数据; } break; ... 其它消息类型处理; ... default: break; } }Exit()//游戏结束{ 发送离开消息给服务器; ... 等待服务器确认; ... 得到服务器确认消息; 与服务器断开连接; 释放游戏数据; 离开游戏; } 服务器:Listen() //游戏服务器等待玩家连接模块{ ... 等待用户的登入信息; ... 接收到用户登入信息; 分析用户名和密码是否符合; if(符合) { 发送确认允许进入游戏消息给客户机; 把此玩家进入游戏的消息发布给场景中所有玩家; 把此玩家添加到服务器场景中; } else { 断开与客户机的连接; } }Game() //游戏服务器循环部分{ ... 等待场景中玩家的操作输入; ... 接收到某玩家的移动输入或NPC的移动逻辑输入; //此处只以移动为例 进行此玩家/NPC在地图场景是否可移动的逻辑判断; if(可移动) { 对此玩家/NPC进行服务器移动处理; 发送移动消息给客户机; 发送此玩家的移动消息给场景上所有玩家; } else 发送不可移动消息给客户机; }Exit() //游戏服务=器结束{ 接收到玩家离开消息; 将此消息发送给场景中所有玩家; 发送允许离开的信息; 将玩家数据存入数据库; 注销此玩家在服务器内存中的数据; } } 让我们来说明一下上面简单网络游戏模型的运行机制。先来讲讲服务器端,这里服务器端分为三个部分(实际上一个完整的网络游戏远不止这些):登入模块、游戏模块和登出模块。登入模块用于监听网络游戏客户端发送过来的网络连接消息,并且验证其合法性,然后在服务器中创建这个玩家并且把玩家带领到游戏模块中;游戏模块则提供给玩家用户实际的应用服务,我们在后面会详细介绍这个部分;在得到玩家要离开游戏的消息后,登出模块则会把玩家从服务器中删除,并且把玩家的属性数据保存到服务器数据库中,如:经验值、等级、生命值等。 接下来让我们看看网络游戏的客户端。这时候,客户端不再像单机游戏一样,初始化数据后直接进入游戏,而是在与服务器创建连接,并且获得许可的前提下才进入游戏。除此之外,网络游戏的客户端游戏进程需要不断与服务器进行通讯,通过与服务器交换数据来确定当前游戏的状态,例如其他玩家的位置变化、物品掉落情况。同样,在离开游戏时,客户端会向服务器告知此玩家用户离开,以便于服务器做出相应处理。以上用简单的伪代码给大家阐述了单机游戏与网络游戏的执行流程,大家应该可以清楚看出两者的差别,以及两者间相互的关系。我们可以换个角度考虑,网络游戏就是把单机游戏的逻辑运算部分搬移到游戏服务器中进行处理,然后把处理结果(包括其他玩家数据)通过游戏服务器返回给连接的玩家。网络互连 在了解了网络游戏基本形态之后,让我们进入真正的实际应用部分。首先,作为网络游戏,除了常规的单机游戏所必需的东西之外,我们还需要增加一个网络通讯模块,当然,这也是网络游戏较为主要的部分,我们来讨论一下如何实现网络的通讯模块。 一个完善的网络通讯模块涉及面相当广,本文仅对较为基本的处理方式进行讨论。网络游戏是由客户端和服务器组成,相应也需要两种不同的网络通讯处理方式,不过也有相同之处,我们先就它们的共同点来进行介绍。我们这里以MicrosoftWindows2000[ 2000Server ]作为开发平台,并且使用Winsock作为网络接口(可能一些朋友会考虑使用DirectPlay来进行网络通讯,不过对于当前在线游戏,DirectPlay并不适合,具体原因这里就不做讨论了)。 确定好平台与接口后,我们开始进行网络连接创建之前的一些必要的初始化工作,这部分无论是客户端或者服务器都需要进行。让我们看看下面的代码片段:WORDwVersionRequested;WSADATAwsaData;wVersionRequestedMAKEWORD(1, 1);if(WSAStartup(wVersionRequested, &wsaData)!0){ Failed(WinSockVersionError!" ); }
上面通过调用Windows的socket API函数来初始化网络设备,接下来进行网络Socket的创建,代码片段如下:
SOCKET sSocket socket( AF_INET, m_lProtocol, 0 ); if( sSocket == INVALID_SOCKET ) { Failed( "WinSocketCreateError!" ); }
这里需要说明,客户端和服务端所需要的Socket连接数量是不同的,客户端只需要一个Socket连接足以满足游戏的需
要,而服务端必须为每个玩家用户创建一个用于通讯的Socket连接。当然,并不是说如果服务器上没有玩家那就不需要创
建Socket连接,服务器端在启动之时会生成一个特殊的Socket用来对玩家创建与服务器连接的请求进行响应,等介绍网络
监听部分后会有更详细说明。
有初始化与创建必然就有释放与删除,让我们看看下面的释放部分:
if( sSocket != INVALID_SOCKET ) { closesocket( sSocket ); } if( WSACleanup() != 0 ) { Warning( "Can'treleaseWinsocket" ); }
这里两个步骤分别对前面所作的创建初始化进行了相应释放。
接下来看看服务器端的一个网络执行处理,这里我们假设服务器端已经创建好一个Socket供使用,我们要做的就是让
这个Socket变成监听网络连接请求的专用接口,看看下面代码片段:
SOCKADDR_IN addr; memset( &addr, 0, sizeof(addr) ); addr.sin_family = AF_INET; addr.sin_addr.s_addr = htonl( INADDR_ANY ); addr.sin_port = htons( Port ); // Port为要监听的端口号 // 绑定socket if( bind( sSocket, (SOCKADDR*)&addr, sizeof(addr) ) == SOCKET_ERROR ) { Failed( "WinSocketBindError!"); } // 进行监听 if( listen( sSocket, SOMAXCONN ) == SOCKET_ERROR ) { Failed( "WinSocketListenError!"); }
这里使用的是阻塞式通讯处理,此时程序将处于等待玩家用户连接的状态,倘若这时候有客户端连接进来,则通过
accept()来创建针对此玩家用户的Socket连接,代码片段如下:
sockaddraddrServer; int nLen sizeof( addrServer ); SOCKET sPlayerSocket accept( sSocket, &addrServer, &nLen ); if( sPlayerSocket == INVALID_SOCKET ) { Failed( WinSocket Accept Error!"); } 这里我们创建了sPlayerSocket连接,此后游戏服务器与这个玩家用户的通讯全部通过此Socket进行,到这里为止,我们服务器已经有了接受玩家用户连接的功能,现在让我们来看看游戏客户端是如何连接到游戏服务器上,代码片段如下SOCKADDR_INaddr;memset(&addr, 0, sizeof(addr));addr.sin_family=AF_INET;//要连接的游戏服务器端口号addr.sin_addr.s_addr=inet_addr(IP);//要连接的游戏服务器IP地址,addr.sin_port=htons(Port);//到此,客户端和服务器已经有了通讯的桥梁,//接下来就是进行数据的发送和接收:connect(sSocket, (SOCKADDR*)&addr, sizeof(addr));if(send(sSocket, pBuffer, lLength, 0)==SOCKET_ERROR){ Failed("WinSocket Send Error!"); } 这里的pBuffer为要发送的数据缓冲指针,lLength为需要发送的数据长度,通过这支SocketAPI函数,我们无论在客户端或者服务端都可以进行数据的发送工作,同时,我们可以通过recv()这支SocketAPI函数来进行数据接收:if(recv(sSocket, pBuffer, lLength, 0)==SOCKET_ERROR){ Failed("WinSocket Recv Error!"); } 其中pBuffer用来存储获取的网络数据缓冲,lLength则为需要获取的数据长度。 现在,我们已经了解了一些网络互连的基本知识,但作为网络游戏,如此简单的连接方式是无法满足网络游戏中百人千人同时在线的,我们需要更合理容错性更强的网络通讯处理方式,当然,我们需要先了解一下网络游戏对网络通讯的需求是怎样的。 大家知道,游戏需要不断循环处理游戏中的逻辑并进行游戏世界的绘制,上面所介绍的Winsock处理方式均是以阻塞方式进行,这样就违背了游戏的执行本质,可以想象,在客户端连接到服务器的过程中,你的游戏不能得到控制,这时如果玩家想取消连接或者做其他处理,甚至显示一个最基本的动态连接提示都不行。 所以我们需要用其他方式来处理网络通讯,使其不会与游戏主线相冲突,可能大家都会想到:创建一个网络线程来处理不就可以了?没错,我们可以创建一个专门用于网络通讯的子线程来解决这个问题。当然,我们游戏中多了一个线程,我们就需要做更多的考虑,让我们来看看如何创建网络通讯线程。 在Windows系统中,我们可以通过CreateThread()函数来进行线程的创建,看看下面的代码片段:
DWORD dwThreadID; HANDLE hThread = CreateThread( NULL, 0, NetThread/*网络线程函式*/, sSocket, 0, &dwThreadID ); if( hThread == NULL ) { Failed( "WinSocket Thread Create Error!"); }
这里我们创建了一个线程,同时将我们的Socket传入线程函数:
DWORD WINAPINetThread(LPVOID lParam)
{ SOCKET sSocket (SOCKET)lParam; ... return 0; }
NetThread就是我们将来用于处理网络通讯的网络线程。那么,我们又如何把Socket的处理引入线程中?
看看下面的代码片段:
HANDLE hEvent; hEvent = CreateEvent(NULL,0,0,0); // 设置异步通讯 if( WSAEventSelect( sSocket, hEvent, FD_ACCEPT|FD_CONNECT|FD_READ|FD_WRITE|FD_CLOSE ) ==SOCKET_ERROR ) { Failed( "WinSocket EventSelect Error!"); }
通过上面的设置之后,WinSockAPI函数均会以非阻塞方式运行,也就是函数执行后会立即返回,这时网络通讯会以事件方式存储于hEvent,而不会停顿整支程式。 完成了上面的步骤之后,我们需要对事件进行响应与处理,让我们看看如何在网络线程中获得网络通讯所产生的事件消息:
WSAEnumNetworkEvents( sSocket, hEvent, &SocketEvents ); if( SocketEvents.lNetworkEvents != 0 ) { switch( SocketEvents.lNetworkEvents ) { case FD_ACCEPT: WSANETWORKEVENTS SocketEvents; break; case FD_CONNECT: { if( SocketEvents.iErrorCode[FD_CONNECT_BIT] == 0) // 连接成功 { // 连接成功后通知主线程(游戏线程)进行处理 } } break; case FD_READ: // 获取网络数据 { if( recv( sSocket, pBuffer, lLength, 0) == SOCKET_ERROR ) { Failed( "WinSocket Recv Error!"); } } break; case FD_WRITE: break; case FD_CLOSE: // 通知主线程(游戏线程), 网络已经断开 break; default: break; } }
这里仅对网络连接(FD_CONNECT)和读取数据(FD_READ)进行了简单模拟操作,但实际中网络线程接收到事件消息后,会对数据进行组织整理,然后再将数据回传给我们的游戏主线程使用,游戏主线程再将处理过的数据发送出去,这样一个往返就构成了我们网络游戏中的数据通讯,是让网络游戏动起来的最基本要素。 最后,我们来谈谈关于网络数据包(数据封包)的组织,网络游戏的数据包是游戏数据通讯的最基本单位,网络游戏一般不会用字节流的方式来进行数据传输,一个数据封包也可以看作是一条消息指令,在游戏进行中,服务器和客户端会不停的发送和接收这些消息包,然后将消息包解析转换为真正所要表达的指令意义并执行。互动与管理 说到互动,对于玩家来说是与其他玩家的交流,但对于计算机而言,实现互动也就是实现数据消息的相互传递。前面我们已经了解过网络通讯的基本概念,它构成了互动的最基本条件,接下来我们需要在这个网络层面上进行数据的通讯。遗憾的是,计算机并不懂得如何表达玩家之间的交流,因此我们需要提供一套可让计算机了解的指令组织和解析机制,也就是对我们上面简单提到的网络数据包(数据封包)的处理机制。为了能够更简单的给大家阐述网络数据包的组织形式,我们以一个聊天处理模块来进行讨论,看看下面的代码结构:
struct tagMessage{ long lType; long lPlayerID; }; // 消息指令 // 指令相关的玩家标识 char strTalk[256]; // 消息内容
上面是抽象出来的一个极为简单的消息包结构,我们先来谈谈其各个数据域的用途: 首先,lType是消息指令的类型,这是最为基本的消息标识,这个标识用来告诉服务器或客户端这条指令的具体用途,以便于服务器或客户端做出相应处理。lPlayerID被作为玩家的标识。大家知道,一个玩家在机器内部实际上也就是一堆数据,特别是在游戏服务器中,可能有成千上万个玩家,这时候我们需要一个标记来区分玩家,这样就可以迅速找到特定玩家,并将通讯数据应用于其上。 strTalk是我们要传递的聊天数据,这部分才是真正的数据实体,前面的参数只是数据实体应用范围的限定。 在组织完数据之后,紧接着就是把这个结构体数据通过Socket连接发送出去和接收进来。这里我们要了解,网络在进行数据传输过程中,它并不关心数据采用的数据结构,这就需要我们把数据结构转换为二进制数据码进行发送,在接收方,我们再将这些二进制数据码转换回程序使用的相应数据结构。让我们来看看如何实现:
tagMessageMsg; Msg.lTypeMSG_CHAT; Msg.lPlayerID 1000; strcpy( &Msg.strTalk, "聊天信息" );
首先,我们假设已经组织好一个数据包,这里MSG_CHAT是我们自行定义的标识符,当然,这个标识符在服务器和客户端要统一。玩家的ID则根据游戏需要来进行设置,这里1000只作为假设,现在继续:
char* p = (char*)&Msg; long lLength = sizeof( tagMessage ); send( sSocket, p, lLength ); // 获取数据结构的长度
我们通过强行转换把结构体转变为char类型的数据指针,这样就可以通过这个指针来进行流式数据处理,这里通过sizeof()获得结构体长度,然后用WinSock的Send()函数将数据发送出去。
接下来看看如何接收数据: long lLength = sizeof( tagMessage ); char* Buffer = new char[lLength]; recv( sSocket, Buffer, lLength ); tagMessage* p = (tagMessage*)Buffer; // 获取数据
在通过WinSock的recv()函数获取网络数据之后,我们同样通过强行转换把获取出来的缓冲数据转换为相应结构体,这样就可以方便地对数据进行访问。(注:强行转换仅仅作为数据转换的一种手段,实际应用中有更多可选方式,这里只为简洁地说明逻辑)谈到此处,不得不提到服务器/客户端如何去筛选处理各种消息以及如何对通讯数据包进行管理。无论是服务器还是客户端,在收到网络消息的时候,通过上面的数据解析之后,还必须对消息类型进行一次筛选和派分,简单来说就是类似Windows的消息循环,不同消息进行不同处理。这可以通过一个switch语句(熟悉Windows消息循环的朋友相信已经明白此意),基于消息封包里的lType信息,对消息进行区分处理,考虑如下代码片段:
switch( p->lType ) // 这里的p->lType为我们解析出来的消息类型标识 { case MSG_CHAT: // 聊天消息 break; case MSG_MOVE: // 玩家移动消息 break; case MSG_EXIT: // 玩家离开消息 break; default: break; }
上面片段中的MSG_MOVE和MSG_EXIT都是我们虚拟的消息标识(一个真实游戏中的标识可能会有上百个,这就需要考虑优化和优先消息处理问题)。此外,一个网络游戏服务器面对的是成百上千的连接用户,我们还需要一些合理的数据组织管理方式来进行相关处理。普通的单体游戏服务器,可能会因为当机或者用户过多而导致整个游戏网络瘫痪,而这也就引入分组服务器机制,我们把服务器分开进行数据的分布式处理。 我们把每个模块提取出来,做成专用的服务器系统,然后建立一个连接所有服务器的数据中心来进行数据交互,这里每个模块均与数据中心创建了连接,保证了每个模块的相关性,同时玩家转变为与当前提供服务的服务器进行连接通讯,这样就可以缓解单独一台服务器所承受的负担,把压力分散到多台服务器上,同时保证了数据的统一,而且就算某台服务器因为异常而当机也不会影响其他模块的游戏玩家,从而提高了整体稳定性。 分组式服务器缓解了服务器的压力,但也带来了服务器调度问题,分组式服务器需要对服务器跳转进行处理,就以一个玩家进行游戏场景跳转作为讨论基础:假设有一玩家处于游戏场景A,他想从场景A跳转到场景B,在游戏中,我们称之场景切换,这时玩家就会触发跳转需求,比如走到了场景中的切换点,这样服务器就把玩家数据从"游戏场景A 服务器"删除,同时在"游戏场景B 服务器"中把玩家建立起来。 这里描述了场景切换的简单模型,当中处理还有很多步骤,不过通过这样的思考相信大家可以派生出很多应用技巧。不过需要注意的是,在场景切换或者说模块间切换的时候,需要切实考虑好数据的传输安全以及逻辑合理性,否则切换很可能会成为将来玩家复制物品的桥梁。 总结 本篇讲述的都是通过一些简单的过程来进行网络游戏通讯,提供了一个制作的思路,虽然具体实现起来还有许多要做,但只要顺着这个思路去扩展、去完善,相信大家很快就能够编写出自己的网络通讯模块。由于时间仓促,本文在很多细节方面都有省略,文中若有错误之处也望大家见谅。
上面通过调用Windows的socket API函数来初始化网络设备,接下来进行网络Socket的创建,代码片段如下:
SOCKET sSocket socket( AF_INET, m_lProtocol, 0 ); if( sSocket == INVALID_SOCKET ) { Failed( "WinSocketCreateError!" ); }
这里需要说明,客户端和服务端所需要的Socket连接数量是不同的,客户端只需要一个Socket连接足以满足游戏的需
要,而服务端必须为每个玩家用户创建一个用于通讯的Socket连接。当然,并不是说如果服务器上没有玩家那就不需要创
建Socket连接,服务器端在启动之时会生成一个特殊的Socket用来对玩家创建与服务器连接的请求进行响应,等介绍网络
监听部分后会有更详细说明。
有初始化与创建必然就有释放与删除,让我们看看下面的释放部分:
if( sSocket != INVALID_SOCKET ) { closesocket( sSocket ); } if( WSACleanup() != 0 ) { Warning( "Can'treleaseWinsocket" ); }
这里两个步骤分别对前面所作的创建初始化进行了相应释放。
接下来看看服务器端的一个网络执行处理,这里我们假设服务器端已经创建好一个Socket供使用,我们要做的就是让
这个Socket变成监听网络连接请求的专用接口,看看下面代码片段:
SOCKADDR_IN addr; memset( &addr, 0, sizeof(addr) ); addr.sin_family = AF_INET; addr.sin_addr.s_addr = htonl( INADDR_ANY ); addr.sin_port = htons( Port ); // Port为要监听的端口号 // 绑定socket if( bind( sSocket, (SOCKADDR*)&addr, sizeof(addr) ) == SOCKET_ERROR ) { Failed( "WinSocketBindError!"); } // 进行监听 if( listen( sSocket, SOMAXCONN ) == SOCKET_ERROR ) { Failed( "WinSocketListenError!"); }
这里使用的是阻塞式通讯处理,此时程序将处于等待玩家用户连接的状态,倘若这时候有客户端连接进来,则通过
accept()来创建针对此玩家用户的Socket连接,代码片段如下:
sockaddraddrServer; int nLen sizeof( addrServer ); SOCKET sPlayerSocket accept( sSocket, &addrServer, &nLen ); if( sPlayerSocket == INVALID_SOCKET ) { Failed( WinSocket Accept Error!"); } 这里我们创建了sPlayerSocket连接,此后游戏服务器与这个玩家用户的通讯全部通过此Socket进行,到这里为止,我们服务器已经有了接受玩家用户连接的功能,现在让我们来看看游戏客户端是如何连接到游戏服务器上,代码片段如下SOCKADDR_INaddr;memset(&addr, 0, sizeof(addr));addr.sin_family=AF_INET;//要连接的游戏服务器端口号addr.sin_addr.s_addr=inet_addr(IP);//要连接的游戏服务器IP地址,addr.sin_port=htons(Port);//到此,客户端和服务器已经有了通讯的桥梁,//接下来就是进行数据的发送和接收:connect(sSocket, (SOCKADDR*)&addr, sizeof(addr));if(send(sSocket, pBuffer, lLength, 0)==SOCKET_ERROR){ Failed("WinSocket Send Error!"); } 这里的pBuffer为要发送的数据缓冲指针,lLength为需要发送的数据长度,通过这支SocketAPI函数,我们无论在客户端或者服务端都可以进行数据的发送工作,同时,我们可以通过recv()这支SocketAPI函数来进行数据接收:if(recv(sSocket, pBuffer, lLength, 0)==SOCKET_ERROR){ Failed("WinSocket Recv Error!"); } 其中pBuffer用来存储获取的网络数据缓冲,lLength则为需要获取的数据长度。 现在,我们已经了解了一些网络互连的基本知识,但作为网络游戏,如此简单的连接方式是无法满足网络游戏中百人千人同时在线的,我们需要更合理容错性更强的网络通讯处理方式,当然,我们需要先了解一下网络游戏对网络通讯的需求是怎样的。 大家知道,游戏需要不断循环处理游戏中的逻辑并进行游戏世界的绘制,上面所介绍的Winsock处理方式均是以阻塞方式进行,这样就违背了游戏的执行本质,可以想象,在客户端连接到服务器的过程中,你的游戏不能得到控制,这时如果玩家想取消连接或者做其他处理,甚至显示一个最基本的动态连接提示都不行。 所以我们需要用其他方式来处理网络通讯,使其不会与游戏主线相冲突,可能大家都会想到:创建一个网络线程来处理不就可以了?没错,我们可以创建一个专门用于网络通讯的子线程来解决这个问题。当然,我们游戏中多了一个线程,我们就需要做更多的考虑,让我们来看看如何创建网络通讯线程。 在Windows系统中,我们可以通过CreateThread()函数来进行线程的创建,看看下面的代码片段:
DWORD dwThreadID; HANDLE hThread = CreateThread( NULL, 0, NetThread/*网络线程函式*/, sSocket, 0, &dwThreadID ); if( hThread == NULL ) { Failed( "WinSocket Thread Create Error!"); }
这里我们创建了一个线程,同时将我们的Socket传入线程函数:
DWORD WINAPINetThread(LPVOID lParam)
{ SOCKET sSocket (SOCKET)lParam; ... return 0; }
NetThread就是我们将来用于处理网络通讯的网络线程。那么,我们又如何把Socket的处理引入线程中?
看看下面的代码片段:
HANDLE hEvent; hEvent = CreateEvent(NULL,0,0,0); // 设置异步通讯 if( WSAEventSelect( sSocket, hEvent, FD_ACCEPT|FD_CONNECT|FD_READ|FD_WRITE|FD_CLOSE ) ==SOCKET_ERROR ) { Failed( "WinSocket EventSelect Error!"); }
通过上面的设置之后,WinSockAPI函数均会以非阻塞方式运行,也就是函数执行后会立即返回,这时网络通讯会以事件方式存储于hEvent,而不会停顿整支程式。 完成了上面的步骤之后,我们需要对事件进行响应与处理,让我们看看如何在网络线程中获得网络通讯所产生的事件消息:
WSAEnumNetworkEvents( sSocket, hEvent, &SocketEvents ); if( SocketEvents.lNetworkEvents != 0 ) { switch( SocketEvents.lNetworkEvents ) { case FD_ACCEPT: WSANETWORKEVENTS SocketEvents; break; case FD_CONNECT: { if( SocketEvents.iErrorCode[FD_CONNECT_BIT] == 0) // 连接成功 { // 连接成功后通知主线程(游戏线程)进行处理 } } break; case FD_READ: // 获取网络数据 { if( recv( sSocket, pBuffer, lLength, 0) == SOCKET_ERROR ) { Failed( "WinSocket Recv Error!"); } } break; case FD_WRITE: break; case FD_CLOSE: // 通知主线程(游戏线程), 网络已经断开 break; default: break; } }
这里仅对网络连接(FD_CONNECT)和读取数据(FD_READ)进行了简单模拟操作,但实际中网络线程接收到事件消息后,会对数据进行组织整理,然后再将数据回传给我们的游戏主线程使用,游戏主线程再将处理过的数据发送出去,这样一个往返就构成了我们网络游戏中的数据通讯,是让网络游戏动起来的最基本要素。 最后,我们来谈谈关于网络数据包(数据封包)的组织,网络游戏的数据包是游戏数据通讯的最基本单位,网络游戏一般不会用字节流的方式来进行数据传输,一个数据封包也可以看作是一条消息指令,在游戏进行中,服务器和客户端会不停的发送和接收这些消息包,然后将消息包解析转换为真正所要表达的指令意义并执行。互动与管理 说到互动,对于玩家来说是与其他玩家的交流,但对于计算机而言,实现互动也就是实现数据消息的相互传递。前面我们已经了解过网络通讯的基本概念,它构成了互动的最基本条件,接下来我们需要在这个网络层面上进行数据的通讯。遗憾的是,计算机并不懂得如何表达玩家之间的交流,因此我们需要提供一套可让计算机了解的指令组织和解析机制,也就是对我们上面简单提到的网络数据包(数据封包)的处理机制。为了能够更简单的给大家阐述网络数据包的组织形式,我们以一个聊天处理模块来进行讨论,看看下面的代码结构:
struct tagMessage{ long lType; long lPlayerID; }; // 消息指令 // 指令相关的玩家标识 char strTalk[256]; // 消息内容
上面是抽象出来的一个极为简单的消息包结构,我们先来谈谈其各个数据域的用途: 首先,lType是消息指令的类型,这是最为基本的消息标识,这个标识用来告诉服务器或客户端这条指令的具体用途,以便于服务器或客户端做出相应处理。lPlayerID被作为玩家的标识。大家知道,一个玩家在机器内部实际上也就是一堆数据,特别是在游戏服务器中,可能有成千上万个玩家,这时候我们需要一个标记来区分玩家,这样就可以迅速找到特定玩家,并将通讯数据应用于其上。 strTalk是我们要传递的聊天数据,这部分才是真正的数据实体,前面的参数只是数据实体应用范围的限定。 在组织完数据之后,紧接着就是把这个结构体数据通过Socket连接发送出去和接收进来。这里我们要了解,网络在进行数据传输过程中,它并不关心数据采用的数据结构,这就需要我们把数据结构转换为二进制数据码进行发送,在接收方,我们再将这些二进制数据码转换回程序使用的相应数据结构。让我们来看看如何实现:
tagMessageMsg; Msg.lTypeMSG_CHAT; Msg.lPlayerID 1000; strcpy( &Msg.strTalk, "聊天信息" );
首先,我们假设已经组织好一个数据包,这里MSG_CHAT是我们自行定义的标识符,当然,这个标识符在服务器和客户端要统一。玩家的ID则根据游戏需要来进行设置,这里1000只作为假设,现在继续:
char* p = (char*)&Msg; long lLength = sizeof( tagMessage ); send( sSocket, p, lLength ); // 获取数据结构的长度
我们通过强行转换把结构体转变为char类型的数据指针,这样就可以通过这个指针来进行流式数据处理,这里通过sizeof()获得结构体长度,然后用WinSock的Send()函数将数据发送出去。
接下来看看如何接收数据: long lLength = sizeof( tagMessage ); char* Buffer = new char[lLength]; recv( sSocket, Buffer, lLength ); tagMessage* p = (tagMessage*)Buffer; // 获取数据
在通过WinSock的recv()函数获取网络数据之后,我们同样通过强行转换把获取出来的缓冲数据转换为相应结构体,这样就可以方便地对数据进行访问。(注:强行转换仅仅作为数据转换的一种手段,实际应用中有更多可选方式,这里只为简洁地说明逻辑)谈到此处,不得不提到服务器/客户端如何去筛选处理各种消息以及如何对通讯数据包进行管理。无论是服务器还是客户端,在收到网络消息的时候,通过上面的数据解析之后,还必须对消息类型进行一次筛选和派分,简单来说就是类似Windows的消息循环,不同消息进行不同处理。这可以通过一个switch语句(熟悉Windows消息循环的朋友相信已经明白此意),基于消息封包里的lType信息,对消息进行区分处理,考虑如下代码片段:
switch( p->lType ) // 这里的p->lType为我们解析出来的消息类型标识 { case MSG_CHAT: // 聊天消息 break; case MSG_MOVE: // 玩家移动消息 break; case MSG_EXIT: // 玩家离开消息 break; default: break; }
上面片段中的MSG_MOVE和MSG_EXIT都是我们虚拟的消息标识(一个真实游戏中的标识可能会有上百个,这就需要考虑优化和优先消息处理问题)。此外,一个网络游戏服务器面对的是成百上千的连接用户,我们还需要一些合理的数据组织管理方式来进行相关处理。普通的单体游戏服务器,可能会因为当机或者用户过多而导致整个游戏网络瘫痪,而这也就引入分组服务器机制,我们把服务器分开进行数据的分布式处理。 我们把每个模块提取出来,做成专用的服务器系统,然后建立一个连接所有服务器的数据中心来进行数据交互,这里每个模块均与数据中心创建了连接,保证了每个模块的相关性,同时玩家转变为与当前提供服务的服务器进行连接通讯,这样就可以缓解单独一台服务器所承受的负担,把压力分散到多台服务器上,同时保证了数据的统一,而且就算某台服务器因为异常而当机也不会影响其他模块的游戏玩家,从而提高了整体稳定性。 分组式服务器缓解了服务器的压力,但也带来了服务器调度问题,分组式服务器需要对服务器跳转进行处理,就以一个玩家进行游戏场景跳转作为讨论基础:假设有一玩家处于游戏场景A,他想从场景A跳转到场景B,在游戏中,我们称之场景切换,这时玩家就会触发跳转需求,比如走到了场景中的切换点,这样服务器就把玩家数据从"游戏场景A 服务器"删除,同时在"游戏场景B 服务器"中把玩家建立起来。 这里描述了场景切换的简单模型,当中处理还有很多步骤,不过通过这样的思考相信大家可以派生出很多应用技巧。不过需要注意的是,在场景切换或者说模块间切换的时候,需要切实考虑好数据的传输安全以及逻辑合理性,否则切换很可能会成为将来玩家复制物品的桥梁。 总结 本篇讲述的都是通过一些简单的过程来进行网络游戏通讯,提供了一个制作的思路,虽然具体实现起来还有许多要做,但只要顺着这个思路去扩展、去完善,相信大家很快就能够编写出自己的网络通讯模块。由于时间仓促,本文在很多细节方面都有省略,文中若有错误之处也望大家见谅。
收藏的用户(0) X
正在加载信息~
推荐阅读
最新回复 (0)
站点信息
- 文章2295
- 用户1336
- 访客10465445
每日一句
Wisdom begins in wonder.
智慧始于好奇。
智慧始于好奇。
新会员