关于 TCP 协议 “粘包和拆包” 的见解。

2024-8-24|2024-9-5
麦兜
麦兜
type
Post
status
Published
date
Aug 24, 2024
slug
summary
tags
network
category
学习思考
password
icon
”粘包和拆包“ 如何处理?等相关问题实际上无论是在面试还是技术论坛都是常见,最近又在学网络知识有新的看法,所以写出来记录记录。不同的人可能有不同的理解,而且本文可能也会有一些荒谬之言甚至错误,还希望得到您的反馈和批评。

“粘包和拆包”

关于“粘包和拆包” 相关的术语我找很多相关权威书籍,终于在一本《 Netty 权威指南(第2 版)》 的第4章 ”TCP粘包/拆包问题的解决之道“ 找到的。但是很多英文书籍及文章都没有提到过,也可能是我不认识 ”粘包和拆包“ 的单词。
 
《 Netty 权威指南(第2版)》 第4章是这么介绍 TCP stream (流)以及“粘包和拆包”。
TCP是个”流”协议,所谓流,就是没有界限的一串数据。大家可以想想河里的流水,它们是连成一片的,其间并没有分界线。TCP底层并不了解上层业务数据的具体含义,它会根据 TCP 缓冲区的实际情况进行包的划分,所以在业务上认为,一个完整的包可能会被 TCP 拆分成多个包进行发送,也有可能把多个小的包封装成一个大的数据包发送,这就是所谓的TCP粘包和拆包问题
对比另一本书籍 《TCP/IP详解 卷1:协议》 第17章介绍 TCP stream (流)。
应用数据被分割成 TCP 认为最适合发送的数据块。这和 UDP 完全不同,应用程序产生的数据报长度将保持不变。由 TCP 传递给 IP 的信息单位称为报文段或段(segment)。
两个应用程序通过 TCP 连接交换 8bit 字节构成的字节流。TCP 不在字节流中插入记录标识符。我们将这称为字节流服务(byte stream service)。
如果一方的应用程序先传10字节,又传20字节,再传50字节,连接的另一方将无法了解发方每次发送了多少字节。收方可以分4次接收这80个字节,每次接收20字节。一端将字节流放到TCP 连接上,同样的字节流将出现在TCP连接的另一端。
另外,TCP 对字节流的内容不作任何解释。TCP 不知道传输的数据字节流是二进制数据,还是 ASCII字符、EBCDIC 字符或者其他类型数据。对字节流的解释由 TCP 连接双方的应用层解释。

TCP 流数据与文件流数据的相似性

对比之后我大致得知,“拆包”也就是 segment 因为 MSS(Maximum Segment Size)报文大小的限制进行切割分段,而“粘包”就是应用数据过于小 MSS ,把多个应用数据和并到一个 segment。实际上 stream 这种数据传输的概念不只 TCP 使用,经常操作系统文件可以看到文件的二进制读取也是常采用 stream 。
 
这种对字节流的处理方式与 Unix 操作系统对文件的处理方式很相似。Unix 的内核对一个应用读或写的内容不作任何解释,而是交给应用程序处理。对Unix 的内核来说,它无法区分一个二进制文件与一个文本文件。
——来源《TCP/IP详解 卷1:协议》 第17章 TCP:传输控制协议
 
为什么使用 stream ? stream 实际上是一种 Buffer (缓冲) ,读取磁盘文件需要必须经过内存(因为内存比磁盘快,如果直接操作磁盘会让 CPU 一直处在等待状态),如果让我们自己处理这个 Buffer 我们还要考虑回收、大小、I\O读取等问题,而 stream 这种抽象设计可以让我们无需关心这些问题,只需要读完字节序即可。
熟悉 Java 的 Class(编译完成的二进制文件)的朋友都知道字节序在二进制文件是如何存储的。
package cn.error0.fund; public class Hello { String value="this is value"; public static void main(String[] args) { } }
notion image
(图1)固定前4个字节 CA FE BA BE 为 魔数(magic) 用于辨识 Class 文件专属,接下来就是各种版本号、常量池数量以及常量池的引用。(请耐心的等我解释完这一切)
 
