04.04 可靠传输:TCP 确认了字节,还没确认消息
返回第四卷 · 通信分层:04.01 · 地址与端点:04.02 · HTTP/WS 前置:04.03
04.04 可靠传输:TCP 确认了字节,还没确认消息
DeepTutor 初稿经技术与教学审阅后的静态课程。序列号、时刻、窗口和确认均为纸上教学数字;本次没有运行 Go、抓包、联网、IM 服务或站点,也不声称测量了 OpenIM 的 TCP 行为。
一、m-a 走过网络,为何 TCP 成功仍不等于已送达
04.01–04.03 已从两个用户通信走到 HTTP 与 WebSocket。现在虚构 u-a 把业务消息 m-a 送往教学服务,再由服务将内容提供给 u-b。中间的 TCP 要解决一个更底层的问题:在连接存活且传输能够继续时,把应用写入的字节按序、无重复地交给另一端字节流。它并不知道“m-a 是消息”“服务端是否已经提交数据库”“B 是否看过”。
业务层:m-a 的身份、会话 c-a、受理/持久/设备确认
HTTP / WebSocket:请求、响应或帧与应用协议
TCP:一条连接上的有序字节流、序列号、确认和重传
IP:包向目标地址转发,途中可丢失、延迟或重排本章先用极小的 4 字节纸上片段练习 TCP,不把这些片段误当作完整 HTTP 报文或聊天消息。后面再回到“服务端处理过 m-a,A 却没收到回应”的业务问题:TCP 的重传负责同一连接里的缺失字节,应用在超时后重新提交则是一次新的业务请求,两者可能产生完全不同的重复风险。
| 第一遍 | 第二遍 | 交付物 |
|---|---|---|
| 序列号、ACK、丢失/重复、乱序 | RTO、窗口、TCP/HTTP/业务确认边界 | 三张时序表与一张“已知/未知/下一证据”表 |
二、序列号给字节编号,ACK 指向下一个期望字节
TCP 序列号记录的是字节在连接字节流中的位置,不是消息 ID,也不是“第几次发送”。假设玩具连接当前从序号 100 开始发送四个字节 ABCD,覆盖半开区间 [100,104);下一段 EFGH 覆盖 [104,108)。半开区间右端不包含:第一段有 100、101、102、103 四个字节。
| 已发数据段 | 字节序号范围 | 若已连续收到,对端返回的累计 ACK |
|---|---|---|
ABCD | [100,104) | ACK=104,下一字节期望 104 |
EFGH 且前段也已到 | [104,108) | ACK=108,此前连续字节已收到 |
累计 ACK 108 表示从这个连续接收范围的起点到 107 的字节都已被接收,下一位期望 108;它不是“确认第 108 条消息”。如果只确认了前四个字节,ACK 停在 104,后面的字节是否已暂存要看接收端行为。真实 TCP 序列空间会回绕,此章只用不回绕的小数字讲清基本关系。
TCP 把应用交给它的是连续字节流。A 进程两次 Write 不必对应 B 进程两次相同大小的 Read;网络可以分段、合并,应用自己的 HTTP 或 WebSocket 协议必须负责报文/帧和业务消息边界。message_id=m-a 与 TCP seq=100 解决不同问题:前者用于跨请求识别一条业务消息,后者只用于一条 TCP 连接内的字节顺序,重连后不能拿旧 TCP 序号查业务状态。
三、丢掉数据或丢掉 ACK,发送方为什么会重传
先看数据到了、ACK 丢了。这张纸上表不规定真实系统的毫秒值,只标因果先后:
| 时刻 | 发送端 A 的 TCP | 接收端服务的 TCP | 应用可直接断言什么 |
|---|---|---|---|
| t0 | 发送 [100,104) | — | 还不知道对端收到没有 |
| t1 | — | 收到四字节,回 ACK=104 | TCP 层有了连续字节;handler 是否处理未知 |
| t2 | ACK 在网络中丢失 | — | A 不知道 ACK 原本发出过 |
| t3 | 等待计时器到期,重发同一序号 [100,104) | 识别为已收到的重复字节,再回 ACK=104 | TCP 不应把这四字节重复交给应用字节流 |
| t4 | 收到 ACK=104 | — | A 的 TCP 知道对端确认了该连续范围 |
若 t0 的数据段本身丢失,接收端没有这四字节,也不会凭空确认到 104;发送端在没有新的确认时通过超时等机制重传,接收端收到后再确认。两种现象对发送端一开始都像“迟迟没有新 ACK”;超时重传不等于证明原始数据一定丢了,也可能是 ACK 丢、链路排队或确认延迟。
重复传输的数据段通过序列号被接收端识别,TCP 对应用呈现的是一条无重复的字节流;不能因为抓包看见两次 [100,104) 就说 Go handler 收到了两次 POST m-a。如果应用自己又发送一份新的 HTTP 请求,就会占用新的字节位置甚至新连接,那才是另一个业务重试问题。
四、后段先到:乱序、缺口和连续确认
假设 A 连续发送 [100,104) 与 [104,108),但网络让后段 EFGH 先到。接收端可把后段暂存在缓冲里,但前四字节仍缺;在这个基础模型里,累计 ACK 仍指向最早缺口 100。当前段后来到达,两段形成连续 [100,108),便可回 ACK=108,并按字节顺序交给上层。
先到: [104,108) 缺 [100,104) → 累计 ACK 仍为 100
后到: [100,104) 连成 [100,108) → 累计 ACK 可到 108这张表只讲基本累计确认。实际 TCP 还可通过选择性确认等扩展告知已收到的不连续范围,但对应用可见的顺序要求仍不同于“c-a 第几条业务消息”。WebSocket 帧有自己的协议边界,数据库里会话 seq 有自己的业务顺序;它们不能靠 TCP 序列号自动生成或替代。若服务端收到了 m-b 的字节,不意味着业务上已先于 m-a 完成持久提交。
乱序还会占用接收缓冲;接收端不能无限收下后段等待前段。缓冲与窗口如何限制在途字节,第六节只讲入口,04.06 再详细区分流量控制和拥塞控制。
五、RTO、HTTP 超时和 Go Context 是三只不同的钟
RTO(重传超时)属于 TCP 发送方:一段数据发出后没有收到足够的新确认,就在计时器到期时考虑重传。RFC 6298 要求 RTO 由往返时间估计及其变化来计算,不能拿“固定等 20 ms”当通用协议规则。RTO 太短容易因正常排队/确认延迟而多发重复段;太长则真实丢失后恢复慢。此章只推导方向,不假装知道某台机器当前内核的 RTO 数值。
| 时钟 | 谁设置/管理 | 到期主要影响 | 不自动推出 |
|---|---|---|---|
| TCP RTO | TCP 实现 | 未确认字节的重传与回退 | m-a 已被应用处理或未处理 |
| HTTP 客户端期限 | 调用方/HTTP 客户端 | 等待请求回应的总时长 | 服务端未受理或未写入 |
Go context 截止 | 应用代码传播的取消/期限 | 请求处理、数据库/网络调用的协作取消 | 已完成状态被自动回滚 |
例如服务端在 A 的 HTTP 期限到达前已接收并处理请求,返回响应却在网络上延迟或丢失。A 看到超时,只能说“期限内未见可用回应”;TCP 可能仍在底层重传字节,服务端业务状态也可能已经变化。context 的取消是通知与资源控制信号,不是时间机器。要判定 m-a 是否已受理,还需服务端受理日志/状态及后续查询证据,见 09.04、11.01。
六、为什么允许多个未确认片段在途
若 A 每发四个字节都必须等 ACK 才发下四个,往返等待可能让网络大部分时间闲着。窗口与流水线允许一定范围内的字节尚未确认也继续发送。纸上把当前可在途范围暂定为 8 字节:A 可以先发 [100,104) 和 [104,108);收到 ACK=104 后,前四字节不再占用未确认预算,才可继续把 [108,112) 纳入可发范围。窗口是边界,不是“越大越可靠”。
窗口预算 8 B
发送 [100,104) + [104,108) → 在途 8 B
收到 ACK=104 → 仍未确认 4 B
发送 [108,112) → 在途再到 8 B发送端要记住哪些字节尚未确认,以便超时重传;接收端可能需要缓存乱序字节。接收方能力限制的是别把对端缓冲塞满;网络拥塞限制的是别把路径中间设备淹没。两者都影响可发送量,但机制不同,04.06 才深入。对 IM 慢设备,不能通过“无限加大服务端缓存”解决:内存和排队时延会增长,最终仍需应用层的有界队列与策略。
七、TCP 重传不是重新发送一条业务消息
对比两种“重试”:
| 情景 | 新发了什么 | 去重由谁负责 | A 现在知道什么 |
|---|---|---|---|
同一连接里 ACK 丢,TCP 重发 [100,104) | 旧字节范围再上网 | TCP 序列号 | 对端 TCP 最终确认到哪里;不知应用提交 |
A 的 HTTP 响应丢,客户端重新 POST m-a | 一份新业务请求,可能在新连接/新字节范围 | IM 的稳定消息 ID、接口合同 | 第一次是否已受理仍可能未知 |
09.02 当前教学接口若内存里已有同会话 m-a,重复 POST 返回 409 DUPLICATE_MESSAGE,旧正文不被覆盖;它还没有设计“客户端超时后重复请求自动返回第一次成功结果”的幂等确认。若客户端在超时后随意换成新消息 ID,可能把同一用户意图变成两条业务消息。TCP 本身不认识 message_id,不会跨连接帮你判断哪两次 POST 表达同一意图。
同样,Go Conn.Write 没报错只表示本次写调用按其接口完成了相应字节处理,不能单凭它证明对端应用已解析、数据库已提交或 B 已读。TCP ACK 最多给出对端 TCP 字节接收进度;HTTP 响应给出某个服务器请求路径的回应;业务消息身份、持久记录和设备回执分别给更高层的证据。三层结论不能相互替代。
八、交付三张时序表与分层练习
本章交付:① [100,104)/ACK=104 的正常累计确认表;② 数据段丢与 ACK 丢的两种重传表;③ [104,108) 先到的乱序表。每表标出发送方已知、接收 TCP 已知、应用仍未知。另用 8 B 纸上窗口算在途范围,并写“m-a 首次受理后回应丢失”时下一份业务证据该去哪里找。示例不依赖真实抓包或网络运行。
分层练习:先答,再展开反馈
1. TCP 序列号编号的是消息 ID 还是字节?
字节在该连接字节流中的位置,不是业务消息 ID。
2. `[100,104)` 含几个字节?
4 个,序号 100、101、102、103。
3. 收到这四字节后 ACK=104 表示什么?
连续接收到 103,下一字节期望 104。
4. 两段 `[100,104)`、`[104,108)` 连续收到,可累计确认到哪里?
ACK=108。
5. A 两次 Write,B 就一定两次等长 Read 吗?
不一定。TCP 是字节流,不保留应用写调用的边界。
6. ACK=104 能证明服务器数据库已保存 m-a 吗?
不能,只是 TCP 接收进度的证据。
7. 数据到了但 ACK 丢了,发送端可能怎样?
等不到确认后重传同序号字节,接收端识别重复并再次确认。
8. 抓包看到同一段两次,handler 一定收到两次 POST 吗?
不一定。TCP 重传段应由协议去重,不自动变成两次应用请求。
9. 首次数据段丢了,接收端能凭空 ACK=104 吗?
不能。需先收到连续范围。
10. 看到重传就能断言原始数据段丢失吗?
不能,也可能是 ACK 丢失或延迟等导致没有及时新确认。
11. `[104,108)` 先到但 `[100,104)` 未到,基本累计 ACK 指向哪里?
最早缺口 100。
12. 前段随后到达,两段连成连续范围后可 ACK 到哪里?
108。
13. TCP ACK 的序号能跨重连当会话消息序号吗?
不能。它只属于该连接的字节序列,业务 seq 另有含义。
14. RTO 应始终固定为 20 ms 吗?
不能。它依赖 RTT 估计与波动,例子不是内核配置。
15. 客户端 HTTP 超时能证明服务端没处理吗?
不能。请求可能已处理,只是回应未在期限内到达。
16. Go context 取消会自动撤销已完成的数据库提交吗?
不会。取消是协作信号,已完成状态要按实际提交点判断。
17. 8 B 窗口里先发两个 4 B 段,在途多少字节?
8 B。
18. ACK=104 后,在途未确认字节还剩多少?
第二段 4 B;纸上窗口可再允许 4 B 新数据。
19. 接收方窗口与拥塞窗口是同一问题吗?
不是。前者关心对端接收能力,后者关心网络路径拥塞,04.06 深入。
20. TCP 重传与客户端重新 POST m-a 是同一动作吗?
不是。前者重发旧字节范围,后者是新应用请求。
21. 当前教学接口对同会话重复 m-a 返回什么?
409 DUPLICATE_MESSAGE,不覆盖旧正文;幂等回放尚未设计。
22. `Conn.Write` 成功能证明 u-b 已读吗?
不能。还需应用处理、持久/设备确认和阅读证据。
来源与下一步
- RFC 9293:TCP:字节流、序列号、累计确认与重复检测。
- RFC 6298:TCP 重传计时器:RTO 与超时重传的基本约束。
- Kurose/Ross 作者互动题:可靠数据传输与TCP 重传:丢失与 ACK 丢失的纸上推演参考。
- Go
net包:后续 Go 网络 I/O 的接口入口;本章不运行套接字。
按学习路线,下一章04.05 TCP 与长连接生命周期再讲建立、半关闭、重置、心跳和重连;04.06 解释接收方与网络拥塞,04.07 进入 TLS。离开本章前,应能分别写出:哪一段字节已被 TCP 确认、哪一次 HTTP 请求有回应、哪条业务消息真正完成了哪个确认点。