“MTU 1500”到底意味着一个包能装多少数据?MSS 是怎么在握手里协商出来的?为什么实际的段长又比 MSS 小一点?小包为什么那么贵?这篇用 tcpdump 和 TCP_CONNECTION_INFO 逐项量。
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。
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 钳小了)。
环回:设置 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 存在的原因(见后面的篇目)。