|
|
前面讲了一些程序架构上的,不知道大家感觉怎么样,其实只要架构搭好了,无论是用什么样的渲染手段,基本都是一样的,可能3D要做出比较好的效果,需要特别处理一些对象。我本来想讲一讲地图的有些做法,但还是留给大家自己想把,地图是游戏程序中最简单的一个对象。我有个例子带源码,稍后可以看一看,写得很仓促,欢迎大家指教。另外有关游戏的一些经验和问题,欢迎大家访问http://bbs.chaosstars.com/ (网络游戏编程版)交流,群的人数满了,请大家见谅。
下面我谈一谈游戏中用到的网络编程
对于网络编程,我也不谈具体的技术,目前Windows下对于各种网络I/O模型,都有很多资料,像select模型,Windows消息模型,Windows事件模型,重叠I/O模型,完成端口模型。每种模型都有各自的特点,可以根据实际需要来选择,我一般认为,如果你需要进行移植,可以选择select模型,如果只是比较简单的收发数据(客户端),用Windows消息模型比较方便,服务器需要大批量处理网络数据的,可以用完成端口模型
由于是C/S模式,目前网络游戏大多采用TCP数据传输协议,不需要考虑网络数据丢包和接帧的问题,但TCP的数据协议采用了一些延时发送的算法,而且每次发送一个包都要进行确认,所以尽可能每次不要发送很小很小的包,一般一个包的大小在1K左右,而且TCP的包是基于流的,包和包之间是一个接一个,像流水一样,需要进行拆包(稍候介绍)
我先说服务器,TCP的服务器一定有一个监听用的Socket,专门用来处理客户端的连接信息,然后对应每一个客户端有一个对应的Socket来传递数据,怎么组织这些Socket,我想大家都有自己的办法,无论是静态数组也好还是动态表也行,但是有一点就是必须能够很快的检索这些Socket,我一般倾向于用静态数组。
SOCKET g_Sock[MAX_USER];
消息有2种形式,一种是基于二进制的,一种是基于字符串的,每种都有各自的优势,说不上哪种好,但我比较倾向于二进制,能够处理自定义的数据类型,字符串类型的都是可见字符,比较容易扩展,下面看基于二进制的消息的定义
struct tagNetMsg
{
WORD wSockID; // 对应的Socket编号,其实也是用户的编号
WORD wMsgID; // 命令编号,就是消息头
WORD wMsgSize; // 命令大小
BYTE wMsgBuf[MAX_SIZE]; // 命令缓冲区
// 这里我们可以定义很多操作符
tagNetMsg& operator=( tagNetMsg& _right );
...
void SetMsg( BYTE * _buf, _int size );
// 输入流
int begin_in( WORD wMsgID ); // 清空buf,并设置开始标志,然后将命令头放进缓冲区
void operator<<( char& _val );
void operator<<( unsigned char& _val );
void operator<<( int& _val );
void operator<<( unsigned int& _val );
...
int end_in( WORD wMsgID ); // 加密函数可以放进这里
// 输出流
int begin_out( WORD& wMsgID ); // 解密并将命令头取出
void operator>>( char& _val );
void operator>>( unsigned char& _val );
void operator>>( int& _val );
void operator>>( unsigned int& _val );
...
int end_out( WORD wMsgID );
};
typedef tagNetMsg NETMSG;
typedef tagNetMsg * LPNETMSG;
对于发送数据,我们直接调用send函数就好了,一般不需要做特别的处理,如果出现发送的时候有Block现象,那么我们可以将发送的内容存储在一个队列里面,稍后再发。
queue<LPNETMSG> g_queSend;
发送数据的时候实际上只是将发送的消息放进队列
LPNETMSG pNewMsg = new NETMSG;
pNewMsg->begin_in( 0x0011 ); // 比如0x0011表示走路
(*pNewMsg)<<1000.0f<<800.0f; // 当前坐标
pNewMsg->end_in( );
g_queSend.lock( ); // 要注意将队列锁住,大家自己实现这个锁,用临界量就行
g_queSend.push( pNewMsg );
g_queSend.unlock( );
然后在专门的发送消息的线程中
DWORD WINAPI SendThread( LPVOID lpParam )
{
LPNETMSG pMsg;
while ( true )
{
while ( g_queSend.size( ) > 0 )
{
g_queSend.lock( ); // 这里也要锁住
pNewMsg = g_queSend.front( );
g_queSend.pop( );
g_queSend.unlock( );
SendMsg( pMsg ); // 发送函数放在锁的后面
}
}
return 0;
}
void SendMsg( LPNETMSG pMsg )
{
int nRet = send( g_Sock[pMsg->wSockID], pMsg->wMsgBuf, pMsg->wMsgSize, 0 );
if ( nRet == SOCKET_ERROR )
{
nRet = WSAGetLastError( );
// 如果发送没有成功,重新压回队列中
g_queSend.lock( );
g_queSend.push( pNewMsg );
g_queSend.unlock( );
}
else
{
delete pMsg; // 释放
}
}
发送还是比较简单的,比较麻烦的是接受消息
定义一个要比BUF_SZIE大很多的缓冲区
BYTE g_pRecvBuf[BUF_SZIE * 4];
WORD g_wRecvSize = 0;
void OnRead( WORD wUserID )
{
BYTE pBufer[BUF_SZIE];
int nRet;
// 这里我就简单用recv函数,完成端口的数据读取可以参考资料
nRet = recv( g_Sock[wUserID], pBufer, BUF_SZIE, 0 );
... // 自己处理错误
// 这个时候读出来的数据不能马上解析,放进那个大的缓冲区里去
lock( ); // 要加锁
if ( nRet + g_wRecvSize > BUF_SZIE * 4 ) // 缓冲区不够了,没办法,扔掉了
{
g_wRecvSize = 0; // 由于是基于流的,中间有错,所有的数据必须都扔了
}
memcpy( g_pRecvBuf[g_wRecvSize], pBufer, nRet );
g_wRecvSize += nRet;
unlock( );
OnBuffer( );
}
为什么要把接收到的数据放进一个大的buffer中呢,难道不能把接收时候的缓冲区摄制的很大一次接受完吗?不能,因为基于流的数据接受,你不能确认每次接受到的数据都是完整的,即使你的缓冲区足够大,TCP也有可能将其分为多个部分。不管了,反正现在g_pRecvBuf里存放的至少有一个完整包,如果没有完整包,那么就不处理,看下面的代码
#define MSG_FLAG_BEGIN 0xDD // 定义一个包开头的标志
#define MSG_FLAG_END 0xEE // 定义一个包结尾的标志
queue<LPNETMSG> g_queRecv; // 接受队列
void OnBuffer( )
{
int nOffset = 0;
int nMsgSize = 0;
lock( );
while ( g_wRecvSize > 0 )
{
if ( g_pRecvBuf[nOffset] == MSG_FLAG_BEGIN )
{
nOffset += sizeof( BYTE );
memcpy( &nMsgSize, g_pRecvBuf[nOffset], sizeof( int ) ); // 消息包的长度
if ( nMsgSize < g_wRecvSize - nOffset )
{
LPNETMSG pNewMsg = new NETMSG;
g_queRecv.lock( ); // 锁住
g_queRecv.push( pNewMsg );
g_queRecv.unlock( );
... // 自己处理把
}
else // 表示后面还有数据,没有接受完
{
}
}
else
{
// 包全部扔了,坏了
g_wRecvSize = 0;
break;
}
}
unlock( );
}
上面我就简单的写了一下,大家不知道明白不明白我的意思
这样消息就放进队列了,然后就是处理了,处理要在专门的线程来解决
DWORD WINAPI SendThread( LPVOID lpParam )
{
LPNETMSG pMsg;
while ( true )
{
while ( g_queRecv.size( ) > 0 )
{
g_queRecv.lock( ); // 这里也要锁住
pNewMsg = g_queRecv.front( );
g_queRecv.pop( );
g_queRecv.unlock( );
OnMsg( pMsg ); //
}
}
return 0;
}
void OnMsg( LPNETMSG pMsg )
{
switch ( pMsg->wMsgID )
{
case 0x0011: // 走路
{
...
}
break;
}
}
当然上面的OnMsg是基于switch的处理方式,我觉得也是很不错的,效率也比较高,当然,你可以采用面向对象的模式,将消息定义成一个基类
class CMsg
{
...
virtual void OnMsg( ... );
};
然后派生一个走路的类
class CMsgMove : public CMsg
{
virtual void OnMsg( ... )
{
// 实现走路
...
}
};
然后在线程中这样处理
DWORD WINAPI SendThread( LPVOID lpParam )
{
CMsg * pMsg;
while ( true )
{
while ( g_queRecv.size( ) > 0 )
{
g_queRecv.lock( ); // 这里也要锁住
pNewMsg = g_queRecv.front( );
g_queRecv.pop( );
g_queRecv.unlock( );
OnMsg( pMsg ); //
}
}
return 0;
}
void OnMsg( CMsg * pMsg )
{
switch ( pMsg->wMsgID )
{
case 0x0011: // 走路
{
(CMsgMove * )pMsg->OnMsg( ... ); // 走路了
}
break;
}
}
呵呵,就这样了,希望大家给我意见,今天又差不多了,下次给大家讲讲客户端的网络处理, |
|