2. UDP协议基础回顾

很多人一听到「粘包」,第一反应就是 TCP。毕竟 TCP 是流式协议,没有边界,粘包是家常便饭。但 UDP 不是有消息边界吗?怎么也会粘包?

嗯,这个问题我当年也被问住过。记得有一次面试,面试官问我:「UDP 粘包你遇到过吗?」我当时自信满满地说「UDP 不会粘包」。结果他笑了笑,让我回去再查查资料。后来在项目中真的踩了这个坑,才明白——UDP 的粘包,本质上是应用层的「假粘包」。

2.1 先搞清楚:UDP 到底有没有边界?

我们先看一个最基础的事实。UDP 是数据报协议,每个 sendto() 对应一个独立的数据报。接收端调用 recvfrom() 时,一次读取就是一个完整的数据报。这是协议层保证的。

举个例子:

// 发送端
sendto(sock, "Hello", 5, ...);   // 数据报1
sendto(sock, "World", 5, ...);   // 数据报2

// 接收端
recvfrom(sock, buf, 1024, ...);  // 一次读到 "Hello"
recvfrom(sock, buf, 1024, ...);  // 一次读到 "World"

你看,在传输层,UDP 的边界是清晰的。每个数据报独立传输,独立接收。那为什么还会出现「粘包」?

核心结论:UDP 在传输层不存在粘包,但在应用层存在「逻辑粘包」。

说白了,就是应用层把多个数据报的内容「拼」在一起处理了,或者一个数据报被「拆」成了多次读取。

2.2 应用层粘包的三种典型场景

我在项目中遇到过三种情况,每一种都让人头疼。咱们一个一个说。

场景一:接收缓冲区没读完

UDP 的 recvfrom() 默认是阻塞的,但如果你指定了缓冲区大小小于数据报长度,会发生什么?

char buf[10];
recvfrom(sock, buf, 10, 0, ...);  // 数据报实际有 100 字节

这时候,recvfrom() 只会读取前 10 个字节,剩下的 90 个字节会被丢弃。注意,是丢弃,不是留在缓冲区里等下次读取。UDP 没有 TCP 那种「流式缓冲区」的概念。

但有些开发者会误以为「没读完的数据下次还能读」,于是写了个循环:

while (1) {
    recvfrom(sock, buf, 10, 0, ...);
    // 处理 buf...
}

结果呢?每次只读到数据报的前 10 个字节,后面的全丢了。这其实不是粘包,是数据截断。但很多人把它归类为「粘包问题」的一种表现。

场景二:多个数据报合并处理

这是最常见的「假粘包」。假设你设计了一个协议,每个数据报包含一个完整的消息。但接收端为了提高效率,把多个数据报的内容拼在一起处理:

// 错误示范:把两次 recvfrom 的结果拼在一起
recvfrom(sock, buf1, 1024, ...);  // 收到 "MSG1"
recvfrom(sock, buf2, 1024, ...);  // 收到 "MSG2"
strcat(buf1, buf2);               // 拼成 "MSG1MSG2"
parse(buf1);                      // 解析失败!

你想想看,本来两个独立的消息,硬被拼成一个字符串去解析,解析器肯定懵了。这不怪 UDP,怪应用层没处理好边界。

场景三:发送端合并发送

有些开发者会在应用层做「批量发送」,把多个小消息拼成一个大数据报发出去:

// 发送端
char packet[1024];
memcpy(packet, msg1, len1);
memcpy(packet + len1, msg2, len2);
sendto(sock, packet, len1 + len2, ...);  // 一次发送两个消息

接收端收到后,如果不知道消息边界,就会把两个消息当成一个来处理。这跟 TCP 粘包的表现一模一样。

注意:UDP 数据报最大长度是 65535 字节(含头部)。如果你合并发送的内容超过这个值,会被自动截断或分片。分片后的数据报在接收端重组,但重组失败会导致整个数据报丢失。

2.3 数据报边界 vs 流式传输:一张图看懂

为了让你更直观地理解,我画了一张对比图。左边是 UDP 的数据报模型,右边是 TCP 的流式模型。

UDP 数据报边界 vs TCP 流式传输 UDP(数据报模型) 数据报1 数据报2 数据报3 独立发送 独立接收 边界清晰 TCP(流式模型) 连续字节流:没有边界,数据像水流一样 数据A B 数据C D 数据E 应用层需要自己定义边界(如长度字段、分隔符) UDP 的「粘包」本质:应用层没有正确处理数据报边界,导致逻辑上的数据混淆

从图上可以看得很清楚:UDP 每个数据报是独立的「包裹」,TCP 是连续的「水管」。UDP 的粘包问题,说白了就是你把水管那套逻辑套在了包裹上。

2.4 为什么很多人误以为 UDP 不会粘包?

我总结了一下,主要有三个原因:

  • 教科书误导:很多教材只说「UDP 有消息边界,不会粘包」,但没说明这个「粘包」指的是传输层。应用层该粘还是粘。
  • 简单场景掩盖问题:如果你只是发几个小数据报,接收端一次读完,确实不会出问题。但一旦涉及批量处理、缓冲区复用、异步接收,问题就暴露了。
  • 混淆「丢包」和「粘包」:UDP 丢包是常态,但有些人把丢包后的数据不完整也归为粘包。这是两码事。

我的建议:不管用 TCP 还是 UDP,应用层都应该设计清晰的数据边界。这不是「要不要」的问题,而是「什么时候会踩坑」的问题。我习惯在项目一开始就定义好消息格式,哪怕只是内部通信。

2.5 一张表对比 TCP 粘包 vs UDP 粘包

对比维度 TCP 粘包 UDP 粘包
本质原因 流式协议无边界,Nagle 算法、缓冲区合并 应用层未正确处理数据报边界
传输层表现 多个数据包合并成一个 TCP 段发送 每个数据报独立传输,边界完整
应用层表现 一次 recv 读到多个消息的部分或全部 一次 recvfrom 读到完整数据报,但应用层误合并
数据丢失风险 不会丢失,数据在缓冲区排队 缓冲区过小会导致数据截断丢弃
解决方案 固定长度、长度字段、分隔符 固定长度、长度字段、分隔符(一样!)

你看,解决方案其实是一样的。不管底层协议是什么,应用层都需要自己定义边界。这就是为什么我说「UDP 粘包是应用层的假粘包」——它跟 TCP 粘包的表现相似,但根因不同。

2.6 避坑指南:我曾经踩过的三个坑

最后分享几个我亲身踩过的坑,希望能帮你省点时间。

坑一:我曾经在一个项目中,用 UDP 传输日志数据。发送端把多条日志拼成一个数据报发送,接收端用 strtok 按换行符分割。结果某天日志里出现了二进制数据,直接导致解析崩溃。后来我改成了「长度字段 + 数据」的格式,再也没出过问题。

坑二:有一次做视频传输,接收端缓冲区设成了 1400 字节,但发送端偶尔会发超过 1500 字节的数据报(含 IP 头部)。结果 MTU 分片后,部分分片丢失,接收端只读到不完整的数据。我花了三天才定位到问题——不是粘包,是分片丢失导致的「半包」。

坑三:新手最容易犯的错误:用 recvfrom 返回值判断消息边界。UDP 的 recvfrom 返回值是实际读取的字节数,但如果你缓冲区比数据报大,一次就读完了。如果你缓冲区比数据报小,一次读不完,剩下的就丢了。所以永远不要假设一次 recvfrom 能读完所有数据。