udp
05. UDP
0. 本章先解决什么问题
很多人第一次学网络时会觉得:TCP 可靠,所以 TCP 更高级;UDP 不可靠,所以 UDP 像是残缺版本。
这个理解会误导你。UDP 的价值不在于“做不到可靠”,而在于它只提供最小的传输层能力,把是否可靠、如何重传、能否丢弃、怎样控制延迟这些策略交给应用自己决定。
本章要解决三个问题:
- UDP 到底提供了什么,又明确不提供什么。
- 为什么很多场景宁愿用 UDP,也不愿直接用 TCP。
- 如果应用选择 UDP,它自己必须承担哪些设计责任。
先看 UDP 的关键边界:每次发送都是独立数据报,网络不替你保证到达、顺序和唯一。
这张图怎么读
先看边界:一次 send 对应一个 UDP datagram,接收方看到的也是一条条消息,而不是 TCP 那样的连续字节流。再看责任划分:UDP 只加端口和校验等最小传输信息,不替应用处理重传、乱序、去重、拥塞和会话状态。凡是“少一包会怎样”“乱序能不能接受”“重复消息能不能幂等”,都要回到应用协议自己设计。
1. UDP 的本质:带端口的独立数据报
IP 能把包送到某台主机,但主机上可能有很多进程。传输层至少要解决:
到达这台机器以后,应该交给哪个进程?
UDP 用端口解决这个问题:
源 IP:源端口 -> 目标 IP:目标端口
它的数据单位是 datagram,也就是数据报。一次发送通常对应一个独立数据报,接收方也是按数据报接收。
这和 TCP 的字节流不同:
| 对比 | UDP | TCP |
|---|---|---|
| 数据形态 | 一条一条数据报 | 连续字节流 |
| 消息边界 | 保留 | 不保留 |
| 连接建立 | 无需握手 | 需要握手 |
| 可靠性 | 不保证 | 保证有序可靠到达 |
| 拥塞控制 | 协议本身不强制 | 协议内置 |
| 应用自由度 | 高 | 低一些 |
2. UDP 报文头为什么很小
UDP 头部只有几个核心字段:
| 字段 | 作用 |
|---|---|
| 源端口 | 发送方进程入口 |
| 目标端口 | 接收方进程入口 |
| 长度 | UDP 头部和数据的总长度 |
| 校验和 | 检测传输中是否损坏 |
可以把 UDP 看成:
IP 提供主机到主机
UDP 加上端口,提供进程到进程
除此之外,它几乎不多管。
3. UDP 不提供哪些保证
UDP 的“不可靠”要拆开看。它不保证:
| 不保证 | 具体含义 |
|---|---|
| 到达 | 包可能在任何中间节点被丢弃 |
| 顺序 | 后发的包可能先到 |
| 唯一 | 网络异常时可能看到重复数据 |
| 完整会话 | 没有协议级连接状态 |
| 自动重传 | 丢了不会由 UDP 自己补发 |
| 流量控制 | 不知道接收方应用是否处理得过来 |
| 拥塞控制 | 不会自动根据网络拥塞降速 |
所以 UDP 应用必须接受一个事实:
代码块收起展开
发出去了,不代表对方一定收到;
收到了,不代表顺序正确;暂时没收到,不代表永远收不到。
机制深挖:无连接不等于系统里没有状态
UDP 常被说成“无连接”,这句话只是在传输层协议语义上成立:UDP 本身没有 TCP 那样的握手、序号确认、连接状态机。它不代表整条路径完全无状态。
真实系统里,状态可能出现在这些地方:
| 位置 | 可能保存什么状态 | 过期或不一致时的现象 |
|---|---|---|
| 应用协议 | 会话 token、序号、重试窗口 | 服务端认为请求过期或重复 |
| NAT 设备 | 内网地址端口到外网地址端口的映射 | 一段时间无流量后回包找不到入口 |
| 防火墙 | 最近允许的流量方向和端口 | 首包被丢、回包被挡 |
| 负载均衡 | 四元组到上游实例的映射 | 后续包打到不同实例 |
| 接收端 socket | 端口绑定和接收缓冲区 | 包到了主机但无人接收或被丢弃 |
这能解释一个非常实际的现象:
应用以为 UDP 不需要连接维护
但中间 NAT/防火墙会悄悄维护映射
映射过期后,外部回包就可能丢失
所以很多 UDP 应用仍然要做心跳或 keepalive。目的不是维持 UDP 连接,而是维持应用会话和路径中间设备的状态。
4. 为什么还要用 UDP
因为“可靠”本身也有成本。
TCP 为了可靠会重传、排序、控制窗口。对文件传输来说这很好,因为旧字节不能丢。但对实时语音、视频、游戏状态、监控采样等场景,过期数据可能已经没有价值。
例如视频会议中,某一帧丢了:
如果等待重传:
画面更完整,但延迟增加
如果直接跳过:
画面有轻微损伤,但实时性更好
很多实时系统宁愿接受少量丢包,也不愿为了补齐旧包让整体延迟升高。
UDP 的优势通常来自:
| 优势 | 解释 |
|---|---|
| 无握手 | 首包更快发出 |
| 保留消息边界 | 应用天然按包处理 |
| 策略可自定义 | 应用决定哪些包值得重传 |
| 避免字节流队头阻塞 | 一个旧包不一定拖住后续语义 |
| 适合轻量查询 | 请求小、响应小、可重试 |
5. UDP 的典型使用场景
| 场景 | 为什么适合 UDP |
|---|---|
| DNS 查询 | 请求/响应小,失败后可以重试 |
| 实时音视频 | 低延迟通常比补齐旧数据更重要 |
| 在线游戏状态同步 | 位置状态会不断刷新,旧状态价值低 |
| 监控/遥测上报 | 少量丢失可接受,持续采样更重要 |
| 局域网发现 | 广播/组播场景常用 |
| QUIC/HTTP/3 | 在 UDP 上实现自定义可靠传输和连接迁移 |
注意:使用 UDP 不等于完全不要可靠性。很多系统是在 UDP 之上自己实现一套更适合业务的可靠策略。
6. 应用如果选择 UDP,需要自己设计什么
如果你的应用不能接受乱序、丢包、重复,就要在应用层处理。
常见设计包括:
| 设计 | 解决的问题 |
|---|---|
| 序号 | 判断顺序、检测丢包、丢弃旧包 |
| ACK | 确认对方收到哪些数据 |
| 超时重发 | 丢包后补发 |
| 去重表 | 防止重复包被重复处理 |
| 心跳 | 判断对方是否还活着 |
| 滑动窗口 | 控制同时在途的数据量 |
| 速率限制 | 防止自己把网络打爆 |
| 校验/签名 | 防止错误数据或伪造数据 |
一个简化的可靠 UDP 思路:
发送方:
给每个消息编号
保存未确认消息
超时没有 ACK 就重发
接收方:
检查编号
处理新消息
丢弃重复消息
回复 ACK
这看起来像 TCP,但应用可以做不同取舍:
- 重要控制消息:必须 ACK,超时重发
- 实时位置消息:新消息来了就丢弃旧消息
- 语音帧:可接受丢失,不等待补齐
应用层协议模板:用 UDP 时至少写清 8 个字段
如果你要在 UDP 上设计自己的消息协议,不要只定义 payload。至少要考虑这些字段或语义:
| 字段/语义 | 解决什么问题 |
|---|---|
| version | 协议升级和兼容 |
| type | 区分心跳、数据、ACK、控制消息 |
| sequence | 判断顺序、丢包、重复 |
| timestamp | 判断过期、估算延迟 |
| session/token | 区分会话,避免把旧包当新包 |
| payload length | 防止解析越界或粘错数据 |
| checksum/auth | 识别损坏或伪造 |
| flags | 标记是否需要 ACK、是否可丢弃、是否分片 |
一个简化头部可以长这样:
[version | type | flags | sequence | timestamp | session | length | payload]
这不是鼓励你重复造 TCP,而是提醒:UDP 给你的自由必须用协议规则补回来。你可以选择“不重传旧位置包”,但你不能不定义“什么叫旧”。
7. UDP 和 MTU:不要随便发超大包
UDP 数据报过大时,底层 IP 可能分片。分片带来两个问题:
- 任意一个分片丢失,整个 UDP 数据报都无法重组。
- 分片增加中间网络处理成本,也更容易被设备过滤。
所以很多 UDP 应用会控制单个包大小,尽量避开路径 MTU 问题。
直觉上:
小包:
头部开销占比更高,但丢失影响小
大包:
头部开销占比更低,但分片风险和丢失代价更高
这也是为什么网络设计不能只看“吞吐”,还要看丢包、延迟、路径 MTU、应用语义。
反例:应用层切片不等于自动可靠
有人看到“大 UDP 包容易分片”,就想:那我把一条大消息拆成多个小 UDP 包不就好了?这只解决了单包过大的问题,没有自动解决可靠性。
假设一条消息被拆成 5 片:
msg=42, part=1/5
msg=42, part=2/5
msg=42, part=3/5
msg=42, part=4/5
msg=42, part=5/5
你至少还要定义:
| 问题 | 不定义会怎样 |
|---|---|
| 分片编号 | 接收方不知道顺序 |
| 总片数或结束标记 | 接收方不知道何时完整 |
| 超时丢弃 | 缺一片时内存一直占着 |
| 重复片处理 | 重传导致同一片被算多次 |
| 最大重组大小 | 恶意或异常输入耗尽内存 |
| 完整性校验 | 拼出的消息可能损坏 |
应用层切片是一个小型协议。只要你开始切片,就必须同时设计重组、超时、内存上限、去重和校验。否则你只是把 IP 分片的问题搬到了应用层,而且更难排查。
8. UDP 的安全问题
UDP 无连接,服务端在收到第一个包前通常没有确认对方身份。攻击者可能伪造源地址,诱导服务器把更大的响应发给受害者,形成放大攻击。
典型风险:
| 风险 | 解释 |
|---|---|
| 源地址伪造 | UDP 没有握手确认来源 |
| 反射放大 | 小请求触发大响应,响应打到伪造源 |
| 资源消耗 | 大量无状态请求消耗 CPU/带宽 |
| 数据伪造 | 没有额外认证时,接收方难判断包来源 |
防护方向:
限制响应大小
验证令牌或会话
做速率限制
过滤异常来源
不要对未验证请求返回过大数据
9. “连接”的 UDP 是什么意思
有些系统 API 允许对 UDP socket 调用 connect。它并不会像 TCP 那样建立握手连接,通常只是给这个 UDP socket 记录默认对端。
好处包括:
| 好处 | 解释 |
|---|---|
| 发送时不用每次指定对端 | API 更方便 |
| 过滤其他来源的数据 | 只接收默认对端 |
| 可能接收部分错误反馈 | 某些系统会把 ICMP 错误映射给该 socket |
但协议层面仍然是 UDP:
不会保证到达
不会保证顺序
不会自动重传
不会变成字节流
10. 联系实际:怎样判断一个系统该不该用 UDP
可以从四个问题判断:
| 问题 | 更偏向 TCP | 更偏向 UDP |
|---|---|---|
| 数据能不能丢 | 不能丢 | 可丢少量或可重试 |
| 旧数据是否有价值 | 有价值,必须按顺序处理 | 旧状态可能可丢弃 |
| 延迟是否极敏感 | 可接受重传等待 | 延迟比完整性更关键 |
| 应用是否愿意自管可靠性 | 不想自己实现 | 愿意定制策略 |
实际排障时,也要按 UDP 的特性看问题:
请求发出了但没响应:
可能是包丢了
可能是目标没监听
可能是防火墙丢弃
可能是响应包回不来
可能是应用没有设计重试
UDP 问题经常不会给你一个清晰的“连接失败”错误,很多时候应用只看到超时。
设计卡:选择 UDP 后必须补上的四个问题
UDP 把很多保证留给应用自己做。只要你打算用 UDP,就必须把下面四件事写清楚:
| 问题 | 如果不设计会怎样 | 常见设计 |
|---|---|---|
| 消息丢失 | 接收方永远等不到某个关键状态 | 序号、确认、重试、过期丢弃 |
| 消息乱序 | 新状态被旧状态覆盖 | 版本号、单调序号、只接受更新状态 |
| 消息重复 | 同一动作执行多次 | 幂等键、去重窗口 |
| 消息过大 | 分片丢失导致整条消息不可用 | 控制大小、应用层切片、路径 MTU 估计 |
练习:假设你要周期性发送一个传感器读数。哪些包丢了可以接受?哪些必须重发?旧读数到达时要不要覆盖新读数?这类问题回答清楚,才算真的理解 UDP 的自由和代价。
11. 学完本章你能解决什么问题
学完本章,你应该能:
- 解释 UDP 提供的是进程到进程的数据报传输,而不是可靠字节流。
- 判断某个场景为什么可能选择 UDP,而不是默认追求 TCP 的可靠性。
- 设计最基本的 UDP 应用层可靠机制,包括序号、ACK、超时、去重。
- 理解 UDP 包过大为什么会带来分片和丢失风险。
- 排查 UDP 请求无响应时,从监听、防火墙、丢包、回包路径、应用重试等方向定位。
- 识别 UDP 反射放大和源地址伪造等安全风险。