Skip to content

第11章 Acknowledgment Number:累计确认

上一章我们为每个字节分配了序列号(Sequence Number)。本章我们将视角切换到接收方,解答一个更为关键的问题:接收方如何用一个 32 位的数字告诉发送方,当前连续字节流已经接收到什么位置了?

试着把 Ack 读成一句完整的话

只有当报文段(Segment)的 ACK 标志位置为 1 时,Acknowledgment Number(确认号)才具有实际意义。假设报文中的确认号为 X,你可以把它翻译成这样一句大白话:

“我期待你接下来发送序列号为 X 的字节;在此之前的所有连续数据,我都已经妥收了。”

也就是说,Ack = X 永远指向接收方“下一个期待的位置”。如果用左闭右开的区间来表示,接收方已经连续接收的数据范围就是 [,X),而右端点 X 本身正是它正在等待的数据。

举个例子:客户端发送了一个 Seq = 1001, TCP Len = 400 的报文段。这 400 字节的数据占据了序列号空间 [1001,1401)。服务端完整收到后,会回复一条 Ack = 1401 的报文。客户端一旦看到这个值,就立刻明白:序列号小于 1401 的这部分连续数据,已经安全送达并被确认。

让我们回到第三篇开头的 HELLO TCP\n 抓包样本:该报文的 Seq = 0x6a1b2c3e, Len = 10。服务端在完整收到这 10 个字节后,它期待的下一个序列号理应是:

0x6a1b2c3e+10=0x6a1b2c48

因此,服务端会在反向报文中使用 Ack = 0x6a1b2c48 来确认这 10 个字节。值得注意的是,该样本本身还携带了 Ack = 0x10203041,这意味着客户端在发送数据的同时,也在告诉服务端:“我正在等待你发送序列号为 0x10203041 的数据”。就这样,一条数据报文不仅推进了本端的 Seq 空间,还顺带汇报了反方向的 Ack 进度。

TCP 连接拥有两个相互独立的序列号空间。在同一个报文中,Seq 描述的是“本端的发送进度”,而 Ack 描述的是“对端的发送进度”。在阅读双向抓包文件时,你可以将它们拆解成两句独立的话:

  • Seq = 3001, Len = 5:我正在发送从 3001 开始的 5 个字节;
  • Ack = 9013:同时,我期待你接下来从 9013 开始发送数据。

这也就解释了为什么同一个 TCP 报文既能携带业务数据,又能携带确认信息。由于 TCP 的双向传输是独立推进的,ACK 往往会“搭便车”,随着反向的数据流一起发送,这种机制在计算机网络中被称为“捎带确认”(Piggybacking)。

“累计确认”的核心:只认连续前缀

假设服务端当前期待接收 Seq = 1401,但网络中发生了乱序,数据到达的顺序如下:

到达次序收到的数据区间当前连续范围可发送的累计 Ack
1[1701,1901)仍停在 14011401
2[1401,1701)已连成 [1401,1901)1901

先到达的 200 字节属于靠后的序列号,这意味着前面还有 300 字节的“空洞”(缺口)。此时,TCP 接收端会将这批乱序数据暂存到接收缓冲区中,但回复的确认号依然死死盯住那个缺口,保持 Ack = 1401 不变。直到缺失的数据段姗姗来迟,这两块数据终于无缝拼成了一个连续的前缀,确认号这才一次性“跃迁”到 1901。

这就是“累计确认”(Cumulative Acknowledgment)的精髓:一个数值较大的 Ack,可以直接覆盖并确认该数值之前所有连续到达的字节。接收方完全可以用一个 Ack = 1901 的报文,一次性确认多个报文段。结合延迟确认(Delayed ACK)、捎带确认以及操作系统的调度策略,网络链路上纯 ACK 报文的数量会被大幅减少。你需要记住的是:TCP 的接收进度永远由 Ack 的数值决定,而与收到的报文段数量无关。

如果连接启用了 SACK(Selective Acknowledgment,选择性确认),那么在第一步收到乱序数据时,TCP 报头中还可能携带 SACK = [1701,1901) 选项。这样一来,累计 Ack 继续停留在 1401 催促缺失的数据,而 SACK 则向发送方精准汇报连续前缀之外已经被暂存的“数据孤岛”。关于这两个选项的协同机制,我们会在第 14 章详细展开。

