你这段内容讲的是:TCP 传输时不会帮你区分"一个完整业务消息"的边界,所以应用层要自己设计规则来拆分消息。
可以按下面这条主线理解:
TCP 只管可靠传输字节,不管你一次发的是几条消息。
1. 为什么会有粘包和拆包?
假设客户端连续发送两条消息:
text
消息1:hello
消息2:world
你以为服务端一定会收到:
text
hello
world
但 TCP 是字节流,服务端实际可能收到:
text
helloworld
这就是粘包。
也可能收到:
text
hel
lowor
ld
这就是拆包。
注意:这里的"包"不是 TCP 真正意义上的包,而是我们应用层自己认为的一条完整消息。
2. 粘包是什么?
粘包:多个应用层消息被一次性读到了。
比如发送方发了两条消息:
text
[消息1][消息2]
接收方一次 read() 读到:
text
[消息1消息2]
接收方如果不知道消息边界,就不知道哪里是第一条结束,哪里是第二条开始。
粘包可能来自两个地方:
发送方导致
发送方为了提高效率,可能会把多个小数据合并后一起发出去,减少网络传输次数。
比如:
text
send("hello")
send("world")
底层可能合并成一次发送:
text
helloworld
接收方导致
即使发送方分两次发,接收方也可能一次从 TCP 缓冲区里读出多条数据。
比如缓冲区里已经有:
text
hello world
接收方一次读取 1024 字节,就可能把两条消息都读出来。
3. 拆包是什么?
拆包:一条应用层消息被拆成多次接收。
比如发送方发送一条大消息:
text
[很长很长的一条消息]
由于 TCP 分段、MSS 限制、缓冲区大小等原因,接收方可能分多次读到:
text
第1次:[消息前半部分]
第2次:[消息后半部分]
这时候接收方不能拿到一半就解析,否则会解析失败。
4. 根本原因是什么?
根本原因不是 TCP 出错,而是:
TCP 是面向字节流的协议,没有消息边界。
TCP 看见的是一串连续字节:
text
010101010101010101...
它不关心你应用层原本是:
text
消息1 + 消息2 + 消息3
所以 TCP 只保证:
- 数据可靠到达;
- 数据按顺序到达;
- 数据不重复、不丢失。
但它不保证:
text
你 send 几次,对方 recv 就收到几次
这一点非常重要。
5. UDP 为什么没有粘包问题?
因为 UDP 是面向数据报的。
你发送一次 UDP 数据报:
text
sendto("hello")
接收方收到的就是这一整个数据报:
text
hello
UDP 保留消息边界。
所以 UDP 的特点是:
text
一次发送,对应一次接收
但 UDP 不保证可靠性,可能丢包、乱序。
TCP 的特点是:
text
可靠、有序,但没有消息边界
6. 怎么解决 TCP 粘包和拆包?
核心思想就是:
应用层自己定义消息边界。
常见方案有三种:定长、分隔符、长度字段/TLV。
方案一:定长消息
约定每条消息固定长度,比如每条都是 1024 字节。
发送方发送:
text
hello
长度不够 1024,就补齐:
text
hello + 填充字符
接收方每次固定读取 1024 字节,就知道这是一条完整消息。
优点:实现简单。
缺点:浪费空间,不灵活。
比如你只发 5 个字节,也要占 1024 字节。
方案二:分隔符
给每条消息后面加一个特殊分隔符,比如:
text
hello\n
world\n
接收方按 \n 切分:
text
hello
world
优点:简单,适合文本协议。
缺点:如果消息内容本身也包含分隔符,就要做转义处理。
比如你用 \n 做分隔符,但正文中本身就有换行,就容易出问题。
FTP、Redis 协议中都有类似思想。
方案三:长度字段 / TLV
这是最常用、最可靠的一种方式。
你图里的结构就是 TLV:
text
Type | Length | Value
含义是:
text
Type:这条消息是什么类型
Length:Value 有多长
Value:真正的数据内容
比如:
text
[T1][L1][V1][T2][L2][V2]
接收方读取时:
- 先读 Type,知道消息类型;
- 再读 Length,知道消息体有多长;
- 然后根据 Length 读取指定长度的 Value;
- 读完后,就知道一条完整消息结束了;
- 继续读取下一条消息。
所以即使 TCP 把两条消息粘在一起:
text
[T1][L1][V1][T2][L2][V2]
接收方也能根据 Length 正确切出来:
text
第一条:[T1][L1][V1]
第二条:[T2][L2][V2]
这就是图中想表达的意思。
7. Content-Length 为什么也算一种解决方案?
HTTP/1.1 中请求头或响应头里有:
text
Content-Length: 100
意思是:
text
后面的请求体/响应体长度是 100 字节
接收方看到这个长度后,就知道应该继续读取 100 字节作为 body。
这本质上就是:
text
Length + Value
只不过 HTTP 的 Type 信息在请求行、状态行、Header 里面。
8. Protobuf 和 WebSocket 怎么解决?
Protobuf
Protobuf 本身主要负责数据序列化,它会把对象编码成二进制数据。
但在 TCP 传输时,通常还需要在 Protobuf 数据前面加一个长度字段:
text
Length + ProtobufData
接收方先读长度,再读对应长度的 Protobuf 数据。
WebSocket
WebSocket 自己定义了帧格式,帧头里包含长度信息,所以接收方知道一帧数据的边界。
所以 WebSocket 底层虽然也是基于 TCP,但它在应用层协议中解决了消息边界问题。
9. 面试里可以这样回答
TCP 是面向字节流的协议,它只保证数据可靠、有序地传输,但不保证应用层消息边界。所以发送方多次写入的数据,接收方可能一次读到,形成粘包;发送方一次写入的大数据,接收方也可能多次读到,形成拆包。
本质原因不是 TCP 的缺陷,而是应用层没有定义清楚消息边界。解决方式一般有三种:定长消息、分隔符、长度字段。实际开发中最常用的是长度字段方案,比如 TLV、HTTP 的 Content-Length、WebSocket 帧格式、Protobuf 前置长度等。
可以简单记成:
text
TCP 只管字节流,不管消息边界;
应用层必须自己定义边界。