频道直达 - 专题 - 新闻 - 技巧 - 组网 - 开发 - 安全 - web编程 - 图像 - 操作系统 - 数据库 - 教育 - 旅游 - 健康 - 时尚 - 驱动 - 软件 - 游戏 - 多媒体 - ERP - 讨论组

网络游戏封包基础

来源: 作者: 出处:巧巧读书 2006-03-14 进入讨论组
上一页 1 2 


网络互连
  在了解了网络游戏基本形态之后,让我们进入真正的实际应用部分。首先,作为网络游戏,除了常规的单机游戏所必需的东西之外,我们还需要增加一个网络通讯模块,当然,这也是网络游戏较为主要的部分,我们来讨论一下如何实现网络的通讯模块。

  一个完善的网络通讯模块涉及面相当广,本文仅对较为基本的处理方式进行讨论。网络游戏是由客户端和服务器组成,相应也需要两种不同的网络通讯处理方式,不过也有相同之处,我们先就它们的共同点来进行介绍。我们这里以Microsoft Windows 2000 [2000 Server]作为开发平台,并且使用Winsock作为网络接口(可能一些朋友会考虑使用DirectPlay来进行网络通讯,不过对于当前在线游戏,DirectPlay并不适合,具体原因这里就不做讨论了)。
  确定好平台与接口后,我们开始进行网络连接创建之前的一些必要的初始化工作,这部分无论是客户端或者服务器都需要进行。让我们看看下面的代码片段:
WORD wVersionRequested;
WSADATAwsaData;
wVersionRequested MAKEWORD(1, 1);
if( WSAStartup( wVersionRequested, &wsaData ) !0 )
{
 Failed( WinSock Version Error!" );
}
  上面通过调用Windows的socket API函数来初始化网络设备,接下来进行网络Socket的创建,代码片段如下:

SOCKET sSocket socket( AF_INET, m_lProtocol, 0 );
if( sSocket == INVALID_SOCKET )
{
 Failed( "WinSocket Create Error!" );
}
  这里需要说明,客户端和服务端所需要的Socket连接数量是不同的,客户端只需要一个Socket连接足以满足游戏的需要,而服务端必须为每个玩家用户创建一个用于通讯的Socket连接。当然,并不是说如果服务器上没有玩家那就不需要创建Socket连接,服务器端在启动之时会生成一个特殊的Socket用来对玩家创建与服务器连接的请求进行响应,等介绍网络监听部分后会有更详细说明。
  有初始化与创建必然就有释放与删除,让我们看看下面的释放部分:

if( sSocket != INVALID_SOCKET )
{
 closesocket( sSocket );
}
if( WSACleanup() != 0 )
{
 Warning( "Can't release Winsocket" );
}

  这里两个步骤分别对前面所作的创建初始化进行了相应释放。
  接下来看看服务器端的一个网络执行处理,这里我们假设服务器端已经创建好一个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( "WinSocket Bind Error!");
}
// 进行监听
if( listen( sSocket, SOMAXCONN ) == SOCKET_ERROR )
{
 Failed( "WinSocket Listen Error!");
}


  这里使用的是阻塞式通讯处理,此时程序将处于等待玩家用户连接的状态,倘若这时候有客户端连接进来,则通过accept()来创建针对此玩家用户的Socket连接,代码片段如下:
sockaddraddrServer;
int nLen sizeof( addrServer );
SOCKET sPlayerSocket accept( sSocket, &addrServer, &nLen );
if( sPlayerSocket == INVALID_SOCKET )
{
 Failed( WinSocket Accept Error!");
}
这里我们创建了sPlayerSocket连接,此后游戏服务器与这个玩家用户的通讯全部通过此Socket进行,到这里为止,我们服务器已经有了接受玩家用户连接的功能,现在让我们来看看游戏客户端是如何连接到游戏服务器上,代码片段如下:
SOCKADDR_IN addr;
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为需要发送的数据长度,通过这支Socket API函数,我们无论在客户端或者服务端都可以进行数据的发送工作,同时,我们可以通过recv()这支Socket API函数来进行数据接收: 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!");
}

  通过上面的设置之后,WinSock API函数均会以非阻塞方式运行,也就是函数执行后会立即返回,这时网络通讯会以事件方式存储于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:
}
}


  这里仅对网络连接(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 都是我们虚拟的消息标识(一个真实游戏中的标识可能会有上百个,这就需要考虑优化和优先消息处理问题)。此外,一个网络游戏服务器面对的是成百上千的连接用户,我们还需要一些合理的数据组织管理方式来进行相关处理。普通的单体游戏服务器,可能会因为当机或者用户过多而导致整个游戏网络瘫痪,而这也就引入分组服务器机制,我们把服务器分开进行数据的分布式处理。
  我们把每个模块提取出来,做成专用的服务器系统,然后建立一个连接所有服务器的数据中心来进行数据交互,这里每个模块均与数据中心创建了连接,保证了每个模块的相关性,同时玩家转变为与当前提供服务的服务器进行连接通讯,这样就可以缓解单独一台服务器所承受的负担,把压力分散到多台服务器上,同时保证了数据的统一,而且就算某台服务

URL:http://www.qqread.com/encrypt/x200885062.html 更多文章 更多内容请看网络管理实用手册网络故障手册网络组网专题专题,或进入讨论组讨论。
上一页 1 2 
收藏此文】【 】【打印】【关闭
相关图文阅读
频道图文推荐
健 康 咨 询
时 尚 咨 询
巧巧读书宗旨
相关专题
讨论组问题推荐
站内各频道最新更新文档
站内最新制作专题
热门关键字导读
Photoshop教 程照片处理 照片制作 PS快捷键 抠图
计 算 机 故 障XP系统修复
艺 术 与 设 计设计 流媒体 设计欣赏 边框
计 算 机 安 全ARP
站内频道文章精选
巧巧电脑频道编辑信箱  告诉我们您想看的专题或文章