粘包原因
TCP(transport control protocol,传输控制协议)是面向连接的,面向流的,提供高可靠性服务。收发两端(客户端和服务器端)都要有一一成对的socket,因此,发送端为了将多个发往接收端的包,更有效的发到对方,使用了优化方法(Nagle算法),将多次间隔较小且数据量小的数据,合并成一个大的数据块,然后进行封包。这样,接收端,就难于分辨出来了,必须提供科学的拆包机制。 即面向流的通信是无消息保护边界的。
UDP(user datagram protocol,用户数据报协议)是无连接的,面向消息的,提供高效率服务。不会使用块的合并优化算法,, 由于UDP支持的是一对多的模式,所以接收端的skbuff(套接字缓冲区)采用了链式结构来记录每一个到达的UDP包,在每个UDP包中就有了消息头(消息来源地址,端口等信息),这样,对于接收端来说,就容易进行区分处理了。 即面向消息的通信是有消息保护边界的。
由于TCP无消息保护边界, 需要在消息接收端处理消息边界问题。也就是为什么我们以前使用UDP没有此问题。 反而使用TCP后,出现少包的现象。
什么时候考虑粘包
如果利用tcp每次发送数据,就与对方建立连接,然后双方发送完一段数据后,就关闭连接,这样就不会出现粘包问题(因为只有一种包结构,类似于http协议)。
如果发送数据无结构,如文件传输,这样发送方只管发送,接收方只管接收存储就ok,也不用考虑粘包
如果双方建立连接,需要在连接后一段时间内发送不同结构数据,如连接后,有好几种结构:
“hello give me sth about yourself”
“Don’t give me sth about yourself”
如果发送方连续发送这个两个包出去,接收方一次接收可能会是”hello give me sth about yourselfDon’t give me sth about yourself” 这样接收方就傻了,到底是要干嘛?不知道,因为协议没有规定这么诡异的字符串,所以要处理把它分包,怎么分也需要双方组织一个比较好的包结构,所以一般可能会在头加一个数据长度之类的包,以确保接收。
解决办法
禁用Nagle算法
因为TCP协议采用Negle算法,导致粘包。所以可以禁用Nagle算法。
1
2
3
4
5
6
7
|
const char chOpt = 1;
int nErr = setsockopt(m_socket, IPPROTO_TCP, TCP_NODELAY, &chOpt, sizeof(char));
if(nErr == -1)
{
TRACE( "setsockopt() error\n", WSAGetLastError());
return ;
}
|
这种方法虽然能一定程度上解决TCP粘包,但是并不能完全解决问题。因为接收方也是可能造成粘包的原因,这种方法只是发送方有效。而且禁用Nagle算法,一定程度上使TCP传输效率降低了。所以,这并不是一种理想的方法。
PUSH标志
PUSH是TCP报头中的一个标志位,发送方在发送数据的时候可以设置这个标志位。该标志通知接收方将接收到的数据全部提交给接收进程。这里所说的数据包括与此PUSH包一起传输的数据以及之前就为该进程传输过来的数据。
当Server端收到这些数据后,它需要立刻将这些数据提交给应用层进程,而不再等待是否还有额外的数据到达。
设置PUSH标志也不能完全解决TCP粘包,只是降低了接收方粘包的可能性。实际上现在的TCP协议栈基本上都可以自行处理这个问题,而不是交给应用层处理。所以设置PUSH标志,也不是一种理想的方法。
自定协议
自定协议,将数据包分为了封包和解包两个过程。在发送方发送数据时,对发送的数据进行封包操作。在接收方接收到数据时对接收的数据包需要进行解包操作。
自定协议时,封包就是为发送的数据增加包头,包头包含数据的大小的信息,数据就跟随在包头之后。当然包头也可以有其他的信息,比如一些做校验的信息。这里主要讨论TCP粘包的问题,所以不考虑其他的。
发送方封包
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
PACKAGE_HEAD pPackageHead; //PACKAGE_HEAD 包头结构体
char PackageHead[1024];
int headLen = sizeof(PACKAGE_HEAD);
int packgeContextLen = strlen(packageContext); //packageContext 发送的数据
pPackageHead->nDataLen = packgeContextLen; //包的大小
char *packge = (char*)malloc(headLen + packgeContextLen); //包的内存分配
memset(packge, 0, headLen + packgeContextLen);
char *packgeCpy = (char*)memcpy(packge, (char*)&pPackageHead, headLen);//拷贝包头
packgeCpy += headLen;
packge = (char*)memcpy(packgeCpy, (char*)&packageContext, packgeContextLen);//拷贝包内容
int ret = 0;
ret = send(m_hSocket, packge, headLen + packgeContextLen, 0); //发送包
if (ret == SOCKET_ERROR || ret == 0)
{
return ret;
}
|
接收方解包
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
char PackageHead[1024];
char PackageContext[1024*20];
int len;
PACKAGE_HEAD *pPackageHead; //PACKAGE_HEAD 包头结构体
while( m_bClose == false )
{
memset(PackageHead, 0, sizeof(PACKAGE_HEAD));
len = ReceiveSize(m_TcpSock, (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 = ReceiveSize(m_TcpSock, (char*)PackageContext,pPackageHead->nDataLen);
}
}
|
接收指定长度的数据函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
//接收指定长度的数据
int ReceiveSize(SOCKET m_hSocket, char* strData, int gLen)
{
if(strData == NULL)
return ERR_BADPARAM;
char *p = strData;
int len = gLen;
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;
}
|
这样就可以达到解决TCP粘包的问题。在实际使用中包头还带有更多的信息,而且包尾可能还会带上分隔符,在redis、FTP中就是这样处理的。.
参考:http://www.cnblogs.com/liyux/p/5594423.html