bigywater.tech
网络协议 · 28

UDP:8 字节头、校验和与“无连接”的真正含义

UDP 头只有 8 个字节,它到底少了什么?“无连接”具体省掉了多少包和多少往返?报文边界是怎么保留的,缓冲区太小会发生什么?这篇在 lo0 上解码真实的 UDP 头,并用 netlab 加延迟实测 UDP 与 TCP 一问一答的差别。

8 字节
UDP 头大小:源端口、目的端口、长度、校验和各 2 字节
1 个往返 对 2 个往返
往返 40 ms 时,UDP 一问一答 40.7 ms,TCP 含建立连接 81.2 ms
65507
UDP 数据报的最大载荷:65535 − 20 − 8;macOS 默认只允许发 9216,调大 SO_SNDBUF 才行

UDP 头

实测 · 本机执行输出
  头 df01 c407 000d fe20:源端口 57089(= 发送套接字的 57089:True),目的端口 50183,长度 13 = 8 + 载荷 5,校验和字段 0xfe20;IP 总长 33 = 20 + 13
  头 df01 c407 0008 fe1b:源端口 57089(= 发送套接字的 57089:True),目的端口 50183,长度 8 = 8 + 载荷 0,校验和字段 0xfe1b;IP 总长 28 = 20 + 8
  头 df01 c407 03f0 0204:源端口 57089(= 发送套接字的 57089:True),目的端口 50183,长度 1008 = 8 + 载荷 1000,校验和字段 0x0204;IP 总长 1028 = 20 + 1008
  载荷为 0 字节的数据报也是合法的:UDP 长度 8,对方 recvfrom 会收到一个空的数据报
  接收方三次 recvfrom 得到的长度:[5, 0, 1000]——每次 recvfrom 对应发送方的一次 sendto(UDP 保留报文边界)

只有源端口、目的端口、长度、校验和,各 2 字节。长度含 UDP 头本身,所以 0 字节载荷的数据报长度是 8,也是合法的。

报文边界

实测 · 本机执行输出
  连发 10、20、30 字节三个数据报:接收方 3 次 recvfrom 分别拿到 [10, 20, 30] 字节
  再发一个 20 字节的数据报,接收方只给 5 字节的缓冲区:recvmsg 得到 5 字节,MSG_TRUNC 标志 置位;再收一次:什么也没有——剩下的 15 字节被丢掉了
  TCP 同样发 10、20、30 字节:接收方一次 recv 拿到 60 字节(三次发送被合并成一个字节流,边界没有了)

每次 sendto 对应接收方的一次 recvfrom;缓冲区小于数据报时多出的部分被丢掉(MSG_TRUNC)。TCP 是字节流,会把几次发送合并。

无连接

实测 · 本机执行输出
  一次 4 字节的请求 / 应答:UDP 2 个包(64 字节 IP 包);TCP(连接 + 请求 + 应答 + 关闭)12 个包(656 字节)
  没有延迟:UDP 一问一答 70 µs、TCP(含建立连接)207 µs
  单向 20 ms(往返 40 ms):UDP 一问一答 40.7 ms(≈ 1 个往返)、TCP 含建立连接 81.2 ms(≈ 2 个往返:先三次握手,再请求应答)

一问一答 UDP 只要 2 个包、1 个往返;TCP 要 12 个包、2 个往返。代价是没有可靠性,也没有连接状态。

端口

实测 · 本机执行输出
  net.inet.ip.portrange.first: 49152;net.inet.ip.portrange.last: 65535;net.inet.ip.portrange.hifirst: 49152;net.inet.ip.portrange.hilast: 65535
  1000 个不指定端口的 UDP 套接字:端口范围 49164 ~ 65496,互不相同的 1000 个,相邻两个恰好 +1 的只有 0 个(随机分配,不是顺序递增)
  第一个套接字绑 0.0.0.0:50625;再绑 0.0.0.0 同一端口:失败(Address already in use);再绑 127.0.0.1 同一端口:失败(Address already in use);SO_REUSEADDR 后再绑 0.0.0.0:失败(Address already in use);SO_REUSEPORT 后再绑 0.0.0.0:成功

临时端口 49152~65535 随机分配(1000 个套接字没有一对连号);同一端口重复绑定默认失败,SO_REUSEPORT 才可以。

大小上限

实测 · 本机执行输出
  net.inet.udp.maxdgram: 9216
  net.inet.udp.recvspace: 786896
  net.inet.udp.checksum: 1
  默认发送缓冲区:9216 字节:发送成功;9217 字节:Message too long;65507 字节:Message too long;65508 字节:Message too long
  调大 SO_SNDBUF 到 1 MiB 后:9216 字节:发送成功;9217 字节:发送成功;65507 字节:发送成功;65508 字节:Message too long

数据报上限 65507,但系统默认只允许发 9216 字节,调大发送缓冲区才能发更大的。

分步演示

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,数字每次运行会有小幅抖动。第一、二、四、五节在回环上做(lo0 上的 UDP 校验和字段是占位值,见第 17 篇);第三节的延迟由 netlab 在 lo0 的 9400~9499 端口上注入(每个方向 20 ms);时间数字为中位数,无延迟时只看量级
    • 没有验证:真实网络上(en0 以外)的 UDP 往返;UDP-Lite(部分校验和);UDP 在 IPv6 里校验和不能省略的规则;SO_REUSEPORT 在多个套接字间如何分流数据报(见广播与组播两篇);不同 macOS 版本的 maxdgram 默认值
    京ICP备2026067502号-1京公网安备11010502063487号