转载: https://www.cnblogs.com/liyux/p/5594423.html
TCP 粘包通俗来讲, 就是发送方发送的多个数据包, 到接收方后粘连在一起, 导致数据包不能完整的体现发送的数据.
TCP 粘包原因分析
导致 TCP 粘包的原因, 可能是发送方的原因, 也有可能是接受方的原因.
发送方
由于 TCP 需要尽可能高效和可靠, 所以 TCP 协议默认采用 Nagle 算法, 以合并相连的小数据包, 再一次性发送, 以达到提升网络传输效率的目的. 但是接收方并不知晓发送方合并数据包, 而且数据包的合并在 TCP 协议中是没有分界线的, 所以这就会导致接收方不能还原其本来的数据包.
接收方
TCP 是基于 "流" 的. 网络传输数据的速度可能会快过接收方处理数据的速度, 这时候就会导致, 接收方在读取缓冲区时, 缓冲区存在多个数据包. 在 TCP 协议中接收方是一次读取缓冲区中的所有内容, 所以不能反映原本的数据信息.
解决 TCP 粘包
分析了产生 TCP 粘包的原因之后, 针对发生的原因, 针对性的采取解决方法.
禁用 Negle 算法
因为 TCP 协议采用 Negle 算法, 导致粘包. 所以可以禁用 Nagle 算法.
- 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 粘包的问题, 所以不考虑其他的.
发送方封包
- 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;
- }
接收方解包
- 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);
- }
- }
接收指定长度的数据函数
- // 接收指定长度的数据
- 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 中就是这样处理的.
UDP 不存在粘包
由于 UDP 不是面向'流'的, 而且 UDP 是具有消息边界的. 也就是说 UDP 的发送的每一个数据包都是独立的. 所以 UDP 并不存在粘包的问题.
返回 网络编程 https://www.cnblogs.com/Toya/p/9745169.HTML
TCP 粘包
来源: http://www.bubuko.com/infodetail-2795805.html