Skip to content

UDP 与应用层可靠性设计 ​

内容摘要:用户数据报协议(User Datagram Protocol,UDP)只提供端口复用、报文长度与校验和,不建立连接,也不保证送达、顺序和唯一性。它的价值是保留消息边界、状态少、控制权高,而不是“天然更快”。本章从 8 字节报文头开始,讲清 UDP socket 语义、MTU 与分片、适用边界,以及在 UDP 上实现请求编号、确认、重传、去重、流控和拥塞控制时必须承担的工程责任。

难度:中级 | 前置知识:04 套接字、09 MTU 与 PMTUD | 所属层次:传输层

一、UDP 提供什么,不提供什么 ​

RFC 768 将 UDP 定义为最小化的、面向数据报的传输协议:

能力UDP 是否提供说明
端口复用是用源/目的端口把数据交给对应进程
消息边界是一次发送对应一个数据报;接收方不会把两个数据报自动拼成一个
校验和是覆盖伪首部、UDP 头和数据;IPv4 可特殊置零,IPv6 通常必须计算
建连握手否sendto() 前无需三次握手
可靠送达否可能丢失
保序否可能乱序
去重否可能重复
流量控制否不知道接收应用是否处理得过来
拥塞控制否应用必须按 RFC 8085 自行控制发送速率

“无连接”不等于“无状态”:协议本身不握手、不维护连接状态,但操作系统仍维护 socket 缓冲区,NAT/防火墙通常仍为五元组建立带超时的连接跟踪状态(19、防火墙与访问控制)。

二、UDP 报文结构 ​

UDP 首部固定 8 字节:

字段位宽取值示例语义生成方接收方处理抓包显示常见异常
源端口16 位53000回包目标;RFC 768 允许无意义时为 0发送端内核/应用参与 socket 解复用udp.srcportNAT 改写
目的端口16 位53目标服务应用指定查找绑定 socketudp.dstport无监听时可能回 ICMP Port Unreachable
长度16 位20UDP 头 + 数据总长度,最小 8发送端校验边界udp.length截断、与 IP 长度不一致
校验和16 位0x8a31覆盖伪首部、UDP 头与数据发送端校验失败丢弃udp.checksum本机抓包受卸载影响(04)

伪首部把源/目的 IP、协议号和 UDP 长度纳入计算,用于检测“报文被错误交付到错误地址”等问题,但伪首部不真正在线上传输。

IPv4 与 IPv6 校验和差异 ​

  • UDP over IPv4:校验和字段为全 0 表示发送端未计算;新应用不应依赖这一例外。
  • UDP over IPv6:校验和通常是强制的;仅少数受严格约束的隧道场景存在例外规范。
  • 计算结果为 0 时,在线上编码为全 1,避免与“未计算”混淆。

三、数据报语义:消息边界被保留 ​

UDP 接收的是一条条独立消息,而不是 TCP 那样的连续字节流(13)。

关键语义:

  • 一个数据报要么作为一条消息交付,要么丢失;UDP 不会把半个数据报作为下一次消息继续返回。
  • 接收缓冲区太小时,数据报会被截断,剩余部分通常被丢弃;不同 API 可用 MSG_TRUNC 感知原长度。
  • 0 字节 UDP 数据报合法。recvfrom() 返回 b'' 不能像 TCP 那样解释为“对端关闭”。
  • 多个数据报可以来自不同源,recvfrom() 必须同时读取源地址。

四、“连接 UDP”到底连接了什么 ​

UDP socket 可以调用 connect(),但这不会产生网络握手:

python
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.connect(("192.0.2.53", 5300))
sock.send(b"query")
data = sock.recv(2048)

内核只是在本地:

  1. 记录默认目的地址,允许使用 send()/recv()。
  2. 根据路由选择本地源地址和端口。
  3. 只把匹配该对端的数据报交给此 socket。
  4. 某些系统可把异步 ICMP 错误反馈给应用。

它不证明对端存在,也不建立 TCP 式可靠状态。第一次 send() 成功通常只表示数据已进入本机发送路径。

五、大小、MTU 与分片 ​

RFC 8085 给出的普通 UDP 理论上限:

  • IPv4 最大 UDP 数据:65,535 - 20(IP) - 8(UDP) = 65,507 字节。
  • IPv6 普通包最大 UDP 数据:65,535 - 8(UDP) = 65,527 字节,不含 IPv6 固定头;Jumbogram 另有规范。

理论可发送不代表工程上应该发送。以太网 MTU 1500 时:

text
IPv4 UDP 无分片载荷上限 = 1500 - 20 - 8 = 1472 字节
IPv6 UDP 无扩展头载荷上限 = 1500 - 40 - 8 = 1452 字节

