UDP
UDP 是 TCP 的"反面"——不保证到达、不保证顺序、不建立连接,但快得飞起。视频通话、直播、在线游戏、语音聊天,这些"要快不要全"的场景,全是 UDP 的地盘。
TCP 像打电话——先确认对方在,再说事,说完确认对方听清了。
UDP 像大喇叭广播——你站在村口喊一嗓子"开会了!",听到的人来,没听到的就算了。
你不确认每个人是否听到,也不重喊——快,但可能有人没听到。
视频通话就是这样——画面偶尔卡一下、花一下,没关系,重要的是"现在"的画面能实时传过去。
先把这一篇的术语翻译成人话
UDP 这一篇的核心概念其实特别少,但名字都挺唬人。先把它们翻译成人话过一遍:
| 术语 | 换成大白话 | 生活里对应的东西 |
|---|---|---|
| 无连接(Connectionless,发之前不打招呼、不确认对方在) | 说白了就是"我不管你在不在,我先喊了" | 往邮局邮筒里塞明信片——不用先确认对方家里有人 |
| 数据报(Datagram,一个包就是一条完整的消息,边界清清楚楚) | 说白了就是"一张纸条就是一句话,不会跟别的纸条粘在一起" | 一张明信片就是一条消息,绝不会跟隔壁那张拼成一句 |
| 校验和(Checksum,一段用来验"内容有没有被改坏"的数字) | 说白了就是信封上的防拆封条 | 快递箱上那道一撕就毁的胶带 |
| 分片(Fragmentation,一个包太大装不下,被拆成几个小包送) | 说白了就是一件大货被拆成几箱寄 | 搬装修材料时把一整卷地板切成几段装车 |
| MTU(Maximum Transmission Unit,最大传输单元——一条线路上一个包最多能装多少字节) | 说白了就是"一个纸箱最大能装多少东西" | 快递公司规定的单箱尺寸上限 |
| 抖动(Jitter,包到达的间隔时快时慢) | 说白了就是"来的节奏不稳" | 公交车不按点:有时三分钟来两辆,有时二十分钟不来一辆 |
| 广播 / 多播(一份数据同时发给很多人) | 说白了就是"喊一嗓子,谁听见谁来" | 学校大喇叭通知"三年级去操场集合" |
| NAT(Network Address Translation,网络地址转换——把家里的内网地址映射成一个公网地址) | 说白了就是"整栋楼共用一个大门牌,进来的信靠前台分发" | 公司只有一个总机号,门牌上写"XX 大厦",具体谁的信靠前台转交 |
这一整篇可以用一个画面串起来:TCP 是"给每个人单独打电话确认到位",UDP 是"站在楼下喊一嗓子"。喊一嗓子当然快,也当然可能有人没听见——而这一篇要讲的就是:什么时候"没听见"其实压根不重要,什么时候你得自己另想办法补上。
"无连接"到底是什么意思
UDP 定义在 RFC 768——这份规范只有 三页纸,是整个 TCP/IP 协议族里最短的核心文档之一(相比之下 TCP 的 RFC 793 有 91 页)。作者 David Reed 在 1980 年 8 月写完它,四十多年来一个字都没改过。这三页纸和九十一页纸的差距,就是 UDP 和 TCP 全部区别的最好概括。
"无连接"这个词需要拆开理解,它同时意味着三件具体的事:
- 发送前不需要任何准备你调
sendto(),内核加上 8 字节头部就直接扔进 IP 层发出去了。不握手、不协商、不确认对方存在。对方的机器可能已经关机了,你也照样发得出去,还不会报任何错。 - 两端都不维护"连接状态"TCP 要为每条连接存序号、窗口、缓冲区、定时器(Linux 里一个 socket 结构体约几 KB)。UDP 什么都不存——它甚至不知道"上一个包"是谁发的。这就是为什么一台 DNS 服务器能同时应付几十万个客户端,而同等配置的 TCP 服务器早就内存耗尽了。
- 每个数据报都是完全独立的发出去的每一个 UDP 数据报都是一次独立事件,彼此之间没有任何关联。这带来一个 TCP 没有的好性质:UDP 保留消息边界——你
sendto()一次 100 字节,对方就recvfrom()到恰好一个 100 字节的包,绝不会粘、绝不会拆。
最后那一条经常被忽略,但它是 UDP 一个实实在在的优点。TCP 的"粘包"问题让每个用 TCP 的应用协议都必须自己设计消息边界(加长度前缀或分隔符),而 UDP 天生就是"一个包一条消息"。所以像 DNS 这种"一问一答、每条消息独立"的协议,用 UDP 反而更自然、代码更简单。
还有一个容易被误解的点:UDP 也可以调 connect()。但这个 connect 跟 TCP 的完全不是一回事——它不发任何网络包,只是在内核里记下"这个 socket 以后默认发给某个地址",好处是之后可以用 send() 而不用每次都写目标地址,而且能收到 ICMP 端口不可达的错误通知。这是一个纯本地操作,网络上什么都没发生。
这个"假 connect"好比你在手机通讯录里给某个号码存了个名字。存名字这个动作,对方那边一点感觉都没有——没打过去、没响过铃、网络上什么都没发生。它带来的唯一好处是:以后你不用每次都拨十一位数字,直接点名字就行了。UDP 的 connect() 干的就是这件事,纯粹是给自己省事,跟对方无关。
八字节头部:极简主义的极致
UDP 头部固定 8 字节,只有四个字段。把它和 TCP 的 20 字节头部并排看,那种"减法的美学"扑面而来:
打个比方:TCP 的头部是一张填得满满的快递面单——寄件人、收件人、第几箱、共几箱、易碎标记、签收要求,一样不少。UDP 的头部只有四栏:从哪来、到哪去、多重、封条。它更像一张明信片背面:地址写上、字写上,寄出去,就这样。没有"第几张"、没有"要签收"、没有"送到打电话"——什么都没有。三页纸的规范和九十一页纸的规范,差距全在这八个字节和二十个字节里。
再说说那个"三页纸"的意义。换成大白话:UDP 的全部说明书,还没有一份洗衣机的使用手册厚——而且四十多年来一个字都没改过。为什么改不动?因为它已经简单到没什么可改的了。这一点反而成了它最大的优势:后面要讲的 QUIC 之所以能藏在 UDP 里满世界跑,正因为它足够朴素,全世界的路由器和防火墙都懂它、都放它过。
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 位) |
+-------------------------------+-------------------------------+
| 数据 ... |
就这四个字段。没有序号、没有确认号、没有窗口、
没有标志位、没有选项、没有紧急指针 —— 全都没有。
| 字段 | 说明 | 值得注意的细节 |
|---|---|---|
| 源端口 | 发送方端口 | 可以填 0,表示"我不需要回复"(真的有协议这么用) |
| 目的端口 | 接收方端口 | 接收方靠它决定交给哪个进程 |
| 长度 | 头部 + 数据的总字节数 | 最小 8(无数据),理论最大 65535。这个字段其实是冗余的——IP 头里已经有总长度了,减去 IP 头就能算出来 |
| 校验和 | 覆盖伪首部 + 头部 + 数据 | IPv4 下是可选的(填 0 表示不校验);IPv6 下强制必填,因为 IPv6 头部自己取消了校验和 |
头部差 12 字节,听起来不多,但在小包场景下影响巨大。算一笔账:一个网游位置同步包只有 20 字节数据。用 UDP 是 20 + 8 = 28 字节载荷,用 TCP 是 20 + 20 = 40 字节——传输量差了 43%。对于每秒要发 30~60 个包、同时在线百万玩家的游戏服务器,这个差异直接换算成带宽账单。
那个"校验和在 IPv4 下可选"的设计今天看来是个错误。UDP 是唯一一个允许"不校验数据完整性"的常用协议——因为链路层的 CRC 只保护单跳,路由器内存里的位翻转、NAT 改写时的 bug 都无法被发现。所以 RFC 1122 明确建议"实现应该默认开启校验和",现代系统也都默认开着。
UDP 的最大数据长度也值得算清楚:
那个"分片"的账翻译成人话特别好懂。MTU(Maximum Transmission Unit,最大传输单元——一个数据包最多能装多少字节,好比一个快递纸箱最大能装多少东西)在以太网上是 1500 字节。你要寄一件 5000 字节的东西,就得拆成四箱。而 UDP 的致命规则是:四箱里任何一箱丢了,整件东西全废,而且它压根不会重寄。
这个账有多亏?假设丢件率是 1%——一箱寄成功的概率是 99%,四箱全到的概率就只有 96%,也就是失败率从 1% 涨到了 4%,翻了四倍。好比你装修时把一整卷地板切成四段分别托运,哪一段丢了,剩下三段都铺不成——但快递公司说了它不负责补发。所以实践中有条铁律:单个 UDP 包尽量别超过 1400 字节左右,宁可自己在应用层分成几条独立的消息,也别让底层去替你切箱。
理论上限:65535 - 8(UDP 头)= 65527 字节
IPv4 实际:65535 - 20(IP 头)- 8 = 65507 字节
但这只是理论!实际发大包会触发 IP 分片:
一个 5000 字节的 UDP 数据报在 MTU=1500 的以太网上
→ 被 IP 层切成 4 个分片发出
→ 【致命问题】任何一个分片丢了,
整个数据报都被丢弃,而且 UDP 不会重传
→ 5000 字节的包在 1% 丢包率下,
实际失败率接近 4%(四个分片任一丢失即失败)
所以实践中的黄金法则:
★ 单个 UDP 数据报不要超过 1400 字节左右
(给各种隧道封装留余量)
★ DNS 传统上限制在 512 字节(RFC 1035),
EDNS0 扩展到 4096,但实践中建议 1232
因为 1280(IPv6 最小 MTU)- 40 - 8 = 1232
UDP 的特点
| 特点 | TCP | UDP |
|---|---|---|
| 连接 | 要先建立连接(三次握手) | 不用连接,直接发 |
| 可靠性 | 保证到达、按序、不重 | 不保证——可能丢、可能乱序 |
| 速度 | 慢(确认、重传、排序都要时间) | 快(发完就不管) |
| 头部开销 | 20 字节 | 8 字节 |
| 流量控制 | 有(滑动窗口、拥塞控制) | 没有——想发多快发多快 |
| 适用场景 | 网页、文件、邮件 | 视频、语音、游戏、直播 |
TCP vs UDP 全面对比
上面那张表是入门版。下面这张是完整版,把两个协议在十六个维度上摊开对照——这也是面试里"说说 TCP 和 UDP 的区别"这道题能答到的最深程度:
| 维度 | TCP | UDP |
|---|---|---|
| RFC / 年份 | RFC 793(1981)→ RFC 9293(2022) | RFC 768(1980),三页纸至今未改 |
| IP 协议号 | 6 | 17 |
| 头部大小 | 20 ~ 60 字节 | 固定 8 字节 |
| 连接 | 面向连接,三次握手四次挥手 | 无连接,直接发 |
| 数据抽象 | 字节流,无消息边界(会粘包) | 数据报,严格保留消息边界 |
| 可靠性 | 确认 + 重传 + 排序,保证不丢不重有序 | 不保证任何一项 |
| 顺序 | 严格按发送顺序交付 | 先到先交,可能乱序 |
| 流量控制 | 滑动窗口 | 无 |
| 拥塞控制 | 慢启动 / 拥塞避免 / 快重传 / 快恢复 | 无——想发多快发多快 |
| 校验和 | 强制 | IPv4 可选、IPv6 强制 |
| 通信模式 | 只能一对一(单播) | 支持单播、广播、多播 |
| 服务端资源占用 | 每连接几 KB 内核内存 + 一个 fd | 一个 socket 可服务无限客户端 |
| 首字节延迟 | 至少 1 个 RTT(握手) | 0——第一个包就带数据 |
| 丢包时的行为 | 停下来重传,后续数据被阻塞 | 丢了就没了,后面照常继续 |
| 能否被中间设备优化 | 可以(TSO/GSO 硬件卸载成熟) | 相对弱,但 GSO/GRO 也在支持 |
| 典型应用 | HTTP/1.1、HTTP/2、SSH、SMTP、数据库 | DNS、DHCP、NTP、SNMP、RTP、QUIC、VXLAN、WireGuard |
倒数第三行"通信模式"是一个常被忽略的关键差异。TCP 在原理上就不可能支持广播和多播——因为它要为每个对端维护序号和确认状态,一对多时"确认"这件事根本无法定义(一千个接收者里有三个没收到,你该怎么办?)。所以凡是"一份数据发给很多人"的场景,UDP 是唯一选择。
为什么"不可靠"反而是优点
初学者最容易产生的误解是"UDP 是残缺版的 TCP"。恰恰相反:UDP 的"不做"是一种主动设计,它把决策权交还给了应用。要理解这一点,得看清 TCP 的可靠性在某些场景下具体是怎么变成负担的。
核心矛盾:实时场景里,"迟到的正确数据"等于垃圾数据。
为什么"补上来"有时候反而更糟?换成大白话:午休只有一小时,你点的外卖迟到了八十分钟。它做得再好、一分钟不少地送到了,可你已经回工位开会了——这份饭对你的价值是零。而 TCP 干的事,恰恰是"哪怕晚八十分钟也一定把这份饭送到你手上"。
更糟的是它连带的副作用。视频通话每秒 30 帧,两帧之间隔 33 毫秒。第 100 帧丢了,TCP 要花 80 毫秒去补——而这 80 毫秒里,第 101、102 帧其实早就到了,却被 TCP 扣在门口不让进(因为它坚持"必须按顺序交付")。结果画面冻结八十毫秒,然后三帧一起涌出来——画面快进、声音爆音。为了不丢那一帧,你付出了整段体验。
UDP 的做法:什么都不做。第 100 帧丢了就丢了,33 毫秒后第 101 帧照常播放。用户看到的是某一瞬间画面略微模糊或有个小马赛克,连续性完全没被打断,很可能压根没注意到。
所以判断准则只有一条:这份数据有没有"保质期"?过期后价值是不是归零?会归零的——语音、视频、游戏里的位置、传感器读数、股票行情——重传就是纯负担。不会归零的——文件、网页、转账记录、聊天消息——晚到多久价值都不变,那就必须用 TCP。好比热菜和干货:热菜凉了就废了,干货放一年还是那个干货。
【场景:视频通话,每秒 30 帧,每帧间隔 33 毫秒】
用 TCP 传,第 100 帧丢了:
t=0ms 发现丢包(3 个重复 ACK)
t=0ms 发起重传请求
t=80ms 重传的第 100 帧终于到达(一个 RTT)
但这 80 毫秒里:
· 第 101~102 帧其实早就到了,
却被 TCP 扣在缓冲区里不交给上层(队头阻塞)
· 画面完全冻结 80 毫秒
· 然后 3 帧一起涌出来 → 画面快进、声音爆音
最终结果:为了"不丢那一帧",付出了 80ms 冻结 + 追帧抖动
用 UDP 传,第 100 帧丢了:
t=0ms 什么都不做
t=33ms 第 101 帧正常播放
用户体验:某一瞬间画面略微模糊或有个小马赛克
连续性完全没被破坏,很可能压根没注意到
★ 关键洞察:那一帧的内容已经过去了。
80 毫秒后把它补回来,播给谁看?
这个逻辑可以推广成一条判断准则:如果数据有"保质期",且过期后价值归零,那么重传机制就是纯粹的负担。符合这个条件的场景包括:语音、视频、游戏中的位置同步、传感器读数、实时监控画面、股票行情推送。而不符合的场景是:文件、网页、数据库事务、消息投递——这些数据不管晚多久到,价值都不变。
UDP 的第二个优势是可控性。TCP 的行为全部由内核决定,你无法干预:什么时候重传、重传几次、拥塞时降多少速——这些策略对所有应用一视同仁。而 UDP 把这些决策交还给你,于是你可以做出 TCP 永远做不到的策略:
- 按重要性分级重传游戏里"放技能"这个操作必须重传,"每 33 毫秒的坐标更新"丢了就丢了——因为下一个坐标马上就来,重传旧坐标毫无意义。TCP 无法区分这两种数据,只会一视同仁地全部重传。
- 用新数据覆盖旧数据如果第 100 帧丢了而第 101 帧已到,正确的做法是直接丢弃对第 100 帧的重传需求。UDP 上你能这么做,TCP 上你无能为力。
- 自定义拥塞策略视频通话的正确反应不是"减少发包数",而是降低码率(换成更低分辨率继续保持 30 帧)。这个决策只有应用层才知道怎么做。
- 在一个 socket 上跑多条逻辑流不受 TCP"一条字节流严格保序"的约束,天然避免了队头阻塞。QUIC 的多流设计就建立在这一点上。
还有一个纯性能层面的优势:服务端的可扩展性。一台 DNS 服务器可以用一个 UDP socket 应付每秒几十万次查询,因为它不需要为任何客户端保存状态;换成 TCP,同样的负载需要维护几十万个连接结构体、几十万个文件描述符,内存和调度开销都要高出好几个数量级。这就是为什么互联网最核心的基础设施服务(DNS、DHCP、NTP)几乎全部选择了 UDP。
这个"可扩展性"好比学校门口的两种通知方式。TCP 的做法是给每位家长单独打电话确认:"您好,明天九点运动会,您听清了吗?好,再见。"两千个家长就要打两千通电话,每通电话期间老师还得记着"这个人打到哪了、说到哪句了"——一个人根本忙不过来。UDP 的做法是拿起大喇叭喊一遍:"明天九点运动会!"喊完就完了,一个人可以同时通知两千个、两万个,因为她压根不需要记住任何人的状态。这就是为什么互联网最核心那几个基础服务,全都选了 UDP。
UDP 为什么快
UDP 快的原因就三个字:不操心。
- 不握手不用先"喂?听得到吗?"——直接说事。
- 不确认发完就忘——不等对方说"我收到了"。
- 不重传丢了就丢了——不补发。
- 不排序不按顺序到?那也不管——先到先用。
TCP 像递水杯——一杯一杯递,每杯都确认对方接住了,没接住就再递一次。
UDP 像泼水——你拿盆一泼,水洒出去,接到多少算多少。
泼水当然快——但地上会湿(丢包)。
UDP 在你生活中的样子
| 应用 | 为什么用 UDP |
|---|---|
| 微信视频通话 | 画面偶尔卡一下你能忍,但"现在"的画面必须实时到 |
| 抖音直播 | 观众容忍花屏 0.5 秒,不能容忍延迟 3 秒 |
| 王者荣耀 | 英雄位置必须实时同步,偶尔丢一帧无所谓 |
| 语音消息 | 声音流必须连续,偶尔爆音没关系 |
| DNS 查询 | 一次问答搞定,不用建立连接 |
| 在线会议(Zoom/腾讯会议) | 多人实时视频,延迟敏感 |
七类典型 UDP 应用,各自为什么选它
上面那张表列了生活中的场景,这里换一个角度:按"选 UDP 的理由"分类。你会发现理由其实只有四种,但组合出了非常多样的应用。
| 应用 | 端口 | 选 UDP 的核心理由 |
|---|---|---|
| DNS(RFC 1035) | 53 | 一问一答,握手成本比查询本身还高。查询包约 30 字节,如果用 TCP 要先花 1 个 RTT 握手——延迟直接翻倍。响应超过 512 字节时才回退到 TCP(这叫 TC 截断标志) |
| DHCP(RFC 2131) | 67 / 68 | 此时客户端还没有 IP 地址,只能靠广播(255.255.255.255)找 DHCP 服务器。TCP 不支持广播,物理上就用不了 |
| NTP(RFC 5905) | 123 | 测时间必须测最短路径延迟。TCP 的重传和缓冲会让时间戳失真,反而算不准时钟偏移 |
| SNMP | 161 / 162 | 监控查询量巨大且容忍丢失,一次查询失败下次轮询就补上了 |
| RTP / RTCP(RFC 3550) | 动态 | 音视频实时流,迟到的帧无价值。RTP 在 UDP 上加了序号和时间戳,只用来"排序和对齐",不用来重传 |
| SIP / VoIP | 5060 | 语音延迟超过 150 ms 人就能感知,超过 400 ms 就无法正常对话 |
| 游戏(自定义协议) | 各异 | 需要按数据重要性区别对待,TCP 的一刀切策略做不到 |
| QUIC / HTTP/3(RFC 9000) | 443 | 要在用户空间重建一个更好的可靠传输层,UDP 是唯一能穿过全球中间设备的载体 |
| VXLAN(RFC 7348) | 4789 | 隧道封装只需要"搬运",可靠性由被封装的内层协议自己负责 |
| WireGuard | 51820 | VPN 隧道;在 TCP 上套 TCP 会导致灾难性的"重传叠加" |
最后那一行提到的问题值得单独说,因为它是一个反直觉但极重要的结论:绝对不要在 TCP 隧道里跑 TCP。这个现象叫 TCP meltdown(TCP 崩塌):外层 TCP 和内层 TCP 会各自独立地做重传和拥塞控制,当链路抖动时,内层已经重传了一次,外层又重传一次,两层的超时定时器互相干扰,形成重传风暴,吞吐量断崖式下跌到接近零。这就是所有正经的 VPN 协议(WireGuard、IPsec、OpenVPN 的 UDP 模式)都默认用 UDP 的根本原因。
DNS 那一行也有一个有趣的技术细节:DNS 其实同时支持 UDP 和 TCP,都用 53 端口。规则是先用 UDP 查,如果响应超过 512 字节(或 EDNS0 协商的更大值),服务器会在响应里设置 TC(Truncated)标志位,客户端看到后自动改用 TCP 重查一次。区域传送(zone transfer,主从 DNS 之间同步整个域的记录)因为数据量大,规范规定必须用 TCP。
在 UDP 上自己造可靠性
既然 UDP 不可靠,那基于 UDP 的应用是怎么保证关键数据不丢的?答案是在应用层重新实现一套,但只实现自己需要的那部分。这里有五种主流手段,实践中往往混合使用。
手段一:应用层序号 + ACK + 选择性重传。这是最直接的做法,本质是"手写一个精简版 TCP",但关键在于只对需要可靠的数据这么做:
// 游戏协议的典型分级设计
struct Packet {
uint32 seq; // 应用层序号
uint8 channel; // 0=不可靠 1=可靠有序 2=可靠无序
uint8 flags; // 是否需要 ACK
...payload
};
channel 0(不可靠):玩家坐标、朝向、动画状态
每 33 ms 发一次,丢了不管 —— 下一个包就是最新状态
重传旧坐标毫无意义,甚至有害(会让角色抽搐回跳)
channel 1(可靠有序):聊天消息、道具购买、房间状态变更
必须到,且必须按顺序 —— 走应用层重传
channel 2(可靠无序):技能释放、伤害结算
必须到,但先后顺序不重要 —— 避免不必要的队头阻塞
★ 这种"分通道"设计是 TCP 永远无法提供的,
因为 TCP 只有一个通道且必须严格保序。
ENet、KCP、Reliable UDP 这些库做的就是这件事。
手段二:FEC 前向纠错(Forward Error Correction)。这是一种思路完全不同的解法——与其丢了再要,不如提前多发一些冗余数据,让接收方能自己算出丢的那份。
FEC(Forward Error Correction,前向纠错——提前多发一点冗余信息,让对方能自己算出丢掉的那份)这个思路换成大白话,就是厨房里那条最朴素的经验:四个人吃饭,你煮五份饭。不是因为有人吃两份,而是因为万一有一份糊了,你不用重新开火等二十分钟。
它妙在哪?重传的思路是"发现丢了 → 打电话要 → 等它送来",一个来回八十毫秒;FEC 的思路是"东西早就在你手上了,你自己算一下就出来了",零等待。好比你手上有三个数和它们的总和,任意丢掉一个数,你都能用总和减出来——不用问任何人。代价是什么?那第五份饭是实打实多花的米——上面的例子里带宽多花了 33%。实际系统会算得更精细:发 10 份数据 + 2 份冗余,只多花 20%,却能容忍任意 2 份丢失。
谁最需要它?凡是"要么等不起、要么根本没法等"的场合。音频流丢包最刺耳,所以微信、Zoom 的语音都用它;而深空探测的一个来回以分钟计,重传这个选项压根就不存在——只能靠多带冗余。
【FEC 的原理:最简单的异或方案】
发送方要发 3 个数据包,额外算一个冗余包:
P1 = "AAA"
P2 = "BBB"
P3 = "CCC"
R = P1 ⊕ P2 ⊕ P3 ← 冗余包(异或)
发出 4 个包,接收方只要收到任意 3 个就能恢复全部:
收到 P1, P2, R(P3 丢了)
→ P3 = P1 ⊕ P2 ⊕ R ← 直接算出来,无需重传!
【为什么这在实时场景里是降维打击】
重传方案:发现丢包 → 请求 → 等待 → 收到 = 1 个 RTT(80ms)
FEC 方案:不需要任何往返,本地立即恢复 = 0 RTT
【代价】
带宽开销:上例是 33%(4 个包传 3 个包的数据)
实际系统用 Reed-Solomon 码可以更精细地权衡,
比如 10 个数据包 + 2 个冗余包 = 20% 开销,
可容忍任意 2 个包丢失
【谁在用】
· 微信、Zoom 等音视频通话的音频流(音频丢包最刺耳)
· 卫星通信、深空探测(RTT 以分钟计,重传压根不现实)
· KCP、QUIC 的部分实现
· 直播的 SRT / RIST 协议
手段三:抖动缓冲(Jitter Buffer)。这个机制解决的不是丢包,而是到达时间不均匀——这在实时音视频里是同等严重的问题。
抖动缓冲(Jitter Buffer,先攒一小段再开始播,用来抹平到达节奏的不齐)好比等公交车的经验。公交车不按点:有时三分钟来两辆,有时二十分钟不来一辆。但只要站台上永远排着几个人,"上车"这件事看起来就是连续均匀的。
音频包也是这样:发送方每 20 毫秒发一个,但到达时间可能是 0、18、45、47、49、95 毫秒——你要是收到就立刻播,声音会时快时慢、断断续续。解法就是先在站台上攒几个人:接收方缓冲若干毫秒再开始播,之后按恒定的 20 毫秒节奏一个一个取出来。核心权衡也和站台一样直白:站台上排的人越多,越不怕车不按点,但每个人等的时间也越长。
这个权衡能一口气解释一件让很多人困惑的事:为什么"直播延迟 3 秒"大家觉得正常,而"通话延迟 3 秒"就完全不能用?因为直播不需要你回话——站台上排一百个人也没关系,反正你只管看。而通话是一来一回的,站台上多排一个人,对话就多别扭一分。同样的技术,缓冲策略完全相反。
【问题】发送方每 20 ms 发一个音频包,但到达时间是:
0ms, 18ms, 45ms, 47ms, 49ms, 95ms, 96ms ...
→ 如果收到就立刻播,声音会时快时慢、断断续续
【解法】接收方先缓冲若干毫秒再开始播
缓冲区 [包1][包2][包3][包4]
↓ 以恒定的 20ms 节奏取出播放
这样即使到达抖动,播放也是平滑的
【核心权衡】
缓冲区大 → 抗抖动强,但端到端延迟增加
缓冲区小 → 延迟低,但容易"欠载"(buffer underrun)造成卡顿
实测取值:
语音通话 40 ~ 100 ms(延迟敏感,缓冲小)
视频会议 100 ~ 200 ms
直播 1 ~ 30 秒(不需要交互,可以大量缓冲换流畅)
点播 可以缓冲几十秒
★ 这就是为什么"直播延迟 3 秒"是正常的,
而"通话延迟 3 秒"是完全不可用的 ——
同样的技术,缓冲策略完全不同。
【自适应抖动缓冲】现代实现会动态调整:
测量最近 N 个包的到达间隔方差 → 网络抖动大就加大缓冲
→ WebRTC 的 NetEq 模块干的就是这件事,
还能在缺包时做"音频插值"(把前一帧拉长)来掩盖丢失
手段四:应用层拥塞控制。UDP 不做拥塞控制不等于应用可以无脑猛发——那样会把网络打垮,也害了自己。所以正经的 UDP 应用都得自己实现一套,只是策略更贴合业务:
- 码率自适应检测到丢包率上升时,视频编码器降低码率而不是降低帧率——从 1080p 降到 720p,保住流畅度牺牲清晰度。WebRTC 的 GCC(Google Congestion Control)算法做的就是这件事,它通过分析包到达延迟的梯度变化来提前预判拥塞,甚至不需要等到丢包。
- 发送速率整形把突发的数据打散成均匀速率发出(pacing),避免瞬时突发把路由器缓冲区打满。QUIC 和 BBR 都强制做 pacing。
- 遵守 TCP 友好性RFC 8085(UDP 使用指南)明确要求:UDP 应用必须实现某种拥塞控制,且不应该比同等条件下的 TCP 抢占更多带宽。这是一条道德约束也是工程约束——如果所有 UDP 应用都无节制发送,整个互联网会回到 1986 年的拥塞崩溃。
NAT 穿透与 UDP 打洞
这是 UDP 一个很少被教材提及但极其实用的应用场景。问题背景是:你和朋友都在家用路由器后面(各自都是内网 IP),怎么让两台机器直连?这对视频通话、P2P 下载、联机游戏至关重要——能直连就不用把流量绕经服务器,延迟更低、服务器带宽成本几乎为零。
难点在于 NAT 的规则:只有内网主动发出去过的连接,回包才能进来。外网想主动连内网,NAT 表里没有对应条目,包直接被丢。而现在双方都在 NAT 后面,谁也没法主动连谁。解法叫 UDP 打洞(UDP Hole Punching):
NAT(Network Address Translation,网络地址转换——整栋楼共用一个对外地址,进出的信件靠前台登记转交)这套规则换成大白话就是办公室前台的铁律:"只有咱们员工先寄出去过的信,对方的回信我才转交进来。没登记过的陌生来信,我一律退回。"
难题来了:A 在甲楼、B 在乙楼,两家前台都守着这条规则。A 想主动给 B 写信?乙楼前台查了登记本没这号事,直接退回。B 主动写给 A 也一样。两个人隔着两道前台,谁也联系不上谁。
"打洞"的解法妙在这里,一共五步:
① 双方各自先给一个第三方(比如街对面那家有公开地址的邮政代办点)寄一封信。这一寄,两家前台的登记本上就都记下了"我们的人往外发过信,对方的回信要放进来"。
② 代办点从信封上看到了两人各自的"对外地址"——也就是甲楼的大门牌和 A 用的那个信箱号,乙楼的大门牌和 B 用的那个信箱号。
③ 代办点把两人的对外地址互相告知。
④ 关键的一步:两人同时按这个地址给对方寄一封信。第一封信几乎一定会被对方的前台退回——但这不重要!因为你寄出这封信的动作,让你自己家的前台在登记本上记下了"对方的回信允许放进来"。洞就是这么打通的。
⑤ 于是对方稍后寄来的信,你家前台就放行了。直连建立,之后完全不用麻烦代办点。
最后还得"续洞":前台的登记本会定期清理,一般十几秒到几十秒没动静就划掉。所以双方必须隔一阵子互发一封信,纯粹为了让登记不过期——这就是所谓的"心跳"。
为什么这招对 UDP 好用,对 TCP 极难?因为 UDP 的前台只看地址,不问来龙去脉;TCP 的前台还要核对"这封信是不是完整流程里的第几步"——一封顺序不对的信,它会当成非法件直接销毁。
【UDP 打洞的完整流程】
角色:A 和 B 在各自 NAT 后,S 是有公网 IP 的信令服务器
① A 向 S 发一个 UDP 包
→ A 的 NAT 建立映射:192.168.1.5:5000 ←→ 1.2.3.4:61001
→ S 看到的源地址是 1.2.3.4:61001,记下来
② B 同样向 S 发包
→ B 的 NAT 映射:10.0.0.8:6000 ←→ 5.6.7.8:62002
→ S 记下 5.6.7.8:62002
③ S 把双方的"公网地址:端口"互相告知
→ A 得知 B 在 5.6.7.8:62002
→ B 得知 A 在 1.2.3.4:61001
④ 【关键的"打洞"动作】双方同时向对方的公网地址发 UDP 包
A → 5.6.7.8:62002 (第一个包很可能被 B 的 NAT 丢弃)
B → 1.2.3.4:61001 (同时也被 A 的 NAT 丢弃)
但!A 发出的那个包让 A 的 NAT 建立了一条
"允许 5.6.7.8:62002 回包"的映射 —— 洞打通了
⑤ 于是 B 稍后发来的包就能穿过 A 的 NAT 了,反之亦然
→ 直连建立,之后完全不经过 S
⑥ 【维持】双方必须定期互发心跳(通常 15~30 秒)
否则 NAT 映射超时会被清除,洞就堵上了
★ 为什么打洞对 UDP 容易、对 TCP 极难?
UDP 的 NAT 映射是"无状态"的,只看地址端口。
TCP 的 NAT 会跟踪握手状态机,一个乱序到达的
SYN 会被认为是非法包直接丢弃,成功率低得多。
这套机制被标准化成了三个协议,合称 ICE 框架,是 WebRTC 的基石:
| 协议 | 作用 | 成本 |
|---|---|---|
| STUN(RFC 8489) | 帮你问出"我在外网看起来是什么地址端口" | 极低,服务器只回一句话 |
| TURN(RFC 8656) | 打洞失败时的兜底:所有流量中转 | 高,要消耗服务器的全部带宽 |
| ICE(RFC 8445) | 把所有可能的路径(本地、STUN、TURN)都试一遍,选最优的 | — |
打洞的成功率取决于 NAT 的类型,这是一个很现实的工程问题。锥形 NAT(Cone NAT)对同一个内网端口总是分配同一个外网端口,打洞成功率很高;而对称 NAT(Symmetric NAT)会为每个不同的目标地址分配不同的外网端口,导致 STUN 问出来的端口对 B 无效,打洞基本失败,只能退回 TURN 中转。业界实测数据是:大约 80%~90% 的连接能通过打洞直连,剩下 10%~20% 必须走 TURN 中转——而这一小部分往往消耗了绝大部分的服务器带宽成本。
QUIC:为什么在 UDP 上重建可靠传输
QUIC(RFC 9000,2021 年)是 UDP 故事里最精彩的一章。它做了一件听起来非常荒谬的事:在"不可靠"的 UDP 上,重新实现了一套完整的可靠传输——序号、确认、重传、拥塞控制、流量控制,一样不少。那为什么不直接用 TCP?
这件事听着玄,其实就是一个非常朴素的动机:TCP 那套东西被焊死在操作系统内核里了,想改一个算法,得等全世界几十亿台设备升级系统,周期以十年计。好比你想给全国所有公交车换个新的报站系统——车是国家的,你改不了。那就干脆自己造一辆小面包车:QUIC 跑在普通程序里,浏览器发一个新版本,全球几亿人的传输层就跟着升级了一次。
那为什么非得藏在 UDP 里,不能自己另开一条路?换成大白话:因为全世界路上的关卡(防火墙、NAT 设备)只认识两种车牌——一种是 TCP,一种是 UDP。挂别的牌子,一路上会被无数关卡直接拦下销毁。有个技术上明显更强的协议叫 SCTP,就是因为挂了第三种车牌,二十多年过去在公网上依然基本跑不通。所以 QUIC 干的事,本质上就是给自己套了一副 UDP 的车牌,一路畅通无阻。
它换来的四样好处也都很好懂。一,各走各的道——一条道上掉了东西,只影响那一条,其他照常(前面讲过的队头阻塞被根治)。二,进门更快——把"打招呼"和"验身份"合成一步,跨洋链路上省掉几百毫秒;熟客甚至连招呼都不用打。三,换网不断线——你走出家门从 WiFi 切到 5G,下载和通话完全不中断,因为它认的是一个跟地址无关的编号,好比认人不认门牌。四,能快速改进——不用等全世界换操作系统。
答案分四层,每一层都很有说服力:
- 理由一 · 彻底消灭队头阻塞TCP 只有一条严格保序的字节流,一个包丢了后面全堵。QUIC 在一条连接里跑多个独立的流(Stream),每个流单独保序、单独重传。流 1 丢包只阻塞流 1,流 2 流 3 照常交付。HTTP/2 在应用层做的多路复用,终于在传输层得到了真正的支持。
- 理由二 · 握手从 3 个 RTT 压到 1 个甚至 0 个TCP 和 TLS 是两个独立的握手,必须串行。QUIC 把传输层握手和加密握手合并成一次:1 个 RTT 建连;如果之前连过(有缓存的会话票据),可以做 0-RTT——第一个包就带着业务数据。跨洋链路上这直接省掉几百毫秒。
- 理由三 · 连接迁移,切网不断连TCP 连接由五元组标识,IP 一变(WiFi 切 5G)连接必死。QUIC 用一个独立的连接 ID标识连接,与 IP 和端口完全解耦。你走出家门从 WiFi 切到 5G,下载和视频通话完全不中断——这在 TCP 上是原理上不可能做到的。
- 理由四 · 可以快速迭代TCP 实现在内核里,改一个算法要等全球几十亿台设备升级操作系统,周期是十年计。QUIC 跑在用户空间,就是一个普通的库——Chrome 发一个版本,全球几亿用户的传输层就升级了一次。这可能是 QUIC 选 UDP 最根本的动机:夺回演进的主动权。
那么为什么不干脆定义一个全新的传输层协议(新的 IP 协议号),而要藏在 UDP 里?因为 SCTP 已经用惨败证明了那条路走不通。SCTP(RFC 9260,2000 年)技术上明显优于 TCP,支持多流、多宿主、天生抗 SYN Flood,但它需要 IP 协议号 132——而世界上大量的 NAT 设备和防火墙只认识 6(TCP)和 17(UDP),看到 132 一律丢弃。二十多年过去,SCTP 在公网上依然基本不可用。
【QUIC 的封装:把传输层整个藏进 UDP 载荷】
[以太网头 14][IP 头 20][UDP 头 8][─── QUIC 包 ───]
│
├─ 长/短包头(含连接 ID)
├─ 包号(受加密保护)
└─ 加密的载荷:
├ CRYPTO 帧(TLS 1.3 握手)
├ STREAM 帧(多个流的数据)
├ ACK 帧
└ 各种控制帧
★ 中间设备(NAT、防火墙、DPI)看到的只是"普通的 UDP 流量"
★ 除了连接 ID,几乎所有字段都被加密 ——
中间盒既看不懂也改不了,这从设计上就防止了
"腰部僵化"再次发生
★ 代价:运营商无法再对 QUIC 做 TCP 那样的
流量优化和分析,部分企业网因此干脆封禁 UDP 443
QUIC 的部署也遇到过现实阻力,值得诚实提及:部分企业防火墙和校园网会直接封禁 UDP 443 端口(因为管不了加密流量)。所以所有支持 HTTP/3 的服务器都必须同时保留 TCP 上的 HTTP/2 作为回退,客户端先用 TCP 连上,通过响应头 Alt-Svc: h3=":443" 得知对方支持 h3,然后并行尝试 QUIC,通了就切过去,不通就继续用 TCP(这个策略叫"快乐眼球")。
广播与多播:UDP 独有的能力
前面对比表里提到过,广播和多播是 TCP 在原理上做不到的事。这里说清它们的机制和用途。
| 模式 | 目标 | 地址范围 | 能跨路由器吗 | 典型用途 |
|---|---|---|---|---|
| 单播 Unicast | 一对一 | 普通 IP 地址 | 能 | 绝大多数通信 |
| 广播 Broadcast | 一对本网段所有主机 | 255.255.255.255(受限) 或 192.168.1.255(定向) | 不能,路由器一律拦截 | DHCP 发现、ARP、局域网设备发现 |
| 多播 Multicast | 一对"订阅了某个组"的主机 | IPv4:224.0.0.0 ~ 239.255.255.255 | 能,但需要路由器支持并配置 | IPTV、金融行情推送、组播文件分发 |
| 任播 Anycast | 一对"最近的那一个" | 普通 IP,靠 BGP 宣告同一地址 | 能 | DNS 根服务器、CDN 就近接入 |
多播的价值需要用数字才能体现。假设一场直播有 10000 个观众,码率 2 Mbps:
这个账换成大白话:单播好比学校要通知一万名学生开运动会,老师挨个打电话说一遍——一万通电话。多播好比她只在广播室说一次,信号顺着走廊里的线路往下走,到每个分叉口才复制成几份,最后每间教室的喇叭里都响起来。老师只说了一句话,一万个人都听到了——带宽需求降低了一万倍。
那为什么公网上几乎没人用多播?原因也很生活化:这条广播线路需要途经的每一个分叉口都装好设备、配好规则。校内的走廊你说了算,可跨了运营商就没人愿意管——那些设备是别人家的,凭什么替你复制信号、谁来付这个电费?所以现实是:运营商自己封闭网络里的 IPTV 大量用多播,金融交易所同机房内用多播,而公网直播全部退回"挨个打电话",只是把打电话的人换成了离你最近的那个 CDN 节点。
【单播方案】服务器给每个观众各发一份
上行带宽 = 2 Mbps × 10000 = 20 Gbps
→ 需要一整个机房的出口带宽
【多播方案】服务器只发一份,由网络设备复制分发
服务器上行 = 2 Mbps(就这么多!)
网络中的每条链路上,同一份数据也只经过一次
路由器在分叉点才复制成多份
→ 带宽需求降低了一万倍
【为什么公网上没人用多播?】
· 需要路径上每一台路由器都支持并正确配置 PIM 等组播路由协议
· 跨运营商的组播路由几乎无法协商(谁给谁买单?)
· 没有拥塞控制机制,容易被滥用做放大攻击
· 计费和权限管理困难
【所以现实是】
· 运营商内部的 IPTV 大量使用多播(封闭网络,可控)
· 金融交易所的行情推送用多播(同机房,微秒级要求)
· 公网直播全部退回单播 + CDN 边缘复制
→ CDN 本质上是"用应用层的方式实现了多播的效果"
【常用的保留多播地址】
224.0.0.1 本网段所有主机
224.0.0.2 本网段所有路由器
224.0.0.5/6 OSPF 路由器
224.0.0.251 mDNS(苹果 Bonjour、局域网设备发现)
239.x.x.x 管理范围内私用(类比私有 IP 段)
顺便说说任播(Anycast),它是一个非常巧妙的思路:在全球几十个地方部署机器,让它们全部宣告同一个 IP 地址,然后靠 BGP 路由自动把用户导向"网络距离最近"的那一台。最著名的例子是 DNS:8.8.8.8(Google)和 1.1.1.1(Cloudflare)并不是"一台服务器",而是遍布全球上百个数据中心的集群共用同一个地址。你在北京访问的和在纽约访问的,是完全不同的物理机器。任播天生适合 UDP——因为 UDP 无状态,路由切换时把包送到另一台机器上也毫无影响;而 TCP 的连接状态只在某一台机器上,路由一变连接就断了。
UDP 的"伪可靠性"
UDP 本身不保证可靠,但应用层可以自己加——很多基于 UDP 的协议,在 UDP 之上自己实现了"确认、重传、排序":
- QUIC(HTTP/3)Google 搞的,基于 UDP,但自己实现了 TCP 的可靠性 + 更快的握手——现在 Cloudflare、Google 都在用。
- RTP(实时传输协议)视频通话用的——UDP 之上加了时间戳和序号,让接收方能"拼"出连续画面。
- 游戏自定义协议王者荣耀、原神自己定义的——关键操作(放技能)用"可靠 UDP"重传,普通移动用"普通 UDP"。
UDP 的暗面:反射放大攻击
UDP 的"无连接"带来了一个严重的安全后果:因为不需要握手,源 IP 可以随意伪造,且伪造者不需要收到任何回包。这造就了 DDoS 攻击里破坏力最大的一类手法。
反射放大攻击(Reflection Amplification,伪造受害者的地址去请求,让一堆无辜的服务器把大响应全砸到受害者身上)这套手法换成大白话就是一个很恶劣的恶作剧:
攻击者填一张寄件单,收件人写成受害者的地址,内容是"请把你们的全套产品目录寄给我"。他自己只花了一张明信片的邮费,可对方寄出的是一本厚厚的册子。他这么给成千上万家公司各寄一张明信片——结果受害者家门口堆满了几万本产品册,门都推不开。
这个恶作剧最狠的地方有两点。第一,放大倍数惊人——最狠的一种(Memcached)能做到一张明信片换回五万本册子。第二,受害者防不住:这些册子来自成千上万家正经公司,他没法通过"拉黑寄件人"来自保,因为每一家都是无辜的、每一家都只是老实照单办事。
为什么 TCP 干不出这种事?因为 TCP 得先来回打三次招呼才能说事。你伪造了别人的地址,那"喂?在吗"的回话就寄到受害者那里去了,你自己永远收不到,也就永远没法把话说下去。握手在这里意外地成了一道安全机制。
QUIC 的对策特别值得学:它在协议里直接写死一条上限——在核实你的地址真伪之前,我发给你的东西不许超过你发给我的三倍。这一条把"一张明信片换五万本册子"压成了"一张明信片最多换三张纸"——恶作剧当场失去经济价值。这是用协议设计而不是靠运维手段解决安全问题的典范。
【反射放大攻击的原理】
攻击者想打垮受害者 V(IP = 1.2.3.4)
① 攻击者向一台开放的 DNS 服务器发查询,
但把源 IP 伪造成 V 的地址:
源 IP=1.2.3.4(假的)→ DNS 服务器:查询 "example.com ANY"
查询包只有 60 字节
② DNS 服务器老老实实地把响应发给"1.2.3.4"
ANY 查询的响应可能有 3000 字节
③ 放大倍数 = 3000 / 60 = 50 倍
攻击者用 1 Gbps 的上行,就能打出 50 Gbps 的攻击流量
而且流量来自成千上万台无辜的服务器,
受害者无法通过封禁源 IP 来防御
【各协议的放大倍数(实测)】
Memcached(UDP 11211) 最高 51000 倍 ← 史上最狠
NTP monlist(UDP 123) 约 550 倍
CharGen(UDP 19) 约 358 倍
DNS ANY(UDP 53) 约 28 ~ 54 倍
SSDP(UDP 1900) 约 30 倍
SNMP(UDP 161) 约 6 ~ 8 倍
【真实事件】
2018 年 2 月,GitHub 遭遇 1.35 Tbps 的 Memcached
反射攻击 —— 当时的历史纪录。攻击者只用了
很小的上行带宽,靠数万台暴露在公网上的
Memcached 服务器完成了放大。
【为什么 TCP 做不到这种攻击】
TCP 要三次握手才能发数据。伪造源 IP 的话,
SYN+ACK 会发到受害者那里,攻击者永远收不到,
也就永远无法完成握手让服务器发出大响应。
→ 握手在这里意外地成了一道安全机制。
【防御手段】
① 不要把 Memcached、Redis、NTP 等服务暴露在公网
② 运营商实施 BCP 38 入口过滤(丢弃源 IP 明显伪造的包)
③ 服务端做响应速率限制(RRL)
④ 对同一源 IP 的大响应先做一次挑战验证
—— QUIC 的做法:要求客户端先回一个地址验证 token,
并规定服务器在验证前发出的数据不得超过
收到数据的 3 倍(RFC 9000 明确的反放大限制)
QUIC 的那条"3 倍限制"设计得很值得学习:它承认了"无连接协议天生可被反射利用"这个事实,于是在协议层面写死一个上限——在完成地址验证之前,服务器发出的字节数不得超过收到字节数的 3 倍。这把放大倍数从几万倍压到了 3 倍,让反射攻击彻底失去经济价值。这是一个"用协议设计而非运维手段解决安全问题"的典范。
顺便把 2018 年 GitHub 那次"1.35 Tbps 攻击流量"翻译成人话,你才能感受到那是什么量级:相当于每秒钟把大约 300 部高清电影同时灌进一根网线。换个说法——一个人用最快的家用宽带下载一部高清电影要几分钟,而这次攻击是在每一秒钟里,把三百部电影的数据同时砸向同一个门口。而攻击者自己实际付出的上行带宽,可能还不如你家一台路由器——全靠那几万台暴露在公网上的服务器替他放大。
动手看看 UDP
最后用几条命令把前面讲的东西验证一遍。UDP 的行为特征在命令行上非常容易观察,而且有几处会让你直观感受到"它真的什么都不保证":
# 看本机所有 UDP 监听端口(注意 UDP 没有"连接状态"这一列)
$ ss -uln
State Recv-Q Send-Q Local Address:Port
UNCONN 0 0 0.0.0.0:68 ← DHCP 客户端
UNCONN 0 0 127.0.0.53:53 ← 本地 DNS 解析器
UNCONN 0 0 0.0.0.0:123 ← NTP
↑ 状态永远是 UNCONN(未连接),因为 UDP 没有连接概念
↑ 对比 TCP 的 ss -tln,那里会显示 LISTEN
$ netstat -anu # Windows / 老系统写法
# 用 nc 做一次最简单的 UDP 收发
# 终端 1(接收端):
$ nc -u -l 9999
# 终端 2(发送端):
$ nc -u 127.0.0.1 9999
hello ← 敲进去,对面立刻出现
# 【重点实验】把接收端关掉,再发一次:
$ nc -u 127.0.0.1 9999
hello ← 没有任何报错!包发出去就没了
↑ 这就是"无连接"最直观的体现
↑ 换成 TCP(nc 127.0.0.1 9999)会立刻
"Connection refused"
# 抓 UDP 包看头部(注意 length 就是那 8 字节头里的长度字段)
$ tcpdump -i any -nn udp port 53 -v
IP 192.168.1.20.54321 > 8.8.8.8.53: 12345+ A? example.com. (29)
↑ UDP 载荷 29 字节
# Wireshark 显示过滤器
udp # 所有 UDP
udp.port == 53 # DNS
udp.length > 1400 # 找可能触发分片的大包
ip.flags.mf == 1 # 找 IP 分片(more fragments 标志)
quic # QUIC 流量(Wireshark 能部分解析)
dns.flags.truncated == 1 # 找到"响应太大改用 TCP"的 DNS
# 观察 UDP 丢包统计(Linux)
$ netstat -su
Udp:
1234567 packets received
89 packets to unknown port received ← 收到但没程序监听
12 packet receive errors
45 receive buffer errors ← 【重要】缓冲区满导致丢包
↑ 这个数字持续增长说明应用读得太慢,
应调大 SO_RCVBUF 或优化处理逻辑
—— 这是 UDP 服务最常见的性能问题
# 调大 UDP 接收缓冲区(防止突发流量被丢)
$ sysctl -w net.core.rmem_max=16777216
$ sysctl -w net.core.rmem_default=262144
# 应用侧:setsockopt(fd, SOL_SOCKET, SO_RCVBUF, ...)
那个 receive buffer errors 计数器是运维 UDP 服务时最该盯的指标。UDP 没有流量控制,所以发送方压根不知道你处理不过来,只会继续猛发;内核缓冲区一满就直接丢包,而且不通知任何人。这个丢包在应用日志里完全看不到痕迹,只能靠这个计数器发现。很多"UDP 服务偶发数据缺失"的疑难问题,最终都定位到这一行数字上。
这件事好比你家门口那个快递柜。柜子满了,后面的件就只能被扔在地上,或者干脆退回——而且没人给你发短信说"有件放不进去"。UDP 没有流量控制,意味着寄件方压根不知道你的柜子满了,只会一件接一件继续寄。你的程序日志里一个字都看不出异常,因为丢件发生在柜子那一层,压根没进你家门。所以对 UDP 服务来说,盯住这一行计数器几乎是唯一的发现手段——它持续增长,就说明你取件取得太慢了,要么扩大柜子(调大接收缓冲区),要么加快取件(优化处理逻辑)。
什么时候绝对不能用 UDP
文件下载
文件必须 100% 完整——少一个字节都打不开。必须用 TCP。
网页浏览
HTML 错一个字可能整页乱码。必须用 TCP(或基于 UDP 但带可靠性的 QUIC)。
银行转账
数据必须准确无误,还要防篡改。必须用 TCP + TLS。
UDP = "泼出去的水"——快、简单、不保证。适合"实时但容错"的场景:视频、语音、游戏、直播。
TCP 和 UDP 不是"谁好谁坏"——是不同场景的不同选择。理解它们的取舍,你就理解了网络设计的核心思想:没有银弹,只有权衡。