大家知道,游戏需要不断循环处理游戏中的逻辑并进行游戏世界的绘制,上面所介绍的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:
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 服务器"中把玩家建立起来。
这里描述了场景切换的简单模型,当中处理还有很多步骤,不过通过这样的思考相信大家可以派生出很多应用技巧。不过需要注意的是,在场景切换或者说模块间切换的时候,需要切实考虑好数据的传输安全以及逻辑合理性,否则切换很可能会成为将来玩家复制物品的桥梁。
总结
本篇讲述的都是通过一些简单的过程来进行网络游戏通讯,提供了一个制作的思路,虽然具体实现起来还有许多要做,但只要顺着这个思路去扩展、去完善,相信大家很快就能够编写出自己的网络通讯模块。由于时间仓促,本文在很多细节方面都有省略,文中若有错误之处也望大家见谅。 |