纯 ACK、SYN 与 FIN 的确认规则

在 TCP 的世界里,不仅实际的数据字节会占用序列号空间,SYN 和 FIN 这两个控制标志同样会各占用一个序列号。对于一个起始序列号为 S、携带 L 字节数据的报文段,其推进后的下一个位置计算公式为:

Snext=S+L+ISYN+IFIN(mod232)

在这里,如果报文段携带了 SYN 或 FIN 标志,对应的指示量 I 就取 1,否则取 0。而其余的标志位(如 ACK、PSH、RST、URG、ECE、CWR 和 AE 等)都属于“不占地盘”的标志,它们本身不会让序列号向前推进。

为了让你直观理解,我们来看一组使用相对简单原始序列号(Raw Sequence Number)的交互推演:

方向FlagsSeqTCP LenAck推理过程
客户端 → 服务端SYN30000SYN 本身占据了序列号 3000
服务端 → 客户端SYN, ACK900003001确认 SYN,服务端期待客户端接下来的 3001
客户端 → 服务端ACK300109001纯 ACK,不占空间;客户端期待服务端的 9001
客户端 → 服务端ACK, PSH300159001包含 5 字节数据,占用区间 [3001,3006)
服务端 → 客户端ACK, PSH9001123006发送 12 字节数据,并捎带确认了客户端的 5 字节
客户端 → 服务端ACK300609013纯 ACK,确认了服务端的 12 字节数据

注意观察第三行和最后一行,它们都是“纯 ACK”(不含应用层数据,也没有 SYN/FIN 标志)。纯 ACK 虽然会在 TCP 头部携带当前的 Seq,但它们并不消耗序列号。如果本端暂时没有数据要发送,连续发出的多个纯 ACK 报文会使用完全相同的 Seq。此外,纯 ACK 报文本身是不具备重传机制的。如果一个纯 ACK 在网络中丢失,TCP 并不慌张,它完全可以依靠后续到达的 ACK、反向的捎带确认,或是发送方触发超时重传来继续推进状态。

FIN 标志的确认逻辑与 SYN 如出一辙。假设服务端发送了一个 Seq = 9013, Len = 20, FIN = 1 的报文段,20 字节的数据首先占用了 [9013,9033) 这个区间,紧接着 FIN 标志再额外占用一个位置。因此,客户端将该报文完整收齐并处理完毕后,回复的确认号将是 Ack = 9034

诊断实战:建立双向“对账本”手算 Ack

面对成百上千行的长篇抓包文件时,我们很容易迷失在 Seq 和 Ack 的数字海洋中。一个非常实用且稳定的分析技巧是,为连接的两个方向各建立一行“对账本”:

发送方向下一个期待位置(连续前缀)已暂存的乱序区间最近观察到的对端 Ack
客户端 → 服务端
服务端 → 客户端

在排查疑难杂症时,逐帧读取报文段并按以下标准动作进行“记账”:

  1. 辨明方向:先根据源 IP、目标 IP 以及端口四元组,确定当前报文是哪个方向发出的;
  2. 计算跨度:利用公式 tcp.len + SYN + FIN 计算出该报文段究竟占用了多少序列号空间;
  3. 推演进度
    • 如果该报文的起始 Seq 刚好等于账本里的“下一个期待位置”,那么将连续右边界向前推进;
    • 如果 Seq 大于期待位置(说明发生了乱序),则先将其记录到“已暂存的乱序区间”中;
  4. 验证确认:提取报文中的 Ack 字段,并与反方向账本上的“连续右边界”进行交叉比对;
  5. 处理重叠:遇到重传或数据重叠的区间时,这只能作为数据覆盖的补充证据,不影响已经推进的连续进度;
  6. 处理回绕:当序列号接近最大值 0xffffffff 时,记得对 232 取模运算,实现序列号的回绕。

