对于基于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缓冲区中存放了几段数据.
三.怎样封包和拆包.
最初遇到"粘包"的问题时,我是通过在两次send之间调用sleep来休眠一小段时间来解决.这个解决方法的缺点是显而易见的,使传输效率大大降低,而
且也并不可靠.后来就是通过应答的方式来解决,尽管在大多数时候是可行的,但是不能解决象B的那种情况,而且采用应答方式增加了通讯量,加重了网络负荷
(但是象FTP等协议采用的就是应答方式).再后来就是对数据包进行封包和拆包的操作.
封包:
封包就是给一段数据加上包头,这样一来数据包就分为包头和包体两部分内容了(以后讲过滤非法包时封包会加入"包尾"内容).包头其实上是
个大小固定的结构体,其中有个结构体成员变量表示包体的长度,这是个很重要的变量,其他的结构体成员可根据需要自己定义.根据包头长度固定以及包头中含有
包体长度的变量就能正确的拆分出一个完整的数据包.
对于拆包目前我最常用的是以下两种方式.
1.动态缓冲区暂存方式.之所以说缓冲区是动态的是因为当需要缓冲的数据长度超出缓冲区的长度时会增大缓冲区长度.
大概过程描述如下:
A,为每一个连接动态分配一个缓冲区,同时把此缓冲区和SOCKET关联,常用的是通过结构体关联.
B,当接收到数据时首先把此段数据存放在缓冲区中.
C,判断缓存区中的数据长度是否够一个包头的长度,如不够,则不进行拆包操作.
D,根据包头数据解析出里面代表包体长度的变量.
E,判断缓存区中除包头外的数据长度是否够一个包体的长度,如不够,则不进行拆包操作.
F,取出整个数据包.这里的"取"的意思是不光从缓冲区中拷贝出数据包,而且要把此数据包从缓存区中删除掉.删除的办法就是把此包后面的数据移动到缓冲区的起始地址.
这种方法有两个缺点.1.为每个连接动态分配一个缓冲区增大了内存的使用.2.有三个地方需要拷贝数据,一个地方是把数据存放在缓冲区,一个地方是把完整的数据包从缓冲区取出来,一个地方是把数据包从缓冲区中删除.这种拆包的改进方法会解决和完善部分缺点.
下面给出相关代码.
先看包头结构定义
#pragma pack(push,1) //开始定义数据包,
采用字节对齐方式
/*----------------------包头---------------------*/
typedef struct
tagPACKAGEHEAD
{
BYTE Version;
WORD Command;
WORD
nDataLen;//包体的长度
}PACKAGE_HEAD;
#pragma pack(pop) //结束定义数据包, 恢复原来对齐方式
然后看存放数据和"取"数据函数.
/*****************************************************************************
Description:添加数据到缓存
Input:pBuff[in]-待添加的数据;nLen[in]-待添加数据长度
Return:
如果当前缓冲区没有足够的空间存放pBuff则返回FALSE;否则返回TRUE。
******************************************************************************/
BOOL
CDataBufferPool::AddBuff( char *pBuff, int nLen
)
{
m_cs.Lock();///临界区锁
if ( nLen < 0
)
{
m_cs.Unlock();
return
FALSE;
}
if ( nLen <= GetFreeSize()
)///判断剩余空间是否足够存放nLen长的数据
{
memcpy(m_pBuff + m_nOffset,
pBuff, nLen);
m_nOffset +=
nLen;
}
else///若不够则扩充原有的空间
{
char
*p = m_pBuff;
m_nSize +=
nLen*2;//每次增长2*nLen
m_pBuff = new
char[m_nSize];
memcpy(m_pBuff,p,m_nOffset);
delete
[]p;
memcpy(m_pBuff + m_nOffset, pBuff,
nLen);
m_nOffset +=
nLen;
m_cs.Unlock();
return
FALSE;
}
m_cs.Unlock();
return TRUE;
}
/*****************************************************************************
Description:获取一个完整的包
Input:Buf[out]-获取到的数据;nLen[out]-获取到的数据长度
Return:
1、当前缓冲区不够一个包头的数据
2、当前缓冲区不够一个包体的数据
******************************************************************************/
int CDataBufferPool::GetFullPacket( char *Buf, int& nLen
)
{
m_cs.Lock();
if ( m_nOffset < m_PacketHeadLen
)//当前缓冲区不够一个包头的数据
{
m_cs.Unlock();
return
1;
}
PACKAGE_HEAD *p = (PACKAGE_HEAD *)m_pBuff;
if(
(m_nOffset-m_PacketHeadLen) < (int)p->nDataLen
)//当前缓冲区不够一个包体的数据
{
m_cs.Unlock();
return
2;
}
//判断包的合法性
/*int IsIntegrallity =
ValidatePackIntegrality(p);
if( IsIntegrallity != 0
)
{
m_cs.Unlock();
return
IsIntegrallity;
}
*/
nLen =
m_PacketHeadLen+p->nDataLen;
memcpy( Buf, m_pBuff, nLen
);
m_nOffset -= nLen;
memcpy( m_pBuff, m_pBuff+nLen, m_nOffset
);
m_cs.Unlock();
return 0;
}
前面提到过这种方法的缺点.下面给出一个改进办法,
即采用环形缓冲.但是这种改进方法还是不能解决第一个缺点以及第一个数据拷贝,只能解决第三个地方的数据拷贝(这个地方是拷贝数据最多的地方).第2种拆包方式会解决这两个问题.
环形缓冲实现方案是定义两个指针,分别指向有效数据的头和尾.在存放数据和删除数据时只是进行头尾指针的移动.
用代码来说明.注:下面的代码是采用一个开源的游戏服务器的代码,我对此代码有所修改.
int
CCircularBufferPool::PutData(TCHAR *pData, int len)
{
if( len <=
0 )
return 1;
EnterCriticalSection(&m_cs);
while
(IsOverFlowCondition(len))///判断缓冲区剩余空间是否够存放len长的数据
{
BufferResize(len);///若不够,则扩充缓冲区.
}
if (IsIndexOverFlow(len))///判断"尾"指针的位置.
{
int
FirstCopyLen = m_iBufSize-m_iTailPos;
int SecondCopyLen = len -
FirstCopyLen;
CopyMemory(m_pBuffer+m_iTailPos, pData,
FirstCopyLen);
if
(SecondCopyLen)
{
CopyMemory(m_pBuffer,
pData+FirstCopyLen, SecondCopyLen);
m_iTailPos =
SecondCopyLen;
}
else
m_iTailPos =
0;
}
else
{
CopyMemory(m_pBuffer+m_iTailPos,
pData, len);
m_iTailPos += len;
}
LeaveCriticalSection(&m_cs);
return 0;
}
void CCircularBufferPool::GetData(TCHAR *pData, int len, bool
Delete)
{
if (len <
m_iBufSize-m_iHeadPos)
{
CopyMemory(pData,
m_pBuffer+m_iHeadPos,
len);
if(Delete==true)
m_iHeadPos +=
len;
}
else
{
int fc,
sc;
fc = m_iBufSize-m_iHeadPos;
sc = len -
fc;
CopyMemory(pData, m_pBuffer+m_iHeadPos,
fc);
if (sc) CopyMemory(pData+fc, m_pBuffer,
sc);
if(Delete==true)
m_iHeadPos =
sc;
if(m_iHeadPos >=
m_iBufSize)
m_iHeadPos = 0;
}
}
//
//进行自定义包的解析
//
int CCircularBufferPool::GetFullPacket( TCHAR
*Buf, int &nLen
)
{
EnterCriticalSection(&m_cs);
if( GetValidCount()
< m_PacketHeadLen
)//当前缓冲区不够一个包头的数据
{
LeaveCriticalSection(&m_cs);
return
1;
}
GetData(Buf,m_PacketHeadLen,false);
PACKAGE_HEAD *p =
(PACKAGE_HEAD *)Buf;
if( (GetValidCount()-m_PacketHeadLen) <
(int)p->nDataLen
)//当前缓冲区不够一个包体的数据
{
LeaveCriticalSection(&m_cs);
return
2;
}
//判断包的合法性
int IsIntegrallity =
ValidatePackIntegrality(p);
if( IsIntegrallity != 0
)
{
LeaveCriticalSection(&m_cs);
return
IsIntegrallity;
}
GetData(Buf,m_PacketHeadLen+p->nDataLen,true);
nLen
= m_PacketHeadLen+p->nDataLen;
LeaveCriticalSection(&m_cs);
return 0;
}
2.利用底层的缓冲区来进行拆包
由于TCP也维护了一个缓冲区,所以我们完全可以利用TCP的缓冲区来缓存我们的数据,这样一来就不需
要为每一个连接分配一个缓冲区了.另一方面我们知道recv或者wsarecv都有一个参数,用来表示我们要接收多长长度的数据.利用这两个条件我们就可
以对第一种方法进行优化了.
对于阻塞SOCKET来说,我们可以利用一个循环来接收包头长度的数据,然后解析出代表包体长度的那个变量,再用一个循环来接收包体长度的数据.
相关代码如下:
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 )
{
break;
}
if(len ==
0)
{
break;
}
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,比如完成端口,我们可以提交接收包头长度的数据的请求,当
GetQueuedCompletionStatus返回时,我们判断接收的数据长度是否等于包头长度,若等于,则提交接收包体长度的数据的请求,若不等
于则提交接收剩余数据的请求.当接收包体时,采用类似的方法.
下面给出相关代码
enum IOType
{
IOInitialize,
IORead,
IOWrite,
IOIdle
};
class OVERLAPPEDPLUS
{
public:
OVERLAPPEDm_ol;
IOTypem_ioType;
bool
m_bIsPackageHead;//当前接收的数据是否是包头数据。
int
m_count;
WSABUF
m_wsaBuffer;
int
m_RecvPos;
char
m_Buffer[1024*8];//此缓冲要尽可能大
OVERLAPPEDPLUS(IOType ioType) {
ZeroMemory(this,
sizeof(OVERLAPPEDPLUS));
m_ioType =
ioType;
}
};
接收连接后发出的第一个请求,请求接收包头大小的数据.
OVERLAPPEDPLUS*pOverlappedPlus
= new OVERLAPPEDPLUS;
pOverlappedPlus->m_wsaBuffer.buf =
pOverlappedPlus->m_Buffer;
pOverlappedPlus->m_wsaBuffer.len
= PACKAGE_HEAD_LEN;///包头的长度
pOverlappedPlus->m_bIsPackageHead
= true;
pOverlappedPlus->m_RecvPos =
0;
pOverlappedPlus->m_ioType = IORead;
DWORD RecvBytes;
DWORD
Flags;
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
{
///相关的错误处理
}
在GetQueuedCompletionStatus所在的函数中.
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 =
pOverlapPlus->m_Buffer;
memset(pOverlapPlus->m_Buffer,0,sizeof(pOverlapPlus->m_Buffer));
pOverlapPlus->m_wsaBuffer.len
= PACKAGE_HEAD_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;
}
}
}
三:如何判断包的合法性.
判断包的合法性可以结合下面两种方式来判断.但是想100%的判定出非法包,只能通过信息安全中的知识来判定了,对这种方法这里不做阐述.
1.通过包头的结构来判断包的合法性.
最初的时候我是根据包头来判断包的合法性,比如判断Command是否超出命令范围,nDataLen是否大于最大包的长度.但是这种方法无法过滤掉非法包,当出现非法包时我们唯一能做的就是断开连接,或许这也是最好的处理办法.
我
们可以给一个完整的包加上开始和结束标志,标志可以是个整数,也可以是一串字符串.以第一种拆包方式为例来说明.当要拆一个完整包时我们先从缓冲区有效数
据头指针地址搜索包的开始标志,搜索到后并且当前数据够一个包头数据,则判断开始标志和包头是否合法,若合法则根据代表数据长度的变量的值定位到包尾,判
断包尾标志是否与我们定义的一致,若一致则这个包是合法的包.若有一项不一致则继续寻找下个包的开始标志,并把下个合法包的前面的数据全部舍弃.
2.通过逻辑层来判断包的合法性.
当取出一个合法的包时,我们还要根据当前数据处理的逻辑来判断包的合法性.比如说在登陆成功后的某段时间服务器又收到了同一个客户端的登陆包,那我们就可以判断这个包是非法的,简单处理就是断开连接.
分享到:
相关推荐
了解了上述知识点后,我们可以在网络协议分析和网络编程中更加得心应手地处理封包和拆包的问题。无论是使用UDP还是RTP协议进行数据传输,核心原理是一致的。正确地应用字节序和位段操作的知识,可以避免数据的错误...
Qt实现一个简单协议的封包和拆包需求平时通讯经常用到遵循一定协议封包和拆包。项目代码CMD_NAV_REALTIME_DATA = 0x01, // navig
### IPsec传输模式下的ESP...通过上述详尽的封包与拆包过程,传输模式下的ESP机制实现了数据在网络层的有效加密和解密,确保了通信安全的同时,也维护了数据的完整性和机密性,为现代网络通信提供了坚实的安全保障。
### C# 中基于 UDP 协议的拆包与封包技术详解 在网络编程领域,特别是在游戏开发、实时通信系统及高性能应用中,UDP (User Datagram Protocol) 作为一种无连接的传输层协议,因其低开销、高效的特点而备受青睐。...
数据在网络上封包拆包过程
简单的WebSocket 握手 消息拆包 消息封包 方法
在`XuanJiSocketDemo`项目中,我们可以预见到`SocketHelper`类可能包含了封包和拆包的实现,而`ServerDemo`和`ClientDemo`则分别代表了服务器端和客户端的Socket实例。 在服务器端,`ServerDemo`可能会监听指定的...
### TCP 粘包解决办法 #### 一、为何基于TCP的通讯程序需要进行封包与拆包? TCP(Transmission ...在实际应用中,开发人员可以根据具体的应用场景和需求选择合适的封包和拆包方案,以实现高效、稳定的通信服务。
综合以上信息,我们可以推测这是一个使用Qt框架开发的多文件传输工具,它采用了自定义的封包拆包策略来解决网络传输中的粘包问题,并且重点在于功能的实现,而非界面设计。这个项目可能包含一个简单的用户界面,用于...
在IT行业中,网络通信是至关重要的部分,尤其是在分布式系统和实时数据交换的场景下。C#作为.NET框架的主要编程语言,提供了丰富的API来处理网络通信,其中之一就是Socket。本篇文章将详细探讨C#中实现Socket异步...
开发者需要编写函数来处理UDP的封包和拆包过程,确保数据正确地发送和接收。 在具体实现过程中,程序会打开串口,与GPRS模块建立连接。接着,通过发送特定的AT命令来激活短信服务,设置短信中心号码,并指定接收方...
同时,数据报可能需要经过封包和拆包处理,以便适应网络协议。 7. **多线程与异步编程**:在高并发环境中,为了提高性能,加密和解密操作可能需要在多个线程中并行进行,或者使用async/await关键字实现异步处理。 ...
"deb解包封包工具"是一款专为非Linux环境设计的应用,它的主要功能是对`.deb`包进行解包和封包操作。这对于开发者、系统管理员或者对Linux软件感兴趣的用户来说非常有用,他们可能需要在非Debian环境中查看或提取`....
2. 包装与拆包:包装是将原始数据按照特定格式组合成封包,拆包则是将接收到的封包分解回原始数据。 易语言实现封包的步骤: 1. 定义数据结构:首先,你需要用易语言定义一个符合需求的数据结构,比如结构体,包括...
6. **处理可变参数**:Python的可变参数封包和拆包机制(*args, **kwargs)可以帮助我们处理C库中带有可变数量参数的函数。我们可以创建一个元组或字典来传递这些参数。 7. **错误处理**:由于ctypes调用C库可能抛...
4. **封包工具**:为了进一步优化和保护代码,开发者可能会使用封包工具(如7-Zip)对编译后的文件进行压缩和加密。7-Zip是一个强大的压缩工具,支持多种压缩格式,包括7z,ZIP,GZIP,BZIP2和TAR等。 5. **生成可...
Unity中的Socket通信是游戏开发中实现网络交互的重要技术,它基于C#语言,允许服务器和客户端之间进行数据的高效传输。本教程将深入探讨如何在Unity中实现Socket通信,包括TCP连接、粘包/拆包问题的解决方案。 一、...
"封包"和"拆包"这两个术语在计算机领域中通常指的是将一组相关的文件和资源集合在一起,形成一个可方便传输和安装的单一文件或压缩包的过程。飞雪封包工具能够帮助开发者或用户将多个文件整合到一个压缩文件中,以...
网络编程Day1 本资源主要介绍了网络编程的基础知识,包括网络编程的概念、联网协议和层、OSI七层模型、...网络封包与拆包是指MTU(Maximum Transmission Unit)最大传输单元,规定了数据链路层所能传送最大数据长度。