bigywater.tech
网络协议 · 48

SACK:选择性确认让重传更精确

累积确认只能告诉发送方“X 之前都收到了”,洞之后有没有到呢?SACK 选项把“这一段我收到了”也告诉发送方。这篇在丢包的回环上解码 ACK 里的 SACK 块,核对重传是不是正好从洞开始、只补丢掉的段。

16332 字节
SACK 块里报告的、洞之后已经收到的一个整段;发送方因此不必重传它
3 / 3
3 个洞里发送方重传从洞开头开始的段的个数
72 / 72
带 SACK 的 ACK 里 SACK 块位于累积确认号之上(中间有洞)的比例

开关

实测 · 本机执行输出
  net.inet.tcp.sack: 1
  net.inet.tcp.sack_maxholes: 128
  net.inet.tcp.sack_globalmaxholes: 65536
  (net.inet.tcp.sack_globalholes 在本机不存在)
  SYN 里的 SACK-permitted(kind 4,2 字节)是双方协商的:两边都带才能在 ACK 里使用 SACK 选项(kind 5)——w37 / w38 已经看到本机和 example.com 都带了

net.inet.tcp.sack = 1,每个连接最多记录 128 个洞,全局上限 65536 个。

SACK 块

实测 · 本机执行输出
  传输 4 MiB 用了 0.82 秒;发送方 tcp_info:发出 266 个包,重传 4 个(65328 字节,占发送字节的 1.53%);设定的丢包率是 3%
  接收方发出的纯 ACK(抓到的记录,含 dummynet 重复)164 条,其中带 SACK 选项的 72 条;SACK 块个数的分布:1 块 × 72
  一个例子:累积确认号(相对)= 212317,SACK 块(相对序号):[228649, 244981)  长 16332 字节
  所有带 SACK 的 ACK 里,SACK 块位于累积确认号之上(中间有洞)的 72 / 72 条;块的最大个数 1(TCP 选项空间只有 40 字节,带时间戳时头部 12 字节,每块 8 字节,所以最多 3 块)

SYN 里协商 SACK-permitted 之后,接收方的 ACK 可以带最多 3 个(有时间戳时)块,每块是已收到的一段 [左, 右)。累积确认号停在第一个洞的开头。

重传

实测 · 本机执行输出
  带 SACK 的 ACK 指向的“第一个洞”(去重后)有 3 个,其中发送方确实重传了从这个序号开始的段的有 3 个;发送方被重传过的段一共 3 个
  被重传的段大小:{16332: 3}——每个重传的都是丢掉的那个整段,没有把洞之后已经收到的数据重发一遍
  对比“累积确认、没有 SACK”的做法(只能从洞开始往后重传,或者等超时一个一个试):本次发送方实际重传 65328 字节 = 4 个完整的段;洞之后已经被接收方收到的字节有很多(SACK 块里报告了),发送方因此知道不必重传它们

发送方根据 SACK 知道哪些段已到,只重传丢掉的整段(这次是 3 个洞、重传 3 个整段)。

DSACK

实测 · 本机执行输出
  第一个 SACK 块的左边缘低于累积确认号的 ACK(DSACK 报告)0 条;这次没有出现

用第一个块报告重复数据,能发现不必要的重传。这次没出现。

分步演示

SACK
本步的对象

    本文用到的工具与边界

    • 本机 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 上,丢包与延迟由 netlab 注入;抓包里 dummynet 会让同一个包被抓到多份,按相隔 30 ms 去重分簇;这次实验的样本很小(3 个洞、4 个重传包),之前用 5 ms 延迟的一次实验里还见过 2~3 块的 SACK 选项,但最终保留的是最后一次运行的数字
    • 没有验证:SACK 恢复期间的发送速率与拥塞窗口(见拥塞控制);关闭 SACK 后的对照(需要 root 改 net.inet.tcp.sack);DSACK 的实际出现(需要触发不必要的重传);SACK 块数量上限(3 块)在没有时间戳时是否变成 4 块
    京ICP备2026067502号-1京公网安备11010502063487号