这套“双向对账”法还有一个极其强大的隐藏功能——发现抓包文件是否缺帧。如果你在分析时发现某个报文的 Ack 突然“大步跨越”了一段在抓包列表里从未出现过的数据区间,这就意味着接收端在真实的网络中已经收到了这些字节,仅仅是你的抓包工具(或者所处的网络镜像位置)漏抓了对应的报文。此时,你需要将“线上的真实确认事实”与“本地抓包文件的完整性问题”区分开来,独立记录。

切忌混淆:TCP 的 Ack 不等于业务成功

我们在排障时经常听到开发同学抱怨:“客户端的抓包里明明看到服务端回复了 ACK,为什么数据库里却没有这条记录?是不是网络吃掉包了?”

这里必须厘清一个核心概念:TCP 的累计确认(Ack)只代表对端操作系统的 TCP 协议栈完整接收了连续的字节流,但这与业务处理结果完全是处于不同层次的两码事。

数据的读取、反序列化、业务逻辑校验、落库以及调用下游服务,都发生在 TCP 确认之后的应用层。一条清晰的 HTTP 请求时间线其实是这样的:

  1. 发送数据:客户端通过 Socket 发送 500 字节的 HTTP 请求;
  2. 传输确认:服务端的内核 TCP 协议栈收齐了这 500 个连续字节,向客户端回复累计 Ack;
  3. 应用读取:服务端的业务进程醒来,通过 read() 系统调用从 Socket 缓冲区中把数据读入用户态内存;
  4. 业务处理:应用代码解析 JSON,发现某个必填字段格式有误,抛出校验异常;
  5. 业务响应:服务端构造并发送一条 HTTP 422 Unprocessable Content 的业务错误响应。

在这个过程中,第 2 步仅仅能证明“数据已经安全送达服务端的机器”,而只有第 5 步才代表了“本次业务的真实处理结果”。对于那些要求极高可靠性和持久化的分布式系统,架构师往往会在应用层设计专门的 Request ID、事务状态机和应用层 ACK(例如消息队列的 ACK)。客户端需要据此去区分当前到底处于“传输已确认”、“请求已受理”还是“事务已提交”的阶段。

总结一下:在抓包文件中看到了完整的请求数据和对应的 TCP ACK,你只能敲定传输层没有任何问题。若要判断业务是否真正成功,你必须结合业务层响应报文、应用运行日志以及数据库的持久化记录综合研判。当遭遇主机突然掉电崩溃、进程 OOM 退出或是数据库事务意外回滚等极端场景时,传输层和应用层给出的“证据”很可能截然相反。

受控实验:对照抓包文件手算 Ack

我们不妨亲自动手,在本地运行一段简单的 Python 脚本,并用抓包工具验证我们的“对账”逻辑。

请在 Windows 的 Wireshark 中选择 Npcap Loopback Adapter(本地环回网卡)进行抓包,我们使用 39011 作为测试端口。首先在终端 A 启动一个简单的一次性服务端:

powershell
python -c "import socket,time; l=socket.socket(); l.bind(('127.0.0.1',39011)); l.listen(1); c,_=l.accept(); f=c.makefile('rb'); time.sleep(1); d=f.read(700); f.close(); print('server bytes:',len(d)); c.sendall(b'OK'); c.close(); l.close()"

接着,在终端 B 启动客户端,故意分两次写入总共 700 字节的数据(先发 400 字节,停顿一小会儿,再发 300 字节):

powershell
python -c "import socket,time; c=socket.create_connection(('127.0.0.1',39011)); c.sendall(b'A'*400); time.sleep(0.2); c.sendall(b'B'*300); f=c.makefile('rb'); print(f.read(2)); f.close(); c.close()"

运行完毕后停止抓包,在 Wireshark 中输入显示过滤器:

text
tcp.port == 39011

