bigywater.tech
网络协议 · 45

序号与确认:字节流如何被编号、累积确认如何工作

序号和确认号到底在数什么?为什么 ACK 可以不是一包一回?32 位序号发满 4 GiB 之后会怎样?这篇逐个包核对序号和确认号的算术,并在回环上真的发 4.5 GiB 看序号回绕。

+1
SYN 和 FIN 各占一个序号:确认号 = 对方发过的字节数 + 2
196 个段 / 45 个 ACK
下载 4 MiB 时服务端的数据段数和客户端回的纯 ACK 数:平均每 4.4 个段回 1 个
4294960721 → 9757
lo0 上发 4.5 GiB 时抓到的序号回绕(2^32 = 4294967296)

序号和确认号

实测 · 本机执行输出
  客户端 ISN = 1936706479,服务端 ISN = 2047459890(下面都用相对值:序号减去各自的 ISN)
    #  方向   标志      相对序号  载荷  序号+载荷(下一个序号)  相对确认号
     1  客户端→  SYN             0     0           1                   0
     2  ←服务端  ACK+SYN         0     0           1                   1
     3  客户端→  ACK             1     0           1                   1
     4  ←服务端  ACK             1     0           1                   1
     5  客户端→  ACK+PSH         1   100         101                   1
     6  ←服务端  ACK             1     0           1                 101
     7  客户端→  ACK+PSH       101   250         351                   1
     8  ←服务端  ACK             1     0           1                 351
     9  客户端→  ACK+PSH       351    30         381                   1
    10  ←服务端  ACK             1     0           1                 381
    11  客户端→  ACK+FIN       381     0         382                   1
    12  ←服务端  ACK             1     0           1                 382
    13  ←服务端  ACK+FIN         1     0           2                 382
    14  客户端→  ACK           382     0         382                   2
  3 个数据段的“序号 + 载荷”都出现在随后服务端的确认号里:3 / 3;最后一个确认号 382 = 1(SYN)+ 100 + 250 + 30 = 381(再加 FIN 占的 1 个序号就是 382)
  规则:序号标的是这个段的第一个字节在字节流里的位置;确认号是“我期望收到的下一个字节的位置”;SYN 和 FIN 各占 1 个序号,所以握手之后的第一个数据字节序号是 ISN + 1

序号 = 本段第一个字节在字节流里的位置;确认号 = 期望收到的下一个字节的位置。SYN 和 FIN 各占 1 个序号。

累积确认

实测 · 本机执行输出
  下载 4194304 字节:服务端发了 196 个数据段(最大载荷 16332 字节),客户端回了 45 个纯 ACK;平均每 4.4 个数据段回 1 个 ACK
  相邻两个 ACK 之间确认号增加的字节数:最小 16332 / 中位 32664 / 最大 571620;总确认 4194305 字节
  一个 ACK 确认的是“从头到这里的所有字节”(累积确认):中间少回几个 ACK 不会丢信息;ACK 越稀,开销越小,但发送方等 ACK 的时间可能更久

一个 ACK 确认从头到这里的所有字节,所以接收方可以隔几个段才回一个 ACK。

回绕

实测 · 本机执行输出
  传输 4.50 GiB 用时 0.9 秒(4934 MiB/s);抓到服务端数据段 263593 个(其余因抓包丢包 / 去重没有统计)
  服务端 ISN = 1355246696;相对序号的最大值 4294966465;绝对序号在 1 处从接近 2^32 回绕到小数:第 162573 个段后 4294960721 → 9757
  回绕处:前一个段序号 4294960721(距 2^32 还差 6575),下一个段序号 9757 = (前一个序号 + 它的载荷) mod 2^32 = 9757:True
  所以比较两个序号“谁在前”不能直接用大小比较,要看它们的差是否小于 2^31(TCP 实现里的 SEQ_LT 宏);时间戳选项的 PAWS(第 63 篇)就是为了在高速网络里防止回绕后的旧段被当成新段

序号是 32 位的,发满 4 GiB 会回绕,比较序号要用模 2^32 的差。高速网络里回绕很快,所以需要时间戳选项的 PAWS。

两个方向

实测 · 本机执行输出
  客户端:SYN 序号 737984981 → 发了 1000 字节 → FIN 的序号 737985982 = ISN + 1 + 1000 = 737985982:True;服务端最后确认客户端的 ACK 号 = 1002 = 1(SYN)+ 1000 + 1(FIN) = 1002
  服务端:SYN-ACK 序号 2038976298 → 发了 500 字节 → FIN 的序号 2038976799 = ISN + 1 + 500 = 2038976799:True;客户端最后确认服务端的 ACK 号 = 502 = 1 + 500 + 1 = 502
  一个连接里每个方向各有自己独立的序号空间:一个方向发了多少字节,和另一个方向无关

每个方向各有独立的序号空间。

分步演示

序号与确认号
本步的对象

    本文用到的工具与边界

    • 本机 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 上做;第三节的 4.5 GiB 传输只抓每个包的头部(snaplen 96),抓包丢掉了一部分包(抓到 263593 个数据段,实际更多),但回绕点在抓到的包里;序号取相对值时减去各自的 ISN
    • 没有验证:序号回绕在 en0 真实网络上多久发生一次;乱序时确认号的变化(见快速重传与 SACK);窗口与序号的关系(见滑动窗口);序号随机化之外的 ISN 攻击(RFC 6528)
    京ICP备2026067502号-1京公网安备11010502063487号