有一次线上音视频服务抖动,画质疯狂往下掉,TCP 通道却一直健康,排查了半天,最后发现是运营商对某个 UDP 大包做了策略限速。那一刻我突然意识到,很多人把 UDP 当成“TCP 的简单版”来学,其实根本没搞懂它的生存规则。UDP 协议头只有 8 字节,没有握手、没有挥手、没有重传,却在 DNS、DHCP、音视频、游戏、物联网和 QUIC 这类现代传输引擎里占据不可替代的位置。这篇文章围绕 UDP 协议,从报文格式、真实网络中的生存状态,到抓包排障,再到如何在 UDP 之上实现可靠传输,把我的实践经验和踩坑记录都摊开讲。适合三类人:写后端接口但经常遇到 UDP 丢包问题的同学;做音视频、游戏、IoT 通信,想自研传输方案的工程师;以及准备网络面试、想真正理解传输层取舍的人。
1. UDP 的“快”到底从哪来:协议初心与核心定位
UDP 的设计初衷在 RFC 768 里只有极短几页:提供一种最小开销、无状态的数据报服务。它不是残缺的 TCP,而是针对“不需要可靠机制”的一类场景做的直接映射。IP 层本来就能把包从源送到目的,TCP 做的事是额外增加连接管理、可靠传输、流量控制和拥塞控制。UDP 几乎什么都不加,所以它和 IP 的关系最近。把 UDP 理解为“带端口号的 IP”,或者“IP 的直通用户接口”,是一个很关键的认识角度。
1.1 无连接、无状态、有边界
“无连接”不只是不用握手,更重要的是内核和网络中间设备都不为 UDP 维护连接状态。发送方把数据报交给 IP 层,之后是好是坏没人管。这种“不管”体现在两个维度:对端无法确认,网络设备不记录流状态。
UDP 报文的边界由数据报决定。应用层调用一次 sendto 发多少内容,接收端 recvfrom 就按一个整体接收。这一点和 TCP 有本质区别:TCP 是字节流,没有消息边界,应用层要自己解决粘包和分包问题;UDP 天然保留边界。对很多短请求场景来说,这种模型省掉了多少解析工作,做过 TCP 拆包的人应该深有体会。
但“边界保留”也会带来一个新坑:如果接收端缓冲区比数据报小,内核会把超出部分丢弃,应用只会看到截断后的内容。这个坑后面第五章会详细讲。
1.2 一张表看懂 UDP 和 TCP 的本质差异
| 对比维度 | UDP | TCP |
|---|---|---|
| 连接状态 | 无连接 | 面向连接,三次握手 |
| 可靠性 | 不保证,不确认,不重传 | 序号确认,超时重传 |
| 消息边界 | 数据报,保留边界 | 字节流,无边界 |
| 头部大小 | 8 字节 | 至少 20 字节 |
| 握手次数 | 0 | 1 个 RTT 起 |
| 拥塞控制 | 不做,发多少算多少 | 有窗口和拥塞控制 |
| 流量控制 | 不做 | 有 |
| 适用场景 | 实时音视频、DNS、IoT、游戏 | 文件传输、网页、数据库、事务 |
看这张表有个诀窍:UDP 那一列几乎全是“不做”和“更低”。但也正是因为不做,它才能把延迟压到极低,把协议开销压到极小。网络世界里没有绝对好坏,只有适合不适合。
1.3 哪些场景必须拥抱 UDP,哪些应绕开
从我的实际项目经验看,下面这些场景选 UDP 是合理的:
- 实时音视频:一帧视频丢了,重传也赶不上播放时间,不如直接跳过。RTP 就是典型基于 UDP 的实时传输协议。
- DNS 查询:一问一答,请求包往往不到 100 字节,若用 TCP 还要先握手,延迟多出整整一个 RTT。
- DHCP 地址获取:设备还没 IP,靠广播通信,只有 UDP 能天然适配。
- 物联网传感器上报:周期性上报,丢一条下一条很快来,重传反而增加功耗和带宽。
- 游戏帧同步和状态同步:玩家操作要最快到达,就算有丢包也要按最新状态继续跑。
- 私有协议和广播组播:一对多通信,TCP 做不到广播,UDP 天然支持。
反过来,大文件传输、复杂事务、跨公网长链路且要求低丢包率的业务,最好还是老实走 TCP 或 QUIC。人为给 UDP 强行加可靠层能解决问题,但成本往往比直接上 TCP 更高。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 8 字节头部逐字段拆解:报文格式背后的边界问题
UDP 头只有 8 字节,四个字段各占 16 位。很多人在面试时能背出“源端口、目的端口、长度、校验和”,但真到排查问题的时候,字段背后的语义才是关键。
2.1 四个字段逐一拆解
- 源端口:发送方使用的端口,接收方靠它回包。如果发送方不需要接收响应,可以填 0。
- 目的端口:接收方进程对应的端口,是内核将数据报分发给哪个 socket 的依据。
- 长度:UDP 头部加上数据的字节数。最小值是 8,如果实际收到的长度小于 8,说明报文非法,协议栈会直接丢弃。
- 校验和:覆盖伪首部、UDP 头、数据的校验和,用来检测传输过程中是否出错。
这里最容易出问题的是长度和校验和之间的配合。很多应用为图省事,recvfrom 的缓冲区只开了 2KB,结果服务端收到大包时,内核发现缓冲区装不下,直接把整个数据报丢弃,应用完全无感知。对项目来说,UDP 接收缓冲区一定要根据业务最大包长度设计,宁可多分配也不能少。
还有一个常被忽略的问题:UDP 的长度字段在 IP 分片时指的是分片前的原始长度。后续分片里没有 UDP 头,只有 IP 层信息,所以某些防火墙和中间设备对非首分片处理不友好,这也是大 UDP 包容易丢的一个重要原因。
2.2 校验和与“伪首部”:UDP 如何跨层验证数据
UDP 校验和不是只校验 UDP 自己的内容,它把 IP 层的部分关键信息也拉进来一起算,这部分叫“伪首部”。伪首部由源 IP 地址、目的 IP 地址、协议号、UDP 长度组成,只在计算校验和时临时拼装,不随报文传输。
为什么需要伪首部?因为 UDP 依赖 IP 层的地址转发,如果 IP 头里的源地址或目的地址出错,而 UDP 层自己又不检查,数据就可能被投递给错误的进程或主机。伪首部的存在就是为了把“这一包确实发给这个 IP、这个端口”绑定在一起。
计算方法是二进制反码求和:把伪首部、UDP 头、数据按 16 位分组,遇到奇数长度数据则在末尾补一个零字节参与计算。IPv4 里 UDP 校验和允许为 0,表示未计算;但如果计算出来的结果正好是 0,发送方要把它表示为 0xFFFF,这是 RFC 里的规定。IPv6 下校验和是强制要求,不能省略。
实操中有一个很迷惑人的现象:用 Wireshark 抓本机包,经常看到校验和显示 invalid,但业务完全正常。这通常是网卡校验和卸载(checksum offload)导致的,传输路径上实际校验由网卡硬件完成,抓包软件取到的快照里校验和还没有被填充。遇到这种显示不要立刻定性为丢包,可以看看 Wireshark 有没有提示 checksum offload 字样。
2.3 UDP-Lite:允许“部分损坏”的变体
UDP-Lite 是在 UDP 基础上扩展出的协议,IP 协议号为 136。它和 UDP 最大的区别是校验和覆盖范围可变:可以只校验头部和部分数据,剩余数据即使损坏也放过去。这个特性对视频、语音这类能容忍部分比特错误的实时流非常理想,因为音频里坏几个字节,播放出来可能只是轻微杂音,但整个包被校验和扔掉反而造成卡顿。
实际落地中,UDP-Lite 的生态不如 UDP 成熟,很多 NAT、防火墙和云平台对协议号 136 的包不识别、不转发。我在项目中评估过一轮,最后还是放弃了 UDP-Lite,原因是兼容性收益抵不上风险。如果要做,先在小规模可控网络里验证整条链路都支持,再谈上线。
3. 现实网络中的 UDP:DNS、音视频、IoT 与中间盒的拉扯
教科书只告诉你“UDP 不可靠”,但真正跑线上会发现,UDP 的麻烦远不止“丢包”两个字。它要面对的是 NAT、防火墙、运营商 QoS、中间设备状态老化这些现实规则。
3.1 高频 UDP 服务与应用分布
| 服务或协议 | 默认端口 | 为什么选 UDP |
|---|---|---|
| DNS | 53 | 一问一答,请求小,握手成本占比太高 |
| DHCP | 67/68 | 客户端无 IP,必须靠广播 |
| TFTP | 69 | 简单文件传输,轻量实现 |
| NTP | 123 | 周期性小包,时间敏感 |
| SNMP | 161/162 | 监控采集,短平快 |
| RTP/RTCP | 动态 | 实时音视频,允许丢包 |
| QUIC | 443 | 在 UDP 上实现更现代的可靠传输 |
| 游戏/物联网 | 动态/厂商 | 低延迟、高频小包、状态同步 |
DNS 是最经典的 UDP 例子。传统 DNS 的 UDP 响应长度限制在 512 字节以内,超出就置 TC 标志,客户端收到后改用 TCP 查询。后来 EDNS0 扩展允许客户端声明更大的接收缓冲,于是很多 DNS 响应可以到 4KB 左右。但大 UDP 包又会触发 IP 分片,而 DNS 查询经常要跨运营商,分片包在路径上的存活率并不理想。所以在设计类似协议时,要主动把响应控制在合理大小,避免依赖分片。
DHCP 的广播特性也让 UDP 不可替代——客户端在这个阶段没有 IP,也不可能建立 TCP 连接。NTP 则是另一种典型:客户端持续发小包对时,包丢了下一轮马上补上,重传意义不大。
3.2 NAT、防火墙和下三层网络对 UDP 的隐形限制
NAT 设备要为每个 UDP 流维护一个映射表。TCP 有 SYN/FIN 这些状态位,中间设备知道连接什么时候开始和结束;UDP 没有握手也没有挥手,NAT 只能靠超时来猜。一般 NAT 对 UDP 映射的保持时间从几十秒到几分钟不等,很多运营商设备在 30 到 180 秒之间。因此长连接型 UDP 应用必须自己做心跳保活,心跳间隔要小于 NAT 表项老化时间。否则一端还在发,NAT 表项已经被回收,外网服务再也收不到内网设备的包。
运营商 QoS 对 UDP 的限制比 TCP 更常见。在带宽拥塞时,很多设备会优先丢 UDP 大包,因为 UDP 没有回退机制,也不会像 TCP 那样因为丢包而降低发送速率,属于“最好欺负”的流量。这就解释了为什么有些视频应用高峰期画质下降,但测 TCP 下载速度完全正常。
企业防火墙的状态检测同样对 UDP 不友好。没有 SYN 握手意味着“新会话”的判定依据不足,很多防火墙对 UDP 的默认策略要么全放、要么靠应用层识别。跨公网部署 UDP 服务时,如果发现某个网络环境下包单向可达、回包丢失,先不要怀疑应用代码,很可能就是中间设备的 UDP 状态处理策略导致的。
3.3 QUIC:现代传输引擎为什么回到 UDP
QUIC 这个例子特别能说明 UDP 的定位。QUIC 在 UDP 之上重新实现了可靠传输、TLS 1.3、流多路复用、连接迁移和现代拥塞控制,等于在用户态把 TCP 的能力重新做了一遍,甚至做得比内核 TCP 更好。它为什么还要选择 UDP 做底座?因为 TCP 协议版本的升级受制于操作系统更新和中间设备兼容性,新特性部署周期以年计。UDP 只是一个透明的 payload 通道,应用层可以在里面定义自己的状态机,不需要等待设备厂商升级。
对普通开发者来说,这个趋势的启发是:面向公网的新应用,如果传输层需求比较复杂,与其从零自研“可靠 UDP”,不如先看 QUIC 是否满足需求。QUIC 已经给了你一套经过标准化、有丰富生态、带 TLS 加密的可靠 UDP 方案,复用它比重复造轮子划算得多。
4. 抓包看 UDP:报文、MTU 与分片下的真实链路
协议讲再多,不如抓一次包。UDP 抓包看起来很简单,因为头部就 8 字节,但一旦涉及分片、MTU 和校验和卸载,隐藏信息量很大。
4.1 tcpdump 与 Wireshark 实用过滤
抓 UDP 最常用的命令:
bash复制# 抓所有 UDP 包
tcpdump -i eth0 -nn -vvv udp
# 抓特定端口的 UDP 包
tcpdump -i eth0 -nn -vvv udp port 53
# 抓某个主机发来的 UDP 包
tcpdump -i eth0 -nn -vvv udp and host 192.0.2.10
# 抓 UDP 长度超过 1000 字节的包
tcpdump -i eth0 -nn -vvv 'udp[4:2] > 1000'
Wireshark 里同样可以过滤:
udp:所有 UDP 包udp.port == 53:特定端口udp.length > 1000:包的 UDP 长度字段udp.checksum_bad:校验和无效的包,结合 offload 情况判断
抓包界面里,Wireshark 会把 UDP 头拆成四行:源端口、目的端口、长度、校验和。如果长度字段的值和 IP 层总长度不一致,说明可能存在分片或者报文被篡改。遇到这种情况,要警惕链路中间设备对 UDP 的非标准处理。
4.2 MTU 与 IP 分片:“小水管”和大 UDP 包的冲突
MTU 是链路层能承载的最大帧大小。以太网 MTU 通常是 1500 字节,减去 IPv4 头 20 字节和 UDP 头 8 字节,IPv4 环境下单个 UDP 数据部分最好不要超过 1472 字节。IPv6 头是 40 字节,所以上限变成 1452 字节。如果是 PPPoE 拨号,链路 MTU 降为 1492,对应 IPv4 UDP payload 上限是 1464 字节。
| 链路类型 | 链路 MTU | IPv4 UDP payload 上限 | IPv6 UDP payload 上限 |
|---|---|---|---|
| 以太网 | 1500 | 1472 | 1452 |
| PPPoE | 1492 | 1464 | 1444 |
| 802.11 | 2304(部分) | 更高,但实际常协商到 1500 | 更高 |
超过这个上限时,IP 层会把 UDP 数据报切成多个分片。问题在于:分片后只有第一个分片带着 UDP 头,后续分片没有端口信息,防火墙和中间设备通常不会放行非首分片。路径 MTU 黑洞就是这样形成的——你发了一个大 UDP 包,分片被中间设备静默丢弃,而发送端又不一定收到 ICMP 错误,表现为“偶发大包丢失”。
在 IPv6 下,路由器不做分片,只有源节点能分片,并且要求路径 MTU 至少 1280。如果发送的包超过路径 MTU,路由器会回 ICMPv6 Packet Too Big,发送端要据此调整包大小。所以设计 UDP 应用时,把数据包控制在 1400 字节以内是很多工程的默认选择,虽然保守,但能绕开大量分片相关的问题。
4.3 一次真实 DNS 抓包如何解读
用 nslookup 查一个域名,抓包你会看到:
- 源端口是一个随机高端口号,目的端口 53。
- UDP 长度通常只有几十字节。
- 响应包从 53 端口回来,源端口变成 53,目的端口变成刚才的随机端口。
- 如果响应包超过了缓冲区限制,DNS 头里的 TC 位会被置 1,客户端收到后改用 TCP 重试。
这个过程中有几个 UDP 相关的细节:
- 源端口随机性在 NAT 环境很重要,端口号不同会让 NAT 建立不同的映射表项。
- DNS 客户端如果设置了 EDNS0,发送的查询包里会包含额外的 OPT 记录,声明自己能接收更大的 UDP 响应。但 EDNS0 也有兼容性问题,有些老旧设备对带 OPT 的 DNS 查询处理异常,导致解析超时。
- 抓包看到响应正常到达,但应用解析超时,优先检查是不是本地防火墙丢了回包,或者源端口被其他进程复用导致响应分发给了错误的 socket。
5. UDP 排障实录:从收不到包到缓冲区耗尽的完整排查链
这是整篇文章最偏实战的部分。UDP 出问题时的首要难点是“定位丢包的层级”。我强烈建议按“从下往上”的顺序排查,也就是先确认报文有没有到网卡,再看网卡有没有丢、协议栈有没有扔、socket 缓冲区有没有积压,最后才怀疑应用代码。
5.1 现场问题描述与核心现象
假设场景:服务 A 通过 UDP 向服务 B 推送事件,业务高峰时段 B 侧开始收包延迟和丢包。A 侧调用 sendto 全部成功,B 侧日志没有打印收到数据的记录。为什么 sendto 成功也会丢?
因为 UDP 的 sendto 成功只代表数据进入了内核的发送路径,不代表对端收到。在 UDP 的世界里,“发送成功”和“被接收”之间隔着网卡、交换机、路由器、防火墙、对端内核缓冲区,任何一环都能把包丢掉,而发送方完全无感知。理解这一点,是 UPD 排障的第一步。
5.2 从网卡到应用:逐层定位丢包的位置
第一步,在 B 侧抓包确认报文是否到达网卡:
bash复制tcpdump -i eth0 -nn -vvv udp port <服务端口>
如果这个命令什么都抓不到,先检查上游链路、路由、源端是否真的在发包,以及 B 侧防火墙是否在更上层直接丢包。如果抓到了,说明报文明明已经到了,问题出在更靠上的位置。
第二步,看网卡统计:
bash复制ethtool -S eth0 | grep -E 'rx_dropped|rx_missed|rx_fifo'
如果 rx_missed 和 rx_dropped 在持续增长,网卡接收环形缓冲区已经不够用了。可以扩大环形缓冲区:
bash复制ethtool -G eth0 rx 4096
还要观察软中断是否集中在一个 CPU 核上,多队列网卡的队列中断要尽量分散到多个核。单队列网卡可以打开 RPS(Receive Packet Steering),把收包处理分散到多个核。
第三步,看协议栈统计。Linux 下用:
bash复制netstat -su
重点关心几个计数:RcvbufErrors 增长,说明 socket 接收缓冲区满;InErrors 增长,可能是校验和错误、长度异常或目标端口没有监听;packets to unknown port 增长,说明有数据发到了没进程监听的端口,通常是端口号配置错误或者进程没起来。
第四步,看具体 socket:
bash复制ss -uanp | grep <端口>
看 Recv-Q 列。如果 Recv-Q 持续堆积,说明内核已经把包交给了 socket,但应用没有及时 recvfrom 消费。不要立刻骂代码,也许只是接收缓冲开小了,也许应用线程被某个阻塞操作卡住。如果 Recv-Q 一直是 0 但 InErrors 在涨,问题偏向校验和、包长或端口分发。
5.3 常用统计字段与修复手段对照表
| 现场现象 | 查看位置 | 可能原因 | 常用措施 |
|---|---|---|---|
| tcpdump 能看到包,应用收不到 | netstat -su 的 RcvbufErrors | socket 接收缓冲区满,消费太慢 | 调大 SO_RCVBUF,优化 recv 循环 |
| 发送方 SndbufErrors 持续增长 | netstat -su 的 SndbufErrors | 发送缓冲区满,发送速率超过承载能力 | 调大 SO_SNDBUF,削峰,检查对方是否在收 |
| 抓包看到 checksum invalid | Wireshark 提示 | 网卡 TSO/UFO checksum offload | 通常是正常现象,不必处理 |
| InErrors 增长、Recv-Q=0 | netstat -su | 校验和错误、包长异常、目的端口未监听 | 检查对端端口和网络中间设备 |
| 特定大 UDP 包总丢 | tcpdump | 路径 MTU 黑洞、IP 分片被丢弃 | 应用层控制包大小,避免分片 |
| 公网偶发丢包、统计全正常 | 抓包加历史对比 | 运营商 QoS 或中间策略 | 减小包体积,增加丢包恢复策略 |
这个表在排障时可以直接当速查手册用。我在多个项目里靠这套组合拳定位过问题,大多数 UDP 收不到的情况都不在应用逻辑,而在缓冲区和路径配置。
5.4 我踩过的三个 UDP 排障误区
第一个误区:抓到包就认为是应用问题。抓包只证明报文到达了网卡或者某个抓包点,不等于进了应用。网卡可能在 ring buffer 层就丢,协议栈也可能在处理中丢弃。要结合 ethtool、netstat 一起判断。
第二个误区:一遇到丢包就骂 UDP。UDP 不可靠是设计如此,但很多应用层的可靠需求根本没有转化为代码。如果业务需要可靠、顺序、不丢包,那就应该在应用层实现确认和重传,而不是赖协议。
第三个误区:忽略 SO_RCVBUF 的大小限制。Linux 下设置 SO_RCVBUF 时,内核会把你设置的值做乘法处理并受 net.core.rmem_max 约束。调大接收缓冲区前,先看系统参数:
bash复制sysctl net.core.rmem_max net.core.rmem_default
如果 rmem_max 太小,setsockopt 设得再大也会被截断。需要同步调大系统参数再改应用。
另外还有一个非常隐蔽的坑:UDP socket 做 connect。很多人不知道 UDP 也能 connect,这个调用不是建立真实连接,而是在内核里记录对端地址,让 sendto 简化为 send,让 recvfrom 简化为 recv,并且只接收来自这个地址的包。connect 过的 UDP socket 还能在对端端口不可达时返回 ECONNREFUSED。如果你的 UDP 服务不关心源地址过滤,connect 能显著减少内核查找开销;反过来,没 connect 的 socket 在某些系统上可能收不到 ICMP 端口不可达错误,排障时信息会少一块。
6. 当应用需要可靠:在 UDP 之上设计重传、FEC 与拥塞控制
很多团队最终都会走到这一步:UDP 快但丢包无法接受,所以要自己加可靠层。先说一句大实话:在 UDP 上做可靠的难度不亚于重新实现半个 TCP。自研之前,一定要想清楚业务真的需要什么。
6.1 什么场景真的值得自研可靠 UDP
值得做的场景大概是这几类:
- 私有协议,需要同时兼容广播或组播。
- 游戏帧同步,对瞬时延迟极致敏感,不能用 TCP 的队头阻塞拖累整条消息。
- 需要自定义拥塞控制算法,比如面对特定弱网环境做针对性调优。
- 不想被操作系统内核 TCP 版本限制,想在用户态快速迭代传输策略。
不值得做的场景是:公网大文件传输、数据库复制、事务系统。这些场景用 TCP 或者 QUIC 的成本远低于自研。能站在成熟协议肩膀上解决的问题,就不要重新发明轮子。
6.2 可靠化的四个基本件:序号、ACK、重传、去重排序
自研可靠 UDP,最基础的四件事绕不开:
- 序列号:每个数据报带上递增序号,用于丢包检测、去重和排序。序列号空间要足够大,并考虑回绕问题。比如 UDP 一秒钟发几千包,32 位序号能跑很久,但也要在协议里处理回绕后的比较,不能用普通减法判断大小。
- ACK:接收方要回确认包。最简单的累计 ACK 用“到目前为止连续收到的最大序号”表达;复杂一点可以用位图,类似 TCP 的 SACK,一次性告诉发送方哪几个位置丢了。累计 ACK 实现简单,但在窗口大、乱序严重时效率偏低。
- 重传:超时重传和快速重传都要考虑。超时时间不能固定,要参考 RTT 动态估算,避免重传风暴。快速重传的思路是:如果发送方收到几个更大的 ACK 序号,说明中间某些包丢了,立刻重传,不用傻等超时。
- 去重与排序:接收端需要滑动窗口缓冲乱序到达的包,按序列号排好后再交给上层。这一步处理不好,就会出现“数据到了顺序不对,应用处理逻辑全乱”的问题。
这里只是最核心的骨架。真要做,还得处理连接建立与超时关闭、ACK 丢失、半开连接、资源回收、缓冲上限等一系列边缘情况。这也是我说自研成本高的原因。
6.3 FEC 与抖动缓冲:从“能到”到“稳到”
很多实时音视频场景并不需要完全可靠,只需要“稳定”。这类场景的答案经常不是重传,而是 FEC 加抖动缓冲。
FEC(前向纠错)的思路是发送冗余数据。比如每 10 个数据包生成 2 个冗余包,接收端只要 12 个包里收到任意 10 个,就能恢复出全部信息。实现上可以用 Reed-Solomon 这类纠删码算法。这样即使网络丢了两个包,对端完全无感,不需要等待重传。代价是带宽增加约 20%,CPU 也要承担编解码开销。
抖动缓冲是接收端的另一招。网络抖动导致包到达时间忽早忽晚,如果应用收到一个放一个,播放就会一顿一顿。方案是接收端先把数据放进缓冲队列,延迟几十毫秒到一百毫秒再按序号播放。缓冲把抖动吞掉,换取平滑的播放体验。代价是端到端延迟变大,所以缓冲大小要按网络状况动态调节,太小容易卡顿,太大则延迟明显。
实际工程中,关键的控制帧用 ARQ 重传保证可靠,实时音视频数据用 FEC 加抖动缓冲保证流畅。把两类数据分开处理,比所有包都用同一种策略高明得多。
6.4 KCP、QUIC 与自研方案怎么选
如果不是非要从零写,选型就变得很重要。我基于项目经验给出一个简单的判断框架:
- QUIC:面向公网的新应用优先看它。标准化成熟,TLS 1.3 加密内建,流多路复用避免了 TCP 队头阻塞,还有很多现代拥塞控制算法。唯一的问题是部分老旧网络中间设备对 UDP/443 的 QoS 策略不友好,但整体上风险可控。
- KCP:游戏和帧同步场景很流行。它把 RT O调得比 TCP 激进,超时重传更快,非常适合延迟敏感的小包传输。缺点是拥塞控制相对简化,在公网大规模部署时没有 TCP/QUIC 的公平性保障。如果只在内网或可控网络上用,KCP 是个好选择。
- 自研:如果业务确实需要广播/组播、极度定制化的传输策略,或者纯粹为了学习传输协议设计,可以试。建议先定义好丢包恢复策略、拥塞控制策略、回绕处理、连接生命周期管理,再开始写代码。不要只写一个“发出去记一下,丢掉重传”的朴素模型就上线。
从我个人的实践体会来说,UDP 最吸引人的地方恰恰是它这种“什么都不承诺”的坦诚。它把可靠、顺序、流量控制、拥塞控制的选择权全部还给应用层,迫使你认真思考自己的业务到底需要什么。想清楚这一点,UDP 不仅不难用,反而是设计低延迟传输系统时最好的起点。到最后你会发现,理解 UDP 的过程,其实就是理解网络传输本质的过程。
