休闲游戏的服务器组成
第一层:登陆/账号服务器(Login Server),负责验证用户身份、向客户端传送初始信息,从QQ聊天软件的封包常识来看,这些初始信息可能包括“会话密钥”此类的信息,以后客户端与后续服务器的通信就使用此会话密钥进行身份验证和信息加密;
第二层:大厅服务器(主服务器),负责向客户端传递当前游戏中的所有房间信息,这些房间信息包括:各房间的连接IP,PORT,各房间的当前在线人数,房间名称等等。
第三层:游戏逻辑服务器,负责处理房间逻辑及房间内的桌子逻辑。它的消息是通过主服务器的实现的。
这里讲述一下登陆/账号服务器(Login Server)的设计,这是我近期写的代码,也算是学习网络编程的一个总结吧。本篇包括以下四个方面:
1, 进程模型
2,对象内存管理模型
3, 数据管理通道
4, TCP封包拆包
一、进程模型
登陆/账号服务器主要包含TCPSvrd进程和LogSvrd进程,其中TCPSvrd主要是实现TCP的连接。LogSvrd实现数据的逻辑操作和与其他的LogSvrd的通信。
二、对象内存管理模型
TCPSvrd服务采用完成端口模式。主要处理单句柄数据和单操作数据。它们分别由CPERIOHandleData和CPERIOData两个对象来实现。下面是几个主要对象的结构图:
对完成端口工作线程的代码如下:
DWORD WINAPI ServerWorkerThread(LPVOID lParam)
{
CTCPSvrd* pTCPSvrd = (CTCPSvrd*)lParam;
CPERIOHandleData* pHandleData = NULL;
CPERIOData* pIOData = NULL;
LPOVERLAPPED lpOverlapped;
DWORD dwBytesTransfered = 0;
DWORD dwRecvBytes =0;
DWORD dwFlags = 0;
int state = 0;
while (TRUE)
{
int nRet = GetQueuedCompletionStatus(pTCPSvrd->m_hCompetionPort, &dwBytesTransfered, (DWORD*)&pHandleData, &lpOverlapped, INFINITE);
if (FALSE == nRet && NULL == lpOverlapped)
{
printf("GetQueueCompletionStautus Error/n");
return 0;
}
if (FALSE == nRet && lpOverlapped != NULL)
{
printf("Client Except Exit/n");
pHandleData->SetOffLine();
continue;
}
if (dwBytesTransfered == 0)
{
printf("Client Normal Exit/n");
pHandleData->SetOffLine();
continue;
}
pIOData = CONTAINING_RECORD(lpOverlapped, CPERIOData, m_OVerlapped);
switch(pIOData->m_nIOType)
{
case IORead:
pTCPSvrd->RecvData(pHandleData, pIOData, dwBytesTransfered);
break;
case IOWrite:
pTCPSvrd->SendData(pHandleData, pIOData, dwBytesTransfered);
break;
}
}
return 0;
}
三、数据管理通道
两个进程间的数据是通过共享内存的方式传递的,本模块就是操作这个数据区域,从而实现数据的传递。实现如下:
class CDataQueue
{
typedef struct tagQueueHead
{
int nTotalSize;
int nWriteBegin;
int nReadBegin;
}QueueHead;
public:
CDataQueue();
CDataQueue(int nBufSize);
~CDataQueue();
public:
void* operator new(size_t nSize);
void operator delete(void *pMem);
void Initialize(int nBufSize);
public:
static size_t GetQueueSize(size_t nBufSize);
static CShareMemory* m_pAttackMem;
public:
int PushOneCode(const BYTE* pCode, unsigned short nSize);
int PeekOneCode(BYTE* pCode, unsigned short& nSize);
BOOL IsFull();
BOOL IsEmpty();
int GetUseLen();
int GetUsedLen();
public:
QueueHead m_QueueHead;
BYTE* m_pDataMem;
};
四、TCP封包拆包
一、为什么基于TCP的通讯程序需要进行封包和拆
TCP是个"流"协议,,谓流,就是没有界限的一串串数据.,一般通讯程序开发是需要定义一个个相互独立的数据包的,比如用于登陆的数据包,用于注销的数据包.由于TCP"流"的特性以及网络状况,在进行数据传输时会出现以下几种情况:
假设我们连续调用两次send分别发送两段数据data1和data2,在接收端有以下几种接收情况:
A.先接收到data1,然后接收到data2。
B.先接收到data1的部分数据,然后接收到data2余下的部分以及data2的全部。
C.先接收到了data1的全部数据和data1的部分数据,然后接收到了data2的余下的数据。
D.一次性接收到了data1和data2的全部数据.。
对于A这种情况正是我们需要的,不再做讨论.对于B,C,D的情况就是大家经常说的"粘包",就需要我们把接收到的数据进行拆包,拆成一个个独立的数据包.为了拆包就必须在发送端进行封包.
另:对于UDP来说就不存在拆包的问题,因为UDP是个"数据包"协议,也就是两段数据间是有界限的,在接收端要么接收不到数据要么就是接收一个完整的一段数据,不会少接收也不会多接收。
二、为什么会出现B.C.D的情况.
"粘包"可发生在发送端也可发生在接收端.
1.由Nagle算法造成的发送端的粘包,Nagle算法是一种改善网络传输效率的算法.简单的说,当我们提交一段数据给TCP发送时,TCP并不立刻发送此段数据,而是等待一小段时间,看看在等待期间是否还有要发送的数据,若有则会一次把这两段数据发送出去.这是对Nagle算法一个简单的解释,详细的请看相关书籍.象C和D的情况就有可能是Nagle算法造成的.
2.接收端接收不及时造成的接收端粘包:TCP会把接收到的数据存在自己的缓冲区中,然后通知应用层取数据.当应用层由于某些原因不能及时的,TCP的数据取出来,就会造成TCP缓冲区中存放了几段数据。
对于完成端口,我们可以提交接收包头长度的数据的请求,当GetQueuedCompletionStatus返回时,我们判断接收的数据长度是否等于包头长度,若等于,则提交接收包体长度的数据的请求,若不等于则提交接收剩余数据的请求。下面相关的代码。
void CTCPSvrd::RecvData(CPERIOHandleData* pHandleData, CPERIOData* pIOData, DWORD dwBytesTransfered)
{
int nRet = 0;
DWORD dwRecvBytes = 0;
DWORD dwFlags = 0;
if (pIOData->m_DataBuf.len == dwBytesTransfered )
{
if( pIOData->m_bPackHead == TRUE )
{
PACKAGE_HEAD *pPackageHead = (PACKAGE_HEAD *)(pIOData->m_Buffer);
pIOData->m_bPackHead = FALSE;
pIOData->m_DataBuf.len = pPackageHead->wDataLen;
pIOData->m_dwRecvBytes += dwBytesTransfered;
pIOData->m_DataBuf.buf = pIOData->m_Buffer + pIOData->m_dwRecvBytes;
printf("recv head. ver = %d, com = %d, len = %d/n", pPackageHead->wVersion, pPackageHead->wCommand, pPackageHead->wDataLen);
dwFlags = 0;
pIOData->m_nIOType = IORead;
nRet = WSARecv(pHandleData->GetClietnSocket(),
&(pIOData->m_DataBuf),
1,
&dwRecvBytes,
&dwFlags,
&pIOData->m_OVerlapped,
NULL);
}
else///接收到的是包体
{
pIOData->m_dwRecvBytes += dwBytesTransfered;
char* p = pIOData->m_Buffer;
p += sizeof(PACKAGE_HEAD);
printf("recv Finished. nCount = %d, %s/n", pIOData->m_dwRecvBytes, p);
//记录这次的句柄和数据ID
PACKAGE_HEAD* pPackHead = (PACKAGE_HEAD*)pIOData->m_Buffer;
pPackHead->wIOHandleID = pHandleData->GetObjectID();
pPackHead->wIODataID = pIOData->GetObjectID();
nRet = m_pC2SMsgBuf->PushOneCode((BYTE*)pIOData->m_Buffer, (unsigned short)pIOData->m_dwRecvBytes);
printf("push one code Return value = %d/n", nRet);
//进行下一次投递
RecvHeadData(pHandleData->GetClietnSocket());
}
}
else///接收的数据还不完整
{
printf("recv continue/n");
pIOData->m_DataBuf.len -= dwBytesTransfered;
pIOData->m_dwRecvBytes += dwBytesTransfered;
pIOData->m_DataBuf.buf = pIOData->m_Buffer + pIOData->m_dwRecvBytes;
dwFlags = 0;
pIOData->m_nIOType = IORead;
nRet = WSARecv(pHandleData->GetClietnSocket(),
&(pIOData->m_DataBuf),
1,
&dwRecvBytes,
&dwFlags,
&pIOData->m_OVerlapped,
NULL);
}
}
void CTCPSvrd::SendData(CPERIOHandleData* pHandleData, CPERIOData* pIOData, DWORD dwBytesTransfered)
{
int nRet = 0;
DWORD dwSendBytes = 0;
if (pIOData->m_DataBuf.len == dwBytesTransfered )
{
if( pIOData->m_bPackHead == TRUE )
{
PACKAGE_HEAD *pPackageHead = (PACKAGE_HEAD *)(pIOData->m_Buffer);
memset(&pIOData->m_OVerlapped, 0, sizeof(OVERLAPPED));
pIOData->m_bPackHead = FALSE;
pIOData->m_DataBuf.len = pPackageHead->wDataLen;
pIOData->m_dwSendBytes += dwBytesTransfered;
pIOData->m_DataBuf.buf = pIOData->m_Buffer + pIOData->m_dwSendBytes;
printf("send head. ver = %d, com = %d, len = %d/n", pPackageHead->wVersion, pPackageHead->wCommand, pPackageHead->wDataLen);
pIOData->m_nIOType = IOWrite;
nRet = WSASend(pHandleData->GetClietnSocket(),
&(pIOData->m_DataBuf),
1,
&dwSendBytes,
0,
&pIOData->m_OVerlapped,
NULL);
}
else///发送的是包体
{
pIOData->m_dwSendBytes += dwBytesTransfered;
printf("Send Finished. nCount = %d/n", pIOData->m_dwSendBytes);
//清空该缓冲区,供下一次全用
pIOData->ClearData();
}
}
else///发送的数据还不完整
{
printf("send continue/n");
pIOData->m_DataBuf.len -= dwBytesTransfered;
pIOData->m_dwSendBytes += dwBytesTransfered;
pIOData->m_DataBuf.buf = pIOData->m_Buffer + pIOData->m_dwSendBytes;
pIOData->m_nIOType = IOWrite;
nRet = WSASend(pHandleData->GetClietnSocket(),
&(pIOData->m_DataBuf),
1,
&dwSendBytes,
0,
&pIOData->m_OVerlapped,
NULL);
}
}
上面仅实现了TCPSvrd,在后面的学习中我将会主要实现LogSvrd中服务器之间的通信。这是我近期学习Windows网络编程的一点总结。希望大家给出好的建议。