在我的 Java 文件中有一个变量为 value 里面可以指定存放一个动态长度的字符串值,在(图2) Class Viewer 直接到常量池找到对应引用。可以看到第一个字节 01 是标识这个字符串类型,第2-3字节表示字符串的长度 00 0D (13) ,接下来13个字节就是具体的字符串。(请耐心的等我解释完这一切)
我想表达的是如果用 stream(流)读取的时候操作系统并不知道这个二进制序列表达是什么结构,具体是什么结构需要应用进行正确的解析。stream 只负责从头读到尾直至读取完毕,所以在存储到二进制文件时候就需要用标识来维护那些字节序是什么结构。 (请耐心的等我解释完这一切)
notion image
 
从磁盘到内存的复制是按 block(块)为最小单位读取,一个块为 4096字节(4KB),如果 Class 没有这个固定前4个字节为魔数的解释,那么这算不算所谓的“粘包”,因为我只想要4个字节但是别的二进制序列却一同合并到一个 block,啊不!这应该叫 “粘块”,但是实际上从来都不会有这个说法。
 
这些二进制序完全由你如何解析,如果你想正确解析就需要给定特殊标识,比如“this is value” 这个字符串的字节序操作系统完全是不知道这是一段文本字符串,所以需要在字符串二进制字节序前面加入长度标识,并且固定这个两个字节就是长度,在解析二进制字节序才能根据长度读取所需要的多少个字节为完整字符串,这和处理 TCP 的 stream(流)是一模一样的。

如何处理 stream 式数据

下面可以来处理字节序一个简单样例,定义一个数据结构,一个字节表示长度,一个为 length 数量字节序为内容。
{ byte length byte[length] value }
在 TCP 传输中我想传输两个数据结构,接收方还要正确的解析。
05 01 02 03 04 05 03 01 02 03
TCP 并不知道你的二进制序是表达什么内容所以不会按你认为的正确的长度字节序传输,接收方可能接收的是分为两个 Segment(段) 05 01 02 03 04 05 03 01 02 03
或者合并一个 Segment (段) 05 01 02 03 04 05 03 01 02 03 。因为我们在字节序加入特定标识 length 我们可以知道接下来字节序表达什么含义。
 
对于05 01 02 03 04 05 03 01 02 03 的第一个 Segment 05 01 02 03 04 05 03 可以根据第一个字节知道长度然后读取接下来的5个字节序就为一个完整数据结构接下来就是另一个数据结构的数据了,现在第一个Segment 还剩 03 字节表示第二个数据结构长度,只需要等待第二个 Segment 到来就可以继续完整解析。
处理 stream 方式还有很多,比如遇到标识符 \n 就为一个完整数据结构等等。

最后

TCP “粘包和拆包” 这个说法像是 TCP 的“设计错误” ,实际上不是,更像是没有认真读文档的程序员造出的名词。
然后还把这个的概念传遍了整个中文互联网,在八股文面试题竟然还经常出现真是让人哭笑不得🤣。

更多对 TCP stream (流)处理的介绍。

来自 Netty4 guide 文档

notion image
 

来自知乎段子

看到“TCP粘包”这个专有名词,我表示极度震惊。
连夜打车回到家里,战战兢兢翻开《计算机网络》,拿着放大镜仔细看了半夜,也没看到“粘包”两个字。我的后背不觉地渗出致密的汗水,双手止不住地发抖。匆忙打开电脑,一篇篇地翻着论文,试图寻找关于这个词的信息。可眼看天就要亮了,我依旧一无所获。
我失望的躺在床上,满脑子都是“粘包,粘包,粘包!”,横竖睡不着,不得已打开了知乎,写下了一个问题“究竟什么是“TCP粘包”。不一会儿答案就如雪花儿般涌了出来,每一片雪花上都写着一句话“TCP没有粘包”。
我颤抖的双手终于停了下来,一股热流从我心底涌到泪腺。啊,原来我并不孤独。
链接:https://www.zhihu.com/question/20210025/answer/1098672130
InnoDB 的文件存储格式学习的方法