为了方便观察,建议你给 Wireshark 的分组列表(Packet List)面板添加 tcp.seqtcp.acktcp.lentcp.flags 这几列,然后试着完成以下验证:

  1. 找到客户端发出的初始 SYN 报文,记下它原始的 Seq 值。接着查看服务端的 SYN+ACK 响应,确认其 Ack 字段是否刚好等于刚才记下的 Seq 值加 1。
  2. 找到传输那 400 字节和 300 字节数据的报文段。注意,由于 TCP 协议栈的缓冲机制、CPU 调度以及网卡的硬件卸载(如 TSO/LRO)特性,数据可能并没有精确地按 400 和 300 字节切分。此时你应该以抓包工具解析出的实际 tcp.len 为准,逐段累加核对。
  3. 对于服务端的每一个报文,请手动计算“当前已连续接收范围的右端点”,然后与报文头部实际携带的 Ack 进行比对,看看两者是否完全吻合。
  4. 仔细观察服务端回复的 OK 响应,看看它是否巧妙地“搭乘”了对客户端最后那批数据的确认。
  5. 在 Wireshark 的 Preferences -> Protocols -> TCP 设置中,尝试开启和关闭 Relative sequence numbers(相对序列号)显示。你会发现,无论是显示相对值还是原始值,它们只是改变了展示基准,两者之间的差值和区间推演规律始终保持一致。

在典型的实验结果中,这 700 字节的数据在底层可能会被封装成一个、两个甚至更多的 TCP 报文段;而服务端可能会发送单独的纯 ACK 进行确认,也极有可能随 OK 报文进行捎带确认。但无论网络行为如何变化,一条铁律始终不变:确认号永远按照连续接收的序列号空间稳步推进。推进的步长精确等于成功并入连续前缀的数据长度,并且会如实计入 SYN 和 FIN 各自占据的一个序列号位置。

理解检查

  1. 假设接收端当前的期待值为 8001,随后它成功接收了一个 Seq = 8001, Len = 600 的报文段。它接下来回复的累计 Ack 应该是多少?
  2. 当前的累计 Ack 停在 8601。这时,接收端突然收到了一个序列号区间为 [9001,9301) 的报文。此时,它的累计 Ack 应该停在哪里?
  3. 排障场景:客户端发出了完整请求,并在抓包中看到了服务端的 TCP ACK 回包,但随后客户端报出了“等待业务响应超时”错误。此刻,你可以斩钉截铁地确认哪一层的事实?
  4. 一个 Seq = 12000, Len = 80, FIN = 1 的报文完整到达接收端并被处理后,对端回复的确认号(Ack)应该指向哪个位置?
参考答案
  1. Ack = 8601
  2. Ack = 8601 不变;因为 [8601,9001) 仍然是连续前缀前方亟待填补的“缺口”。如果连接启用了 SACK 选项,接收端可以通过 SACK 额外报告它已经暂存了 [9001,9301) 这个乱序区间。
  3. 你可以确认:这批请求的字节流已经安然无恙地抵达了服务端的内核 TCP 接收缓冲区(传输层无异常)。至于应用层进程是否及时读取、业务逻辑处理是否顺利、数据库事务是否成功提交,这些都需要通过排查服务端的应用日志、响应报文或数据库状态来补充证明。
  4. 80 字节的数据让序列号推进了 80,而 FIN 标志还会额外占据 1 个序列号位置,因此最终结果为 Ack = 12081

本章小结

  • Ack 永远指向己方期待接收的下一个序列号,它宣告了在此之前的所有连续数据已妥善接收。
  • 网络中乱序到达的数据可以先缓存在接收队列中;但累计 Ack 必须死守在最前方的缺口处等待补齐。若需报告乱序接收的“数据孤岛”,可以借助 SACK 选项。
  • 无论是普通的数据 ACK 还是纯 ACK,都不会消耗序列号空间;而建立连接的 SYN 和断开连接的 FIN,则会各自占据一个序列号位置。
  • 同一个 TCP 报文中的 Seq 和 Ack 分别独立描述了两个不同方向的发送进度,因此业务数据与确认信息完全可以“搭便车”同行(捎带确认)。
  • 谨记:TCP ACK 仅仅能证明对端操作系统 TCP 协议栈的接收进度;业务维度的成功与否,必须依赖应用层面的响应、运行日志以及持久化状态来综合判定。
  • 在手工推演序列号时,建议先画出左闭右开的半开区间,再依次将实际数据长度(Len)、SYN 和 FIN 占据的位置累加到右端点上。

规范依据


上一章:第10章 Sequence Number · 所属篇:第三篇 · 下一章:第12章 TCP 标志位 · 教程总览

Licensed under CC BY-NC-SA 4.0.