3、粘包拆包的常见场景:连续发送小数据包、发送数据大于缓冲区、接收端读取不及时
好,咱们直接进入正题。粘包拆包这个问题,说白了就是 TCP 这个“老实人”在传输时搞出来的幺蛾子。TCP 是面向流的协议,它不关心你发的是几个包,它只关心字节流。这就导致了三个最常见的翻车现场。
3.1 场景一:连续发送小数据包
这是我在项目中遇到最多的情况。客户端连续发了三次数据,比如:
- 第一次发 "Hello"
- 第二次发 "World"
- 第三次发 "!"
你猜接收端收到了什么?
大概率是 "HelloWorld!" 粘在一起了。这就是典型的粘包。
为什么会这样?因为 Nagle 算法在背后搞鬼。TCP 为了效率,会把多个小包攒一攒再发出去。你想想看,如果每个小包都单独发,光头部开销就够受的。所以 TCP 说:“兄弟,我帮你合并一下,省点带宽。”
核心问题:发送端多次 write 的数据,被 TCP 合并成一次发送。接收端一次 read 就读到了多份数据。
我个人习惯在写教学代码时,故意用 sleep 来模拟这种场景。你会发现,即使发送端每次只发 5 个字节,只要发得够快,接收端那边就是一坨。
3.2 场景二:发送数据大于缓冲区
这个场景我印象特别深。有一次我在做一个文件传输工具,发送端一次发了 64KB 的数据。结果接收端只收到了 16KB。
嗯,这里要注意。TCP 接收缓冲区是有上限的。比如你的接收缓冲区是 8KB,但你一次发了 20KB 的数据。那 TCP 会怎么做?它会分多次投递给你。
这就是拆包。
你想想看,TCP 底层是 IP 包,IP 包又有 MTU(最大传输单元)限制。一个 TCP 段如果太大,IP 层会把它拆成多个分片。到了接收端,再组装起来。但应用层 read 的时候,可能只读到一部分。
避坑指南:我曾经以为一次 send 对应一次 recv,结果线上出了大问题。后来才明白,TCP 是流,不是包。你发 100KB,接收端可能分 10 次才能读完。
来看个代码示例:
// 发送端
char buffer[65536];
send(sockfd, buffer, sizeof(buffer), 0);
// 接收端
char recv_buf[1024];
int n = recv(sockfd, recv_buf, sizeof(recv_buf), 0);
// n 可能只有 1024,而不是 65536
看到了吗?发送端一次发了 64KB,接收端一次只读了 1KB。剩下的 63KB 还在内核缓冲区里等着呢。
3.3 场景三:接收端读取不及时
这个场景其实很隐蔽。很多新手会忽略。
假设发送端发了两个包:
- 包 A:500 字节
- 包 B:500 字节
接收端这边呢?业务逻辑处理太慢,或者 CPU 被其他任务占用了。等它终于有空 read 的时候,两个包都已经到达了。结果一次 read 就读到了 1000 字节。
这算粘包还是拆包?
严格来说,这是应用层粘包。数据在 TCP 层面是分两次到达的,但应用层读取不及时,导致两次数据被合并读取。
我的经验:在写高并发服务器时,一定要考虑接收端的处理速度。如果业务逻辑耗时较长,建议用独立线程收数据,另一个线程处理数据。否则很容易出现读取不及时导致的粘包。
3.4 三种场景对比
我把这三种场景整理成了表格,方便你对比:
| 场景 | 原因 | 表现 | 常见于 |
|---|---|---|---|
| 连续发送小数据包 | Nagle 算法合并 | 多个小包粘成一个 | 聊天消息、心跳包 |
| 发送数据大于缓冲区 | TCP 分段/缓冲区限制 | 一个大包拆成多个 | 文件传输、大对象序列化 |
| 接收端读取不及时 | 应用层处理慢 | 多个包被合并读取 | 高并发服务器、慢业务逻辑 |
3.5 核心逻辑图
下面我用一张 SVG 图来展示这三种场景的核心逻辑。你看完应该能一目了然。
你看这张图,左边是连续小包粘在一起,中间是大包被拆成多段,右边是读取不及时导致合并。三种情况,一个本质:TCP 不保边界。
记住一句话:TCP 只保证数据按序到达,不保证每次读取的数据正好是你一次发送的数据。这就是粘包拆包的根本原因。
好了,这三种场景你记住了吗?下一章我会讲如何用自定义协议来解决这些问题。到时候我会给出具体的代码实现,包括我在实际项目中踩过的坑。