一 概述
经过上图咱们看到了接收方为了接受这两条连贯的指令,一共作了三次接受,第二次接收的时候,收到了一部分message1的内容和一部分message2的内容。这里要说明几个注意事项: 1. MSS(maxmium segment size最大报文段大小,TCP层):MSS属性是TCP链接双方在三次握手时所确认的每个TCP报文段中数据字段的最大长度。注意,一是链接双方协商出来的;二是只是数据段的最大长度,不包括IP协议头和TCP协议头的最大长度。 2. 半包是指接收方应用程序在接收信息时,没有接收到一个完成的信息格式块;粘包是指,接收方应用程序在接受信息时,除了接收到发送方应用程序发送的某一个完整数据信息描述外,还接受到了一下发送方应用程序发送的下一个数据信息的一部分。 3. 半包和粘包是针对应用程序来讲的,这个问题只会发生在TCP一些进行连续发送数据时(TCP长链接)。UDP不会出现这个问题,由于UDP都是有边界的数据报;TCP短链接也不会出现,由于发送完一个指令信息后链接就断开了,不会发送第二个指令数据。 4. 半包和粘包问题产生的根本是由于TCP本质上没有“数据块”的概念,而是一连串的数据流。在应用程序层面上咱们所定义的“数据块”在TCP层面上并不被协议承认 5. 半包/粘包是一个应用层问题。要解决半包/粘包问题,就是在应用程序层面创建协商一致的信息还原依据。常见的有两种方式:一是消息定长,即保证每个完整的信息描述的长度都是必定的,这样不管TCP/IP协议如何进行分片, 数据接收方均可以按照固定长度进行消息的还原。二是在完整的一块数据结束后增长协商一致的分隔符(例如增长一个回车符)。
MTU(maxmium transfer unit最大传输单元,处于数据链路层,mac也是数据链路层),46-1500字节大小。如果ip数据包超过1500,会分片。IP层的分片更多的是为传输层的UDP服务。UDP不会分段,就由IP来分。TCP会分段,当然就不用IP来分了!
为什么会发生 TCP 粘包、拆包?
- 要发送的数据大于 TCP 发送缓冲区剩余空间大小,将会发生拆包。
- 待发送数据大于 MSS(最大报文长度),TCP 在传输前将进行拆包。
- 要发送的数据小于 TCP 发送缓冲区的大小,TCP 将多次写入缓冲区的数据一次发送出去,将会发生粘包。
- 接收数据端的应用层没有及时读取接收缓冲区中的数据,将发生粘包。
二 处理粘包分包代码:
(1)协议:
(2)代码:
#include "stdafx.h" #include"windows.h" #include<iostream> #define CHONG_QING_SIX_LOCK_FRAME_MAX_LENGTH 4096 #define CHONG_QING_SIX_LOCK_DATA_HEADER_LENGTH 6 int RecvData() { DWORD recv_len = 0; int dataLength = 0; int sumDataLength = 0; int nRemainSize = 0; int lastPos = 0; BYTE recvbuf[CHONG_QING_SIX_LOCK_FRAME_MAX_LENGTH], databuf[CHONG_QING_SIX_LOCK_FRAME_MAX_LENGTH]; char oneFrameData[1024]; memset(recvbuf, 0, sizeof(recvbuf)); memset(databuf, 0, sizeof(databuf)); //收到服务端消息 //接受数据,处理粘包,拆分包 recv_len = (int)recv(m_Socket, (char *)recvbuf, CHONG_QING_SIX_LOCK_FRAME_MAX_LENGTH, 0); if (recv_len > 0) { memcpy(databuf + lastPos, recvbuf, recv_len); lastPos += recv_len; //判断消息缓冲区的数据长度大于消息头 while (lastPos >= CHONG_QING_SIX_LOCK_DATA_HEADER_LENGTH) { //包头做判断,如果包头错误,收到的数据全部清空 if (databuf[0] == 0xEF && databuf[1] == 0xEF && databuf[2] == 0xEF && databuf[3] == 0xEF) { dataLength = MAKEWORD(databuf[4], databuf[5]); sumDataLength = CHONG_QING_SIX_LOCK_DATA_HEADER_LENGTH + dataLength + 6; //判断消息缓冲区的数据长度大于消息体 if (lastPos >= sumDataLength) { //CRC校验 if (CheckSum((byte *)databuf, dataLength + CHONG_QING_SIX_LOCK_DATA_HEADER_LENGTH + 2)) { memcpy(oneFrameData, databuf, sumDataLength); //处理数据 DealData(oneFrameData); //剩余未处理消息缓冲区数据的长度 nRemainSize = lastPos - sumDataLength; //将未处理的数据前移 if (nRemainSize > 0) { memcpy(databuf, databuf + (dataLength + CHONG_QING_SIX_LOCK_DATA_HEADER_LENGTH + 6), nRemainSize); lastPos = nRemainSize; } } else { if (nRemainSize > 0) { memcpy(databuf, databuf + sumDataLength, nRemainSize); } lastPos = nRemainSize; } } else { break; } } else //寻找下一个包头 { BOOL isFind = FALSE; int nFindStart = 0; for (int k = 1; k < lastPos; k++) { if (databuf[k] == 0xEF && databuf[k + 1] == 0xEF && databuf[k + 2] == 0xEF && databuf[k + 3] == 0xEF) { nFindStart = k; isFind = TRUE; break; } } if (isFind == TRUE) { memcpy(databuf, databuf + nFindStart, lastPos - nFindStart); lastPos = lastPos - nFindStart; } else { memset(databuf, 0, sizeof(databuf)); lastPos = 0; break; } } } } return 0; } int _tmain(int argc, _TCHAR* argv[]) { system("pause"); return 0; }
三 对以上代码处理流程解释:
1)可以看到以上代码分为2个缓冲区,第一个只负责接收数据,第二个负责处理数据。
2)当服务端收到一包数据时,2种情况时,将处理数据的缓冲区的数据全部清空,(1)当出现包头验证错误,(2)CRC校验失败。
3)当接受到的数据长度大于包头,但是不大于整个消息体的长度时,保存前面接收的数据后,跳出while循环,继续接受数据,
4)当接收到的数据是多包数据时,它会一直在while循环里面处理数据,直到把每一包数据都分开并处理完后跳出。