分片会放大丢包:一个数据报分为 n 片,只要任意一片丢失,整个数据报就不可用。应用应:

  • 优先限制消息大小,避免 IP 分片。
  • 实施 PMTUD/PLPMTUD(09)。
  • 需要传大对象时,在应用层分块并给每块编号、校验和与重组上限。
  • 不要把“局域网 MTU 1500”当成端到端路径 MTU,VPN/Overlay 会进一步吃掉空间(VPN 与隧道)。

六、在 UDP 上实现可靠性 ​

“可靠 UDP”不是加一个重试循环,而是重新设计一套传输语义。

1、最小机制清单 ​

机制解决的问题设计要点
请求/消息 ID重复与响应关联在幂等窗口内唯一
序列号乱序、缺口考虑回绕与比较规则
ACK确认连续已收数据累积 ACK 信息少但开销低
SACK/位图多处丢失只重传缺失块
重传定时器ACK 永远不来基于 RTT 动态估计,不用固定常量
指数退避对端故障/拥塞时避免重试风暴加随机抖动、总时限和次数上限
去重缓存ACK 丢失导致发送端重试缓存消息 ID 与处理结果
流控接收方处理不过来限制在途请求/发送窗口
拥塞控制网络处理不过来必须对丢包、ECN、RTT 上升降速
完整性与认证伪造/篡改AEAD、DTLS 或经过审计的协议

2、幂等与“恰好一次”陷阱 ​

假设客户端发送“扣款 100 元”,服务端执行成功,但响应丢失。客户端重试:

  • 没有请求 ID:可能重复扣款。
  • 有请求 ID + 服务端原子去重:可返回第一次结果。
  • 去重记录过期后重试:仍可能重复。

因此网络层无法凭空提供真正的“恰好一次”。工程上通常实现:

  • 至少一次(at-least-once)+ 幂等处理;或
  • 事务性消息表、唯一键、状态机等业务约束。

去重键必须与业务操作绑定,并规定保留期限、并发竞争和失败恢复策略。

3、不要手写一个劣化版 TCP ​

若需求同时包含可靠、有序、大流量、公网公平性与加密,优先使用成熟协议:

  • TCP:可靠有序字节流(13)。
  • QUIC:基于 UDP,实现加密、可靠流、多路复用与拥塞控制(16)。
  • RTP/RTCP:实时媒体,允许时效优先于补齐旧包。
  • DNS:小型请求/响应;响应过大或需要可靠性时可回退 TCP。

只有在“应用确实需要自定义时效、消息语义或多路传输策略”时,才在 UDP 上承担上述复杂度。

七、拥塞控制不是可选增强 ​

RFC 8085 明确要求互联网 UDP 应用实施拥塞控制,避免拥塞崩溃并与其他流量公平共享。

错误做法:

python
while True:
    sock.sendto(packet, destination)  # 按网卡线速无限发送

可行策略取决于业务:

  • 低频事务:限制每目的速率;超时采用指数退避与抖动。
  • 批量传输:按 RTT、丢包或 ECN 反馈调整窗口/速率。
  • 实时媒体:根据接收报告动态降码率、丢弃过期帧,而非无限重传。
  • 多 worker/多 socket:对发往同一目的的总流量统一控制,不能各自认为流量很小。

UDP 本身不慢启动,不意味着应用可以忽略慢启动式容量探测。未知公网路径不能假定带宽、RTT、丢包和可用 MTU固定不变。

八、适用场景与选型 ​

场景为什么适合/不适合 UDP典型协议
短小查询无握手,单次消息语义清晰DNS、NTP
实时音视频过期数据不值得重传,应用控制时效RTP/RTCP
现代多流传输UDP 作为可部署底座,上层完整实现可靠与拥塞控制QUIC/HTTP/3
局域网发现支持广播/组播mDNS、服务发现
文件/数据库复制通常需要可靠、有序、大流量优先 TCP/QUIC,勿裸 UDP
金融指令重复与丢失代价高需成熟可靠协议 + 业务幂等

九、最小可复现实验:验证消息边界 ​

1、环境与命令 ​

本机 macOS 26.6.2、Python 3;仅使用回环地址,不访问外网。

bash
python3 - <<'PY'
import socket

server = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
server.bind(("127.0.0.1", 0))
client = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

for payload in (b"alpha", b"beta-gamma", b""):
    client.sendto(payload, server.getsockname())

client_port = client.getsockname()[1]
for i in range(3):
    data, peer = server.recvfrom(65535)
    print(
        f"datagram[{i}] len={len(data)} payload={data!r} "
        f"source_port_stable={peer[1] == client_port}"
    )

client.close()
server.close()
PY

2、本机真实输出 ​

