|
发表于 2010-5-23 17:25:19
|显示全部楼层
确定好平台与接口后,我们开始进行网络连接创建之前的一些必要的初始化工作,这部分无论是客户端或者服务器都
需要进行。让我们看看下面的代码片段:
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:
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;
} |
|