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