bigywater.tech
网络协议 · 29

UDP 的丢包、乱序与重复:本机能不能复现

UDP 丢了包、乱了序、收到两份,应用怎么知道?本机能不能复现这些现象?这篇用 netlab 造丢包和限速,用自己写的中继造乱序和重复,再对真实的公共 DNS 服务器连发 UDP,最后用一个最小的停等协议看“自己做可靠”要花多少代价。

900 / 1000
一口气发 1000 个 1000 字节的数据报进 500 Kbit/s 的管道,被丢掉的个数;收到的延迟中位 881 ms
13.3%
对公共 DNS A 每 20 ms 发一个 UDP 查询时应答的丢失率;公共 DNS B 是 0%
332 次
每个方向丢 40% 时,停等协议传 200 个数据报需要的重发次数,用了 10.32 s

丢包

实测 · 本机执行输出
  设置丢包 5%:发 2000 个,收到 1906 个(1906 个不同序号),丢失 94 个 = 4.7%;连续丢失的段 89 段,最长 2;长度分布 1个连丢×84,2个连丢×5;收到的序号是否严格递增:True
  设置丢包 20%:发 2000 个,收到 1620 个(1620 个不同序号),丢失 380 个 = 19.0%;连续丢失的段 305 段,最长 7;长度分布 1个连丢×246,2个连丢×48,3个连丢×9,4个连丢×1,7个连丢×1;收到的序号是否严格递增:True
  netlab 的丢包是独立随机丢的(没有突发),所以连续丢失的段大多长度为 1;真实的无线 / 拥塞丢包往往是成串的

netlab 能造随机丢包,实测与设置值接近;丢包不会改变收到的包的顺序,而且是各自独立的,单个丢失居多。

超过带宽

实测 · 本机执行输出
  突发:8 ms 内发完 1000 个(约 971 Mbit/s 的速度),收到 100 个,丢失 900 个;收到的延迟 最小 67 / 中位 881 / 最大 1695 ms;收到的序号范围 0~99
  慢发(每 20 ms 一个 1000 字节 = 400 Kbit/s,低于 500 Kbit/s 的管道):2.0 s 发完 100 个,收到 100 个,丢失 0 个;延迟 最小 66 / 中位 79 / 最大 80 ms(50 ms 延迟 + 1000 字节在 500 Kbit/s 上的发送时间 16 ms)
  超过管道带宽的突发会在队列里排队,队满以后后面的包直接丢弃(尾部丢弃),队里的包延迟也大幅增加;UDP 自己不会降速

UDP 不会因为管道窄就降速:突发 1000 个包进 500 Kbit/s 的管道,只通过前 100 个,延迟涨到几百毫秒;慢发则没有丢失。

乱序与重复(模拟)

实测 · 本机执行输出
  发 2000 个;中继复制了 40 份;接收端收到 2040 个(不同序号 2000 个,丢失 0 个)
  接收端检测:乱序(比已见到的最大序号小的新序号)1467 个,重复 40 个,最大“落后”距离 23;中继发出顺序里的真实值:乱序 1467、重复 40、最大落后 23;两者一致
  乱序占 73.4%——随机延迟 0~30 ms、每 1 ms 一个包,所以相邻的包经常互换位置

回环上不会自己出现,用中继人为制造后,按序号的检测算法与真实记录一致。

真实互联网

实测 · 本机执行输出
  公共 DNS A,慢发 每 20 ms 一个:发 300 个,收到 260 个不同的应答,丢失 40 个(13.3%),重复 0 个,乱序 0 个
  公共 DNS A,突发 不留间隔:发 300 个,收到 285 个不同的应答,丢失 15 个(5.0%),重复 0 个,乱序 22 个(最大落后 46)
  公共 DNS B,慢发 每 20 ms 一个:发 300 个,收到 300 个不同的应答,丢失 0 个(0.0%),重复 0 个,乱序 1 个(最大落后 1)
  公共 DNS B,突发 不留间隔:发 300 个,收到 300 个不同的应答,丢失 0 个(0.0%),重复 0 个,乱序 5 个(最大落后 12)

向两个公共 DNS 连发 UDP:一个丢 13.3%,一个不丢;突发时能看到几个到几十个的乱序,没有重复。

可靠性要自己做

实测 · 本机执行输出
  每个方向丢 0%:发了 200 次(200 个数据报,重发 0 次),服务端收到 200 个不同序号,总用时 0.02 s;理论平均发送次数 200/(1−0.0)² = 200 次(计算值)
  每个方向丢 5%:发了 225 次(200 个数据报,重发 25 次),服务端收到 200 个不同序号,总用时 0.80 s;理论平均发送次数 200/(1−0.05)² = 222 次(计算值)
  每个方向丢 20%:发了 287 次(200 个数据报,重发 87 次),服务端收到 200 个不同序号,总用时 2.72 s;理论平均发送次数 200/(1−0.2)² = 312 次(计算值)
  每个方向丢 40%:发了 532 次(200 个数据报,重发 332 次),服务端收到 200 个不同序号,总用时 10.32 s;理论平均发送次数 200/(1−0.4)² = 556 次(计算值)

停等协议把丢包变成重发,丢得越多越慢;真实的实现(滑动窗口、快速重传、拥塞控制)在 TCP 与 QUIC 的部分展开。

分步演示

UDP 丢包乱序重复
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64(Apple 芯片)/ Python 3.14.8 / tcpdump(libpcap)/ scapy 2.8 / dpkt / dnspython / h2 / aioquic / OpenSSL / curl 8.7.1;抓包在 lo0(环回)上做,少数实验用真实网卡 en0(这台机器上 en0 是 Wi-Fi,tcpdump 看到的是系统转成以太网格式的帧,所以文中的“以太网头”都指这个 14 字节的头)或访问公网(会在文章里注明);需要丢包 / 延迟 / 限速时,用一个只作用于 lo0 端口 9400-9499 的 dummynet 辅助脚本(netlab);所有服务端都是本机自己起的进程;时间戳取自 tcpdump 的 -tt,数字每次运行会有小幅抖动。第一、二、五节由 netlab 在 lo0 的 9400~9499 端口注入丢包与限速;第三节是用户态中继的模拟;第四节对公共 DNS A(1.1.1.1)和 B(8.8.8.8)各发 600 个查询(只发 example.com 的 A 记录),结果会随当时的无线与路径环境变化
    • 没有验证:公共 DNS A 丢包的原因(Wi-Fi、路径、服务器限速);更多样本下的乱序概率;真实网络上成串(突发)丢包的统计;重复数据报在真实网络中出现的条件;滑动窗口或前向纠错对 UDP 可靠性的改进
    京ICP备2026067502号-1京公网安备11010502063487号