计算机网络-TCP黏包和拆包
- 本质上是 应用层问题, 并非TCP协议本身在传输层上的设计缺陷。
- 包的讲法,本质上是不恰当的
- 有人会把nagle协议的内容当成粘包问题,实际上是两码事
1. 为什么UDP协议没有黏包的问题
- TCP采用的方式是流式传输,没有消息保护边界,而UDP协议有自己的消息保护边界,所以不存在黏包的问题。
- 在socket网络编程中,TCP协议是面向连接的,但是UDP协议是尽最大能力去传输的。TCP传输是一对一的,发送端为了把多个包发送到目的地,采用了优化的办法(Nagle算法),将多次间隔比较小而且数据量比较小的数据,合并成为一个比较大的数据块,然后进行封包。
- 对于UDP而言,UDP支持的是一对多的模式,所以接收端的skbuff(套接字缓冲区)采用了链式结构来记录到达的每一个UDP包,在每个UDP包中就有了消息头(消息来源、端口等信息),这样,对于接收端就可以进行处理了。
保护消息边界,就是指传输协议把数据当作一条独立的消息在网上传输,接收端只能接收独立的消息。也就是说存在保护消息边界,接收端一次只能接收发送端发出的一个数据包。
而面向流则是指无保护消息保护边界的,如果发送端连续发送数据,,接收端有可能在一次接收动作中,会接收两个或者更多的数据包。
- 我们举个例子来说,例如,我们连续发送三个数据包,大小分别是2k, 4k , 8k,这三个数据包,都已经到达了接收端的网络堆栈中,如果使 用UDP协议,不管我们使用多大的接收缓冲区去接收数据,我们必须有 三次接收动作,才能够把所有的数据包接收完.而使用TCP协议,我们 只要把接收的缓冲区大小设置在14k以上,我们就能够一次把所有的数据包接收下来.只需要有一次接收动作。
- 这就是因为UDP协议的保护消息边界使得每一个消息都是独立的。而流传输,却把数据当作一串数据流,他不认为数据是一个一个的消息。
- 为啥TCP协议需要采用流式传输的模式呢,因为面向消息的传输,可以尽量减少发送包的数量,减少额外的开销,不然就要反复进行验证了。但是拆包等操作其实也会增加机器负荷,所以适用于数据传输要求比较可靠,但是不太需要太频繁传输的场合。
- 而UDP,由于面向的是消息传输,它把所有接收到的消息都挂接到缓冲区的接受队列中,因此,它对于数据的提取分离就更加方便,但是,它没有粘包机制,因此,当发送数据量较小的时候,就会发生数据包有效载荷较小的情况,也会增加多次发送的系统发送开销(系统调用,写硬件等)和接收开销。因此,应该最好设置一个比较合适的数据包的包长,来进行UDP数据的发送。(UDP最大载荷为1472,因此最好能每次传输接近这个数的数据量,这特别适合于视频,音频等大块数据的发送,同时,通过减少握手来保证流媒体的实时性)
2.黏包和拆包
- 因为TCP是面向流,没有边界,而操作系统在发送TCP数据时,会通过缓冲区来进行优化,例如缓冲区为1024个字节大小。
- 如果一次请求发送的数据量比较小,没达到缓冲区大小,TCP则会将多个请求合并为同一个请求进行发送,这就形成了粘包问题。
- 如果一次请求发送的数据量比较大,超过了缓冲区大小,TCP就会将其拆分为多次发送,这就是拆包。
- 关于粘包和拆包可以参考下图的几种情况:
3. 如何避免 todo
这里的所有改进都是应用层上面的做法,和传输层的TCP协议本身没有任何关系
(1)发送固定长度的消息(2)把消息的尺寸与消息一块发送(3)使用特殊标记来区分消息间隔
- 如果采用
包头 + 包体
的做法,如上图所示 - 但是实际上,这里所谓的
包
完全就是不恰当的说法,实际上就是应用层对传输层的流式数据进行拆解
4. TCP不是基于流的吗?这里怎么又出现了“数据包”,这个“包”具体是说什么的?
在 TCP 中,数据在网络上的传输单元被称为“段”(segment),而不是“数据包”(packet)。TCP 会将应用层的数据划分为合适大小的段,并为每个段添加一个 TCP 头部信息。这些段在 IP 层被封装成 IP 数据包,然后在网络中进行传输。
尽管 TCP 本身是基于流的,但在应用层通常会将数据分成多个数据单元,这些数据单元在传输过程中可能会粘连在一起,形成“粘包”现象。实际上,这个粘包现象并非严格意义上的 TCP 层面的问题,而是应用层处理数据时遇到的问题。
5. 通过一个例子去解读
- 比如
Alice
和Bob
两个人通过应用层的app
进行聊天 Alice
发了两条消息,你好和吃了吗?,这两条消息可能在一个TCP分段里面连续发送,发送端认为它们可以封装在一个字节流当中- 但是接收端如果没有做好约定的话,就不会找到应用层而言的消息边界,会把两句话当成一个消息进行处理
- ChatGPT提供的解决办法
- 长度前缀:在每条消息前添加一个表示消息长度的前缀,这样在接收端就可以按照这个长度值来分割消息。例如,发送端发送的消息格式为:{消息1长度} {消息1内容} {消息2长度} {消息2内容}。
- 分隔符:使用特定的分隔符来标记消息边界。发送端将分隔符添加到每条消息的末尾,并在接收端根据分隔符分割消息。例如,发送端发送的消息格式为:{消息1内容} {分隔符} {消息2内容} {分隔符}。请注意,分隔符需要是一个不会在正常消息中出现的字符或字符序列。
- 定长消息:如果所有消息具有相同的长度,那么可以预先约定一个固定的消息长度。在这种情况下,接收端可以直接按照这个长度来分割消息。