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 图来展示这三种场景的核心逻辑。你看完应该能一目了然。

粘包拆包三种场景核心逻辑 场景一:连续小包 A B C ABC 粘在一起 场景二:大包拆包 大包(64KB) 1 2 3 场景三:读取不及时 包A 包B 一次 read 读到 AB 总结:三种场景本质都是 TCP 流式传输特性导致的 解决方案:自定义协议 + 消息边界标识(长度字段/分隔符)

你看这张图,左边是连续小包粘在一起,中间是大包被拆成多段,右边是读取不及时导致合并。三种情况,一个本质:TCP 不保边界。

记住一句话:TCP 只保证数据按序到达,不保证每次读取的数据正好是你一次发送的数据。这就是粘包拆包的根本原因。

好了,这三种场景你记住了吗?下一章我会讲如何用自定义协议来解决这些问题。到时候我会给出具体的代码实现,包括我在实际项目中踩过的坑。

可可精品资料库