1.什么是TCP粘包
TCP(Transmission Control Protocol,传输控制协议)是一种面向连接的、可靠的、基于字节流的传输协议,它保证了数据的可靠性和顺序性。然而,由于TCP是基于字节流而不是消息的,因此在传输过程中可能会出现粘包(Packing)和拆包(Unpacking)问题。
**粘包问题(TCP粘包现象)**指的是发送方在传输数据时,TCP协议把多个发送的小数据包“粘”在一起,形成一个大的数据包发送;或者接收方在接收数据时,多个小的数据包被“粘”在一起,形成一个大的数据包接收。这种现象的发生是由于TCP协议的工作机制导致的。
原因和机制
-
TCP工作方式:TCP是基于字节流的协议,它并不了解上层应用发送的消息边界(Message Boundary)。它只负责把接收到的字节流按照顺序交给应用层,因此多个发送的小数据包在传输过程中有可能会被合并成一个大的数据包发送,或者一个大的数据包被拆分成多个小数据包接收。
-
发送端的粘包:
- 发送端应用程序往往会先把数据放入TCP发送缓冲区,然后TCP根据自身的发送策略(如Nagle算法等)进行发送,可能会合并多个数据包一起发送,以提高网络利用率和性能。
- 如果发送端应用程序发送的消息比较小,并且发送速率较快,这些小消息在TCP层可能会被合并成一个大的数据包发送,导致接收方接收到的数据出现粘包现象。
-
接收端的粘包:
- 接收端应用程序从TCP接收缓冲区中读取数据时,由于TCP层不了解应用层的消息边界,可能一次性把多个发送的小数据包“粘”在一起交给应用层处理。
- 如果接收端应用程序处理消息的速度跟不上数据的接收速度,会导致接收到的数据出现粘包现象。
例如:
- 客户端和服务器之间要进行基于TCP的套接字通信
- 通信过程中客户端会每次会不定期给服务器发送一个不定长度的有特定含义的字符串。
- 通信的服务器端每次都需要接收到客户端这个不定长度的字符串,并对其进行解析
根据上面的描述,服务器在接收数据的时候有如下几种情况:
- 一次接收到了客户端发送过来的一个完整的数据包
- 一次接收到了客户端发送过来的N个数据包,由于每个包的长度不定,无法将各个数据包拆开
- 一次接收到了一个或者N个数据包 + 下一个数据包的一部分,还是很悲剧,无法将数据包拆开
- 一次收到了半个数据包,下一次接收数据的时候收到了剩下的一部分+下个数据包的一部分,更悲剧,头大了
- 另外,还有一些不可抗拒的因素:比如客户端和服务器端的网速不一样,发送和接收的数据量也会不一致
解决方案
粘包问题在实际的网络编程中是常见的,需要采取一些策略来解决或者减少其影响:
-
消息边界标记:在发送的消息中加入特定的消息边界标记(如换行符
\n
),接收端根据消息边界标记来分割接收到的数据,从而识别出完整的消息。有缺陷: 效率低, 需要一个字节一个字节接收, 接收一个字节判断一次, 判断是不是那个特殊字符串 -
消息长度固定:发送端将每个消息的长度固定,接收端根据固定长度来分割接收到的数据,从而确保每个接收到的数据包含完整的消息。缺点:容易造成空间浪费
-
消息头部长度字段:发送端在每个消息前加入一个固定长度的消息头部,包含消息的长度信息,接收端根据头部长度字段来读取对应长度的消息数据。这时候数据由两部分组成:数据头+数据块,数据头:存储当前数据包的总字节数,接收端先接收数据头,然后在根据数据头接收对应大小的字节,数据块:当前数据包的内容
-
使用标准的应用层协议(比如:http、https)来封装要传输的不定长的数据包
2.解决方案具体实现
这里我们使用消息头+数据块的解决方案,如果使用TCP进行套接字通信,如果发送的数据包粘连到一起导致接收端无法解析,我们通常使用添加包头的方式轻松地解决掉这个问题。关于数据包的包头大小可以根据自己的实际需求进行设定,这里没有啥特殊需求,因此规定包头的固定大小为4个字节,用于存储当前数据块的总字节数。
发送端设计
对于发送端来说,数据的发送分为以下四步:
-
动态申请内存: 根据待发送的数据长度 N申请一块大小为 N+4 的内存,其中4个字节用于存储包头信息。
-
写入包头: 将待发送数据的总长度(N)写入申请的内存的前四个字节中,并将其转换为网络字节序(大端序)。
-
拷贝数据并发送: 将待发送的数据拷贝到包头后面的地址空间中,然后将整个数据包发送出去。这里需要确保数据包能够完整发送,因此可以设计一个发送函数,确保当前数据包中的数据全部发送完毕。
-
释放内存: 发送完毕后,释放申请的堆内存。