一.为什么基于TCP的通讯程序需要进行封包和拆包.
TCP是个"流"协议,所谓流,就是没有界限的一串数据.大家可以想想河里的流水,是连成一片的,其间是没有分界线的.但一般通讯程序开发是需要定义一个个相互独立的数据包的,比如用于登陆的数据包,用于注销的数据包.由于TCP"流"的特性以及网络状况,在进行数据传输时会出现以下几种情况.
假设我们连续调用两次send分别发送两段数据data1和data2,在接收端有以下几种接收情况(当然不止这几种情况,这里只列出了有代表性的情况).
A.先接收到data1,然后接收到data2.
B.先接收到data1的部分数据,然后接收到data1余下的部分以及data2的全部.
C.先接收到了data1的全部数据和data1的部分数据,然后接收到了data2的余下的数据.
D.一次性接收到了data1和data2的全部数据.
对于A这种情况正是我们需要的,不再做讨论.对于B,C,D的情况就是大家经常说的"粘包",就需要我们把接收到的数据进行拆包,拆成一个个独立的数据包.为了拆包就必须在发送端进行封包.
二.为什么会出现B.C.D的情况.
"粘包"可发生在发送端也可发生在接收端.
1.由Nagle算法造成的发送端的粘包:Nagle算法是一种改善网络传输效率的算法.简单的说,当我们提交一段数据给TCP发送时,TCP并不立刻发送此段数据,而是等待一小段时间,看看在等待期间是否还有要发送的数据,若有则会一次把这两段数据发送出去.这是对Nagle算法一个简单的解释,详细的请看相关书籍.象C和D的情况就有可能是Nagle算法造成的.
三.怎样封包和拆包.
封包就是给一段数据加上包头,这样一来数据包就分为包头和包体两部分内容了(以后讲过滤非法包时封包会加入"包尾"内容).包头其实上是个大小固定的结构体,其中有个结构体成员变量表示包体的长度,这是个很重要的变量,其他的结构体成员可根据需要自己定义.根据包头长度固定以及包头中含有包体长度的变量就能正确的拆分出一个完整的数据包.
环形缓冲实现方案是定义两个指针,分别指向有效数据的头和尾.在存放数据和删除数据时只是进行头尾指针的移动.
用代码来说明.注:下面的代码是采用一个开源的游戏服务器的代码,我对此代码有所修改.
2.利用底层的缓冲区来进行拆包
由于TCP也维护了一个缓冲区,所以我们完全可以利用TCP的缓冲区来缓存我们的数据,这样一来就不需要为每一个连接分配一个缓冲区了.另一方面我们知道recv或者wsarecv都有一个参数,用来表示我们要接收多长长度的数据.利用这两个条件我们就可以对第一种方法进行优化.
相关代码如下:
char PackageHead[1024];
char PackageContext[1024*20];
int len;
PACKAGE_HEAD *pPackageHead;
while( m_bClose == false )
{
memset(PackageHead,0,sizeof(PACKAGE_HEAD));
len = m_TcpSock.ReceiveSize((char*)PackageHead,sizeof(PACKAGE_HEAD));
if( len == SOCKET_ERROR )
{
}
if(len == 0)
{
}
pPackageHead = (PACKAGE_HEAD *)PackageHead;
memset(PackageContext,0,sizeof(PackageContext));
if(pPackageHead->nDataLen>0)
{
len = m_TcpSock.ReceiveSize((char*)PackageContext,pPackageHead->nDataLen);
}
m_TcpSock是一个封装了SOCKET的类的变量,其中的ReceiveSize用于接收一定长度的数据,直到接收了一定长度的数据或者网络出错才
返回.
int winSocket::ReceiveSize( char* strData, int iLen )
{
if( strData == NULL )
return ERR_BADPARAM;
char *p = strData;
int len = iLen;
int ret = 0;
int returnlen = 0;
while( len > 0)
{
ret = recv( m_hSocket, p+(iLen-len), iLen-returnlen, 0 );
if ( ret == SOCKET_ERROR || ret == 0 )
{
return ret;
}
len -= ret;
returnlen += ret;
}
return returnlen;
}
对于非阻塞的SOCKET,比如完成端口,我们可以提交接收包头长度的数据的请求,当GetQueuedCompletionStatu
下面给出相关代码
enum IOType
{
IOInitialize,
IORead,
IOWrite,
IOIdle
};
class OVERLAPPEDPLUS
{
public:
OVERLAPPEDm_ol;
IOTypem_ioType;
bool
int
WSABUF
int
char
OVERLAPPEDPLUS(IOType ioType) {
ZeroMemory(this, sizeof(OVERLAPPEDPLUS));
m_ioType = ioType;
}
};
接收连接后发出的第一个请求,请求接收包头大小的数据.
OVERLAPPEDPLUS*pOverlappedPlus =
pOverlappedPlus->m_wsaBuffer.buf =
pOverlappedPlus->m_wsaBuffer.len =
pOverlappedPlus->m_bIsPackageHead = true;
pOverlappedPlus->m_RecvPos = 0;
pOverlappedPlus->m_ioType = IORead;
DWORD RecvBytes;
Flags = 0;
if (WSARecv(clientSocket, &(pOverlappedPlus->m_wsaBuffer), 1, &RecvBytes, &Flags,
&pOverlappedPlus->m_ol, NULL) == SOCKET_ERROR)
{
if (WSAGetLastError() != ERROR_IO_PENDING)
{
delete pOverlappedPlus;
}
else
{
///相关的错误处理
}
}
else
{
///相关的错误处理
}
在GetQueuedCompletionStatu
if( pOverlapPlus->m_ioType== IORead)
{
if( pOverlapPlus->m_wsaBuffer.len == dwIoSize )
{
if( pOverlapPlus->m_bIsPackageHead == true )///接收到的是包头。
{
PACKAGE_HEAD *pPackageHead = (PACKAGE_HEAD *)(pOverlapPlus->m_Buffer);
if(pThis->IsLegalityPackageHead(pPackageHead)==false)///判断是否是合法的包
{
closesocket(lpClientContext->m_Socket);
continue;
}
pOverlapPlus->m_bIsPackageHead = false;
pOverlapPlus->m_wsaBuffer.len = pPackageHead->nDataLen;
pOverlapPlus->m_RecvPos += dwIoSize;
pOverlapPlus->m_wsaBuffer.buf = pOverlapPlus->m_Buffer+pOverlapPlus->m_RecvPos;
}
else///接收到的是包体
{
pOverlapPlus->m_RecvPos += dwIoSize;
///这时pOverlapPlus->m_Buffer里就存放了一个完整的数据包,长度为pOverlapPlus->m_RecvPos
///继续请求 请求下一个数据包的包头
pOverlapPlus->m_wsaBuffer.buf =
memset(pOverlapPlus->m_Buffer,0,sizeof(pOverlapPlus->m_Buffer));
pOverlapPlus->m_wsaBuffer.len =
pOverlapPlus->m_bIsPackageHead = true;
pOverlapPlus->m_RecvPos = 0;
}
}
else///接收的数据还不完整
{
pOverlapPlus->m_wsaBuffer.len -= dwIoSize;
pOverlapPlus->m_RecvPos += dwIoSize;
pOverlapPlus->m_wsaBuffer.buf = pOverlapPlus->m_Buffer+pOverlapPlus->m_RecvPos;
}
pOverlapPlus->m_ioType = IORead;
state = WSARecv(lpClientContext->m_Socket, &(pOverlapPlus->m_wsaBuffer), 1, &RecvBytes, &Flags,
&pOverlapPlus->m_ol, NULL);
if ( state == SOCKET_ERROR)
{
if(WSAGetLastError() != ERROR_IO_PENDING)
{
continue;
}
}
}