text
datagram[0] len=5 payload=b'alpha' source_port_stable=True
datagram[1] len=10 payload=b'beta-gamma' source_port_stable=True
datagram[2] len=0 payload=b'' source_port_stable=True

3、结论 ​

  • 三次 sendto() 对应三次 recvfrom(),5 字节与 10 字节消息没有粘连。
  • 第三个 0 字节数据报被正常接收;它不是连接关闭信号。
  • source_port_stable=True 只说明本次测试由同一客户端 socket 发出,不代表 UDP 已建立连接。

该实验在无丢包的回环链路上运行,不能证明 UDP 可靠或有序。

十、Linux 丢包、乱序与限速实验 ​

以下命令适用于 20 统一实验拓扑,当前 macOS 未执行:

bash
# [未在当前环境执行] 在 router 注入 5% 丢包、20ms 延迟、25% 乱序
sudo ip netns exec rtr tc qdisc replace dev veth-rs root netem \
  delay 20ms 5ms loss 5% reorder 25% 50%

# server 接收 UDP,client 发送带序列号的数据
sudo ip netns exec srv socat -v UDP4-RECVFROM:9000,fork -
for i in $(seq 1 30); do
  printf 'seq=%03d\n' "$i"
done | sudo ip netns exec cli socat - UDP4-DATAGRAM:198.51.100.2:9000

# 验证后清理
sudo ip netns exec rtr tc qdisc del dev veth-rs root

预期:服务端看到序列号缺失、乱序;UDP 不自动补齐。准确结果由随机故障注入决定,不能预写固定丢包序列。

十一、安全风险与加固 ​

1、源地址伪造与反射放大 ​

UDP 无握手,攻击者可伪造受害者源 IP,向公开服务发送小请求,让服务把大响应发给受害者。

加固:

  • 网络边界实施 BCP 38 源地址验证。
  • 响应大小不超过未经验证请求;采用 cookie/token 验证回程可达。
  • 每源、每目的、全局速率限制;异常流量自动降级。
  • 不把管理服务绑定到公网;关闭无业务用途的广播响应。
  • 解析输入时限制长度、数量、递归深度和重组内存。

2、UDP 校验和不提供安全性 ​

校验和只能检测偶然损坏,不能抵御攻击者伪造。敏感数据使用:

  • DTLS(Datagram Transport Layer Security);或
  • QUIC 的内建 TLS 1.3;或
  • 经过审计的消息级 AEAD(Authenticated Encryption with Associated Data)。

十二、常见误区与排查 ​

误区/现象正确认知与排查
“UDP 一定比 TCP 快”只省去协议内建机制;应用重做可靠与拥塞控制后未必更快
sendto() 成功等于对端收到只表示本机接受发送请求
0 字节 recvfrom() 等于关闭0 字节 UDP 数据报合法
局域网无丢包即可不上拥塞控制路径与负载会变化,公网更不能假定
单个 60KB 数据报能发就适合生产极易分片,一片丢失整报文失败
丢包只查网络接收 socket 缓冲区满、应用读取慢也会丢

排查顺序:

  1. ss -uapn/netstat -an -p udp 确认绑定地址与端口。
  2. 双端抓包,区分“未发出、网络丢、已到内核、应用未读”。
  3. 查看 UDP 接收错误与缓冲区丢包计数(Linux netstat -su、/proc/net/snmp)。
  4. 检查 MTU、分片、NAT 会话超时和防火墙。
  5. 检查应用序列号、重试、去重和速率控制指标。

十三、版本与平台差异 ​

  • UDP 基础规范为 RFC 768;互联网应用使用指南以 RFC 8085(BCP 145)为准。
  • Linux 可用 IP_RECVERR/错误队列获得异步网络错误;不同系统对 connected UDP 的 ICMP 错误暴露不同。
  • socket 缓冲区参数、最大值翻倍记账规则和丢包计数名称存在系统差异。
  • IPv6 UDP 校验和例外只适用于特定隧道规范,普通应用不得泛化。

十四、关联章节 ​

检索关键词 ​

UDP 报文头 8 字节 UDP 数据报消息边界 connected UDP UDP 0 字节数据报 UDP 校验和 IPv4 IPv6 UDP MTU 分片 可靠 UDP 序列号 ACK SACK 应用层重传 去重 幂等 UDP 拥塞控制 RFC 8085 UDP 反射放大 BCP 38

权威来源 ​

UDP 报文与使用边界已于 2026-09-07 对照 RFC Editor 正文核验。消息边界实验已在 macOS 26.6.2 本机真实执行;tc netem 故障注入因当前环境不是 Linux 而明确标记为未执行。

ICP 备案号待补充 · Gitee 主页