想减少小包有三条路:TCP_NODELAY 让小包马上发、Nagle 帮你合并、或者像 Linux 的 TCP_CORK 那样先塞住再一起发。这篇在 macOS 上把这些手段逐个实测:选项读回是什么、分两次 write 到底能不能被合并、NOPUSH 这个软木塞到底在什么时候放气。
新连接上 getsockopt(TCP_NODELAY) = 0(客户端)、0(服务端 accept 出来的) 客户端 setsockopt 后读回 = 4 在监听套接字上先设 TCP_NODELAY=1,accept 出来的连接读回 = 4(非 0 = 已置位,即被继承;macOS 读回的不是 1 而是内部标志位的值) socket 模块里有 TCP_CORK: False;有 TCP_NOPUSH: False(值 4);有 MSG_MORE: False TCP_NOPUSH 默认 = 0;设成 1 后读回 4096
TCP_NODELAY 默认关;macOS 读回的不是 1 而是标志位值 4,监听套接字上设置会被 accept 的连接继承。Python 的 socket 模块这里没有 TCP_CORK 和 MSG_MORE。
先 write 头、再 write 体 Nagle 开 :数据段 400 个(200 条消息) 先 write 头、再 write 体 TCP_NODELAY:数据段 400 个(200 条消息) 拼成一个缓冲区一次 send Nagle 开 :数据段 200 个(200 条消息) 拼成一个缓冲区一次 send TCP_NODELAY:数据段 200 个(200 条消息) sendmsg([头, 体]) 一次 Nagle 开 :数据段 200 个(200 条消息) sendmsg([头, 体]) 一次 TCP_NODELAY:数据段 200 个(200 条消息) 分两次 write 的写法,不论 Nagle 开还是 TCP_NODELAY,每条消息都是两个包:回环上第一个包的 ACK 几十微秒就回来了,Nagle 没有机会把第二次 write 合并进去;只有应用自己一次发出(拼接或 sendmsg)才是一条消息一个包
头和体分两次 write,不论 Nagle 开还是 TCP_NODELAY,回环上每条消息都是两个包。拼成一个缓冲区或用 sendmsg 一次交给内核,才是一条消息一个包。
Nagle 开(默认) + NOPUSH:
写 3×100 字节(NOPUSH 开)后 0.5 秒:线上累计数据段 0 个,接收方累计读到 0 字节
清除 NOPUSH 后 0.5 秒:线上累计数据段 0 个,接收方累计读到 0 字节
再写 1 字节后 0.5 秒:线上累计数据段 1 个,接收方累计读到 301 字节
shutdown(SHUT_WR) 后 0.5 秒:线上累计数据段 1 个,接收方累计读到 301 字节
各数据段载荷:[301]
TCP_NODELAY + NOPUSH:
写 3×100 字节(NOPUSH 开)后 0.5 秒:线上累计数据段 0 个,接收方累计读到 0 字节
清除 NOPUSH 后 0.5 秒:线上累计数据段 0 个,接收方累计读到 0 字节
再写 1 字节后 0.5 秒:线上累计数据段 1 个,接收方累计读到 301 字节
shutdown(SHUT_WR) 后 0.5 秒:线上累计数据段 1 个,接收方累计读到 301 字节
各数据段载荷:[301]开着 NOPUSH,小数据一个包都不发;在这台 macOS 上清除选项并不会把攒着的数据冲出去,下一次写入才会连同新数据一起发出。
接收方在 1.5 秒内读到 32664 字节(共写 40000);线上数据段载荷:[16332, 16332] 40000 字节 = 2 个满段 (32664 字节) + 零头 7336 字节;零头没有发出 清除 NOPUSH 后 0.5 秒,接收方没有再读到数据
NOPUSH 开着写 40000 字节,满 MSS 的两个段正常发出,只有 7336 字节的尾巴被扣住。