1、UDP协议基础回顾:UDP与TCP的核心区别、UDP报文结构、UDP的无连接特性
各位同学,咱们正式开始这门课之前,得先把地基打牢。UDP 粘包拆包这个话题,说白了就是跟 UDP 的数据边界较劲。如果你对 UDP 本身的理解还停留在“不可靠、快”这个层面,那后面讲实战的时候你可能会懵。所以第一章,咱们老老实实把 UDP 协议的核心概念再过一遍。
我个人习惯是,不管学什么协议,先搞清楚它“为什么存在”。UDP 的存在,就是为了解决 TCP 解决不了的问题——或者说,TCP 太“重”了。
1.1 UDP 与 TCP 的核心区别
很多教科书喜欢列一个长长的对比表,我换个方式说。你想想看,TCP 像个什么?像个快递员,必须当面签收,丢了要重发,顺序乱了要重新排好,而且一次只能送一个包裹,送完才能送下一个。UDP 呢?像个飞镖,你扔出去就不管了,对方接没接到、接到的是不是完整的,你都不关心。
嗯,这里要注意,UDP 不是“故意”不可靠,而是它选择了不维护这些复杂的状态。它把可靠性交给了上层应用。我在项目中遇到过不少新手,一听说 UDP 不可靠就觉得它没用,其实恰恰相反——实时音视频、游戏同步、DNS 查询,这些场景下 TCP 的重传机制反而是灾难。
咱们直接看对比,这样更清楚:
| 对比维度 | TCP | UDP |
|---|---|---|
| 连接状态 | 面向连接(三次握手、四次挥手) | 无连接(直接发,不需要握手) |
| 可靠性 | 可靠传输(确认、重传、序号) | 不可靠(尽力交付,不保证到达) |
| 数据边界 | 流式协议(无边界,粘包是常态) | 报文协议(有边界,但可能拆包) |
| 传输效率 | 较低(头部 20 字节,有拥塞控制) | 较高(头部 8 字节,无拥塞控制) |
| 应用场景 | 文件传输、网页浏览、邮件 | 实时通信、DNS、DHCP、游戏 |
关键点:UDP 的“无连接”和“有边界”这两个特性,直接决定了我们后面要讲的粘包拆包问题的处理方式。TCP 的粘包是因为它是流,你需要自己切分;UDP 的“粘包”其实更准确地说叫“数据边界丢失”,因为 UDP 报文本身是有边界的,但应用层如果处理不当,边界信息就会丢失。
1.2 UDP 报文结构
UDP 的报文结构非常简单,简单到让人怀疑人生。我刚开始学网络编程的时候,看到 TCP 的头部就头疼,20 个字节,一堆标志位。UDP 呢?就 8 个字节的头部,四个字段,每个字段 2 个字节。
咱们直接看结构:
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 源端口(16位) | 目的端口(16位) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 长度(16位) | 校验和(16位) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 数据部分 |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
四个字段,我一个个说:
- 源端口(2 字节):发送方的端口号。注意,这个字段是可选的,如果不需要回复,可以填 0。但实际开发中我建议你永远不要填 0,否则对方回包你都不知道往哪发。
- 目的端口(2 字节):接收方的端口号。这个必须填,不然数据包到不了目标应用。
- 长度(2 字节):整个 UDP 报文的长度,包括头部和数据。最小是 8(只有头部,没有数据),最大是 65535。但实际中受限于 MTU,很少有超过 1500 字节的 UDP 包。
- 校验和(2 字节):覆盖头部和数据部分的校验和。注意,UDP 的校验和是可选的(IPv4 下可以填 0),但 IPv6 下是强制的。我曾经在调试一个嵌入式设备时,发现对方把校验和填了 0,结果数据在传输过程中被路由器悄悄改了,排查了整整两天……从那以后,我写的所有 UDP 代码都强制计算校验和。
一个小技巧:UDP 的“长度”字段其实有点冗余,因为 IP 层已经知道整个 IP 包的长度,减去 IP 头部长度就能算出 UDP 数据报的长度。但 UDP 还是保留了这个字段,主要是为了校验——如果 IP 层传上来的数据长度和 UDP 头部里的长度不一致,说明数据被截断或篡改了,直接丢弃。
1.3 UDP 的无连接特性
这个特性是理解 UDP 粘包拆包问题的关键。什么叫无连接?说白了就是:发送方不需要跟接收方建立任何关系,直接往一个 IP:Port 扔数据就行。接收方也不需要提前“接听”,数据来了就收,没来就等着。
你想想看,这跟 TCP 完全不一样。TCP 必须经过三次握手,双方都确认对方存在了,才能开始传数据。UDP 呢?我管你在不在,我先发了再说。
这个特性带来了几个重要的后果:
- 没有拥塞控制:UDP 不会因为网络拥堵而主动降低发送速度。这既是优点(实时性好),也是缺点(容易导致网络崩溃)。
- 没有重传机制:丢了就丢了,UDP 不会帮你重新发送。应用层如果需要可靠性,得自己实现。
- 没有保序机制:后发的包可能先到,先发的包可能后到。UDP 不保证顺序。
- 数据边界保留:这一点最重要。UDP 是报文协议,每次 sendto 发送的数据,在接收端 recvfrom 一次就能完整读取。不会出现 TCP 那种“发了两次,收了一次”的粘包情况。
注意:虽然 UDP 保留了数据边界,但这并不意味着你不需要处理边界问题。恰恰相反,UDP 的“边界”是网络层的边界,不是应用层的边界。如果你的应用层协议需要传输大于 MTU 的数据,或者需要将多个 UDP 报文组合成一个完整的业务消息,你就必须自己设计数据边界。这就是我们这门课要解决的核心问题。
为了让你更直观地理解 UDP 在整个网络协议栈中的位置,以及它跟 TCP 的关系,我画了一张图:
从这张图你可以看到,UDP 和 TCP 都工作在传输层,直接承载在 IP 之上。但它们的差异从设计哲学上就分道扬镳了。UDP 选择了“轻量、快速、无状态”,TCP 选择了“可靠、有序、有状态”。没有谁对谁错,只有谁更适合你的场景。