前言
TCP是面向字节流(字节流可以理解为一个双向的通道里流淌的数据,这个数据其实就是我们常说的二进制数据,简单来说就是一大堆 01 串。这些 01 串之间没有任何边界)的协议,本来就是没有界限的一串数据,不存在什么“粘包”问题。把它称为“包”是为了更好地理解“粘包、拆包”这两种现象。
TCP开发情况中,“包”可以是TCP在传输的时候封装的报文,分为首部和负载数据;另一种可以说是开发者在应用层封装的报文结构。
为什么要将数据切片
消息在进入传输层时会被切片位一个个数据包,这个数据包的长度是MSS。
我们的网络可以把它比喻成一个管道,这个管道是有宽度的,这个宽度由链路层提供给网络层,以太网一般认为是MTU(1500字节),如果直接传入这个消息,那么就会超过这个宽度,所以需要切片。
- MTU: Maximum Transmit Unit,最大传输单元。 由网络接口层(数据链路层)提供给网络层最大一次传输数据的大小;一般 MTU=1500 Byte。 假设IP层有 <= 1500 byte 需要发送,只需要一个 IP 包就可以完成发送任务;假设 IP 层有> 1500 byte 数据需要发送,需要分片才能完成发送,分片后的 IP Header ID 相同。
- MSS:Maximum Segment Size 。 TCP 提交给 IP 层最大分段大小,不包含 TCP Header 和 TCP Option,只包含 TCP Payload ,MSS 是 TCP 用来限制应用层最大的发送字节数。 假设 MTU= 1500 byte,那么 MSS = 1500- 20(IP Header) -20 (TCP Header) = 1460 byte,如果应用层有 2000 byte 发送,那么需要两个切片才可以完成发送,第一个 TCP 切片 = 1460,第二个 TCP 切片 = 540。
什么叫“粘包”
TCP协议中,发送方发送的若干包数据到接收方接收时粘成一包,从接收缓冲区看,后一包数据的头紧接着前一包数据的尾。
现在比如说发了两条消息,“巧克力味”和“咖啡”,但是粘包后,变成了“巧克力味咖”和“啡”,“巧克力味”作为上一个包的内容与下一个包的“咖”粘在一起,一起被错误地当成了一个数据包解析了出来。
应用层传到 TCP 协议的数据,不是以消息报为单位向目的主机发送,而是以字节流的方式发送到下游,这些数据可能被切割和组装成各种数据包,接收端收到这些数据包后没有正确还原原来的消息,因此出现粘包现象。
至于拆包,如果一次请求发送的数据量比较大,超过了缓冲区大小,TCP就会将其拆分为多次发送,这就是拆包。
为什么UDP没有粘包?
粘包拆包问题在传输层、网络层、链路层都有可能发生,日常网络应用开发都在传输层(U3D面试问传输层居多),由于UDP有消息保护边界,不会发生粘包拆包问题,因此只会有TCP发生这两个问题。
粘包拆包发生场景
正常的理想情况,两个包恰好满足TCP缓冲区的大小或达到TCP等待时长,分别发送两个包;
粘包:两个包较小,间隔时间短,发生粘包,合并成一个包发送;
拆包:一个包过大,超过缓存区大小,拆分成两个或多个包发送;
拆包和粘包:Packet1过大,进行了拆包处理,而拆出去的一部分又与Packet2进行粘包处理。
怎么处理粘包
加入特殊标志
可以通过特殊的标志作为头尾,比如当收到了0xfffffe
或者回车符,则认为收到了新消息的头,此时继续取数据,直到收到下一个头标志0xfffffe
或者尾部标记,才认为是一个完整消息。
采用0xfffffe
标志位,用来标志一个数据包的开头,就不怕你发的某个数据里正好有这个内容吗?
所以一般除了这个标志位,发送端在发送时还会加入各种校验字段(校验和或者对整段完整数据进行 CRC
之后获得的数据)放在标志位后面,在接收端拿到整段数据后校验下确保它就是发送端发来的完整数据。
加入消息长度信息
这个一般配合上面的特殊标志一起使用,在收到头标志时,里面还可以带上消息长度,以此表明在这之后多少 byte 都是属于这个消息的。如果在这之后正好有符合长度的 byte,则取走,作为一个完整消息给应用层使用。