bigywater.tech
网络协议 · 3

MTU 与 MSS:一个数据包最大能多大

“MTU 1500”到底意味着一个包能装多少数据?MSS 是怎么在握手里协商出来的?为什么实际的段长又比 MSS 小一点?小包为什么那么贵?这篇用 tcpdump 和 TCP_CONNECTION_INFO 逐项量。

16344 / 1460
lo0 与 en0 上本机 SYN 里的 MSS(MTU − 40)
1388
访问 example.com 时每个数据段实际装的应用数据(对方通告 1400 − 12 字节时间戳)
134217 vs 4326
500 字节小块写与 1 MiB 大块写发送同样 64 MiB 所用的数据包数

MTU

实测 · 本机执行输出
  lo0: flags=8049<UP,LOOPBACK,RUNNING,MULTICAST> mtu 16384
  en0: flags=8863<UP,BROADCAST,SMART,RUNNING,SIMPLEX,MULTICAST> mtu 1500

MTU 是链路层一次能发的最大 IP 包:lo0 是 16384,真实网卡 en0 是 1500。

MSS

实测 · 本机执行输出
  lo0:  SYN 里 MSS=[16344]   SYN-ACK 里 MSS=[16344]   (MTU 16384 − 40 = 16344)
  en0:  SYN 里 MSS=[1460]   SYN-ACK 里 MSS=[1400]   (本机 MTU 1500 − 40 = 1460;example.com 一侧通告 1400,比 1460 小说明对方或路径上有设备钳制了 MSS)

TCP 在 SYN 里用 MSS 选项通告“我能收的最大段”,通常是 MTU − 20 − 20,双方取较小者。

实际段长

实测 · 本机执行输出
  lo0:  1 MiB 被切成 66 个数据段;最大 16332 字节(= MSS 16344 − 12 字节时间戳选项 = 16332);出现次数最多的段长 [(16332, 64), (1560, 1), (1768, 1)]
  en0:  发出的数据段共 30 个,最大 1388 字节;出现次数最多的段长 [(1388, 30)](通告的 MSS 减 12 字节时间戳选项)
  所以“MTU 1500”真正能装的应用数据是每包 1388 字节,而不是 1500

每个段还要给 TCP 选项让位:启用时间戳选项后,段长 = MSS − 12。lo0 上是 16332,en0 上访问 example.com 是 1388(对方通告了 1400,小于本机的 1460,说明路径上有设备把 MSS 钳小了)。

调不动的 MSS

实测 · 本机执行输出
  环回:设置 TCP_MAXSEG=512 后,连接上 getsockopt 读到 16332;抓到的 SYN 里 MSS=[16344]——SYN 没变
  en0 → example.com:SYN 里 MSS=[1460];客户端发出 15 个数据段,最大 1388 字节——和不设置时(第三节)一样是 1388,TCP_MAXSEG=512 没有生效;20080 字节的请求被切成 15 段
  结论:在这个 macOS 版本上,connect 之前的 TCP_MAXSEG=512 虽然 setsockopt 不报错,但读回来的仍是 16332,实际协商的 MSS 与发出的段长也都不受它影响;要在实验里改变段长,只能靠应用层写入大小加 TCP_NODELAY(下一节)

macOS 上 connect 之前设置 TCP_MAXSEG=512 不生效,SYN 与段长都不变。

小包的代价

实测 · 本机执行输出
  1 MiB 大块写: 64 MiB 收齐用时 10 ms(6104 MiB/s),发出 4326 个数据包,平均每包 15513 字节
  500 字节小块写 + NODELAY: 64 MiB 收齐用时 535 ms(120 MiB/s),发出 134217 个数据包,平均每包 500 字节

开销按包计:同样 64 MiB,500 字节一包要发 134217 个包、慢 50 多倍。这也是 Nagle 算法和 TSO / GSO 存在的原因(见后面的篇目)。

分步演示

MTU 与 MSS
本步的对象

    本文用到的工具与边界

    • 本机 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,数字每次运行会有小幅抖动。MSS 的值读自 SYN / SYN-ACK 的 TCP 选项(dpkt 解析 tcpdump -w 的 pcap);第三节的 en0 数据来自向 example.com:80 POST 256 KiB(服务端很快回复并关闭,所以只发出了 30 个段);TCP_MAXSEG 的尝试只在 macOS 上做过;第五节用大小写入模拟小 MSS,txpackets 来自发送方连接的 TCP_CONNECTION_INFO
    • 没有验证:Linux 上 TCP_MAXSEG 的行为;路径 MTU 发现与 ICMP(见 PMTUD 一篇);IPv6 的 MSS(MTU − 60);网卡 TSO 把大块拆包的位置;巨型帧(MTU 9000);example.com 一侧 MSS 钳制发生在哪一跳
    京ICP备2026067502号-1京公网安备11010502063487号