TCP粘包原因及解决办法

1、粘包的概念

粘包:多个数据包被连续存储于连续的缓存中,在对数据包进行读取时由于无法确定发生方的发送边界,而采用某一估测值大小来进行数据读出,若双方的size不一致时就会使指发送方发送的若干包数据到接收方接收时粘成一包,从接收缓冲区看,后一包数据的头紧接着前一包数据的尾。

比如说:发送方发送了两个数据,接收方一次收了一个半数据(接收方可能不清楚一个包有多大)
在这里插入图片描述

2、tcp,udp报文最大长度

ip报文:
在这里插入图片描述
tcp报文:
在这里插入图片描述
udp报文:
在这里插入图片描述
ip和udp都有16位的长度字段,理论上ip和udp报文的最大负载长度应该为2^16,64KB,而tcp没有长度字段,理论上应该和ip一样。
但是存在其他限制
网络层限制:MTU(Maximum Transmission Unit,最大传输单元),这限制了ip报文长度,ip报文长度大于mtu,就需要分片。通常设为1500字节。
传输层限制:tcp协议中有个MSS(最大报文长度),tcp通常将数据分成长度为MSS的若干块。通常为MTU(1500)-IP数据包包头(20)-TCP数据段的包头(20)=1460字节。
所以tcp一次传输数据应该最大有1460字节。

3、出现粘包的原因

出现粘包现象的原因是多方面的,它既可能由发送方造成,也可能由接收方造成。

发送方引起的粘包是由TCP协议本身造成的:

  • TCP为提高传输效率,发送方往往要收集到足够多的数据后才发送一包数据。若连续几次发送的数据都很少,通常TCP会根据优化算法把这些数据合成一包后一次发送出去,这样接收方就收到了粘包数据。
    在这里插入图片描述
  • TCP协议规定有MSS,如果数据包过长就会被分开传输。这样接收方就收到了粘包数据。
    在这里插入图片描述

接收方引起的粘包是由于接收方用户进程不及时接收数据,从而导致粘包现象。这是因为接收方先把收到的数据放在系统接收缓冲区,用户进程从该缓冲区取数据,若下一包数据到达时前一包数据尚未被用户进程取走,则下一包数据放到系统接收缓冲区时就接到前一包数据之后,而用户进程根据预先设定的缓冲区大小从系统接收缓冲区取数据,这样就一次取到了多包数据。

在代码中常见体现:

  1. 要发送的数据大于TCP发送缓冲区剩余空间大小,将会发生拆包。
  2. 要发送的数据大于MSS,TCP在传输前将进行拆包。
  3. 要发送的数据小于TCP发送缓冲区的大小,TCP将多次写入缓冲区的数据一次发送出去,将会发生粘包。
  4. 接收数据端的应用层没有及时读取接收缓冲区中的数据,将发生粘包。
    等等。

4、粘包的处理方式:

  1. 当时短连接的情况下,不用考虑粘包的情况
  2. 如果发送数据无结构,如文件传输,这样发送方只管发送,接收方只管接收存储就ok,也不用考虑粘包
  3. 如果双方建立长连接,需要在连接后一段时间内发送不同结构数据
    • 发送端给每个数据包添加包首部,首部中应该至少包含数据包的长度,这样接收端在接收到数据后,通过读取包首部的长度字段,便知道每一个数据包的实际长度了。
    • 发送端将每个数据包封装为固定长度(不够的可以通过补0填充),这样接收端每次从接收缓冲区中读取固定长度的数据就自然而然的把每个数据包拆分开来。
    • 可以在数据包之间设置边界,如添加特殊符号,这样,接收端通过这个边界就可以将不同的数据包拆分开。
      等等。

在网课学习中看到的指定数据长度的解决方法,主要思路:
我们在数据结构中有个成员代表了长度(消息头),我们准备一个足够大的消息缓冲区(程序中是1024000个字节),循环使用socket中的recv每次读取最多102400个字节,然后把循环接收的消息拼接到消息缓冲区中,直到接收到的消息大于消息头指示的长度,则接收到了一个完整的消息(所以我们的消息缓冲区要比完整的消息还要大才行),进行消息处理。

主要代码:

// 缓冲区最小单元大小
#ifndef RECV_BUFF_SZIE
#define RECV_BUFF_SZIE 102400
#endif // !RECV_BUFF_SZIE
// 第二缓冲区 消息缓冲区
char _szMsgBuf[RECV_BUFF_SZIE * 10] = {};
// 消息缓冲区的数据尾部位置
int _lastPos = 0;
// 接收缓冲区
char _szRecv[RECV_BUFF_SZIE] = {};

// 接收数据 处理粘包 拆分包
int RecvData(SOCKET cSock)
{
	// 5 接收数据
	int nLen = (int)recv(cSock, _szRecv, RECV_BUFF_SZIE, 0);
	//printf("nLen=%d\n", nLen);
	if (nLen <= 0)
	{
		printf("<socket=%d>与服务器断开连接,任务结束。\n", cSock);
		return -1;
	}
	// 将收取到的数据拷贝到消息缓冲区
	memcpy(_szMsgBuf+_lastPos, _szRecv, nLen);
	// 消息缓冲区的数据尾部位置后移
	_lastPos += nLen;
	// 判断消息缓冲区的数据长度大于消息头DataHeader长度
	while (_lastPos >= sizeof(DataHeader))
	{
		// 这时就可以知道当前消息的长度
		DataHeader* header = (DataHeader*)_szMsgBuf;
		// 判断消息缓冲区的数据长度大于消息长度
		if (_lastPos >= header->dataLength)
		{
			// 消息缓冲区剩余未处理数据的长度
			int nSize = _lastPos - header->dataLength;
			// 处理网络消息
			OnNetMsg(header);
			// 将消息缓冲区剩余未处理数据前移
			memcpy(_szMsgBuf, _szMsgBuf + header->dataLength, nSize);
			// 消息缓冲区的数据尾部位置前移
			_lastPos = nSize;
		}
		else
		{
			//消息缓冲区剩余数据不够一条完整消息
			break;
		}
	}
	return 0;
}

可运行程序

百度云链接:https://pan.baidu.com/s/1D4VsM2TGIsGUa8tO9p5x5w
提取码:fk5m

  • 25
    点赞
  • 118
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值