一句话结论(30s)
TCP 的可靠不是单一机制,而是六大机制环环相扣:因为「可靠」被拆成无差错、不丢失、不重复、按序四个目标,分别由校验和、确认应答+超时重传、序列号去重、序列号+排序缓冲区保证,再叠加流量控制防接收方溢出、拥塞控制防网络雪崩、连接管理建立序列号上下文,缺一不可。
核心原理(2min)
四大目标对应六大机制:校验和保证无差错;确认应答 + 超时重传保证不丢失;序列号去重保证不重复;序列号 + 排序缓冲区保证按序。流量控制用 rwnd 防接收方溢出,拥塞控制用 cwnd 的慢启动 / 拥塞避免 / 快速重传 / 快速恢复防网络雪崩,连接管理(三次握手 / 四次挥手)建立序列号与参数上下文。
底层深入(5-10min)
可靠的四个目标
TCP 的可靠性不是笼统的”保证送到”,而是精确的四个目标:
- 无差错:数据在传输过程中没有比特翻转(由校验和保证)
- 不丢失:每个字节最终都会被正确送达(由确认应答 + 超时重传保证)
- 不重复:接收方不会收到两份相同的数据(由序列号去重保证)
- 按序到达:接收方看到的数据顺序与发送方发送的顺序一致(由序列号 + 排序缓冲区保证)
这四大目标由六大机制协同实现。
机制一:连接管理(三次握手 / 四次挥手)
连接管理的详情在专门的博客中已深入分析,这里聚焦于它对可靠性的贡献:
三次握手建立了可靠传输的基础上下文:
- 交换初始序列号(ISN),为后续的数据编号和确认奠定基础
- 协商 MSS(最大报文段大小),避免中间路由器的 IP 分片
- 协商窗口缩放因子,为流量控制提供基础
- 确认双方收发能力正常(“你听到我了吗?我听到你了。我也听到你了。”)
没有三次握手,双方无法对序列号空间达成共识,后续所有可靠性机制都无从谈起。
四次挥手确保数据完整交付后才释放连接:
- 主动关闭方发送 FIN:我的数据发完了
- 被动方 ACK + 再发送 FIN:你的数据我收到了,我的数据也发完了
- 主动关闭方最终 ACK + TIME_WAIT:确认你收到了我的 ACK,并等待网络中残留的迟到数据包
机制二:序列号与确认应答
序列号
TCP 把数据看作一个连续的字节流,每个字节都有一个唯一的序列号。TCP 首部中的序列号字段(32 位)记录的是本报文段第一个数据字节的序列号。初始序列号(ISN)随机初始化(防攻击),之后按字节数递增。
序列号的主要作用:
- 按序重组:接收方根据序列号将乱序到达的报文段按正确顺序排列
- 去重:序列号相同的报文段是重复的,直接丢弃
- 确认应答:确认号基于序列号,告诉对方收到哪里了
💭 思考:为什么 TCP 要给“每一个字节”编号,而不是给“每一个包”编号?——因为 TCP 是字节流,数据在传输中可能被任意切分、重组、重传,包的边界本身就不稳定。以字节为最小单位编号,接收方才能精确地说出“我收到了第 N 个字节,现在等第 N+1 个”,去重、排序、确认都基于这个统一的坐标轴。如果按包编号,一旦重传时包被切成不同的片,编号体系就乱了。
确认应答
TCP 的确认机制是累计确认——确认号 N 表示”N-1 及之前的所有字节都已收到,现在期望第 N 个字节”。这种设计简洁高效:一个 ACK 可以确认所有先前连续收到的数据。
但累计确认也有局限:如果报文段 1、2、4 到达而 3 未到达,接收方只能确认到 2。发送方需要收到三次重复 ACK(对序列号 2 的 ACK 重复三次)后才能触发快速重传(不等超时,直接重传丢失的报文段 3)。
思考:为什么「累计确认」简洁却需要「快速重传」来补?因为累计确认一个 ACK 能确认「之前连续收到的所有数据」,成本极低;但它也有盲区——中间漏了一个包,接收方只能反复 ACK 同一个序号。发送方收到三次重复 ACK 就明白「后面的都到了,只丢中间那一个」,于是不等超时直接重传,把等待时间从「一个 RTO」压缩到「三次 ACK 的时间」。
SACK(选择性确认)
TCP Option 中的 SACK 字段解决了累计确认的歧义问题。接收方通过 SACK 告诉发送方”我收到了 4-5 但缺 3”,发送方只需重传 3 而不需要重传 3、4、5。在大窗口高带宽场景下,SACK 显著提升了重传效率。
💭 思考:既然累计确认“一个 ACK 确认一片”已经够简洁,为什么还要引入 SACK?——因为累计确认在“中间丢包、后面已到”时信息不够:它只能反复 ACK 同一个序号,发送方搞不清“到底是丢了一个还是后面全丢了”。SACK 让接收方额外说一句“4、5 我都收到了,只缺 3”,发送方就能只重传缺的那个,省下重传已到数据的带宽。这在“大窗口、高带宽、长距离”的场景下省下的量非常可观——是“用几个字节的选项,换掉整段不必要的重传”。
机制三:超时重传
发送方每发送一个报文段,就启动一个重传定时器。如果在超时之前收到确认,清除定时器。如果超时还没收到确认,重传报文段。
RTO 的计算——Jacobson 算法
超时时间(RTO, Retransmission Timeout)不能太长(增加丢包后的等待延迟)也不能太短(误判丢包导致无意义重传)。TCP 使用 Jacobson 算法动态计算 RTO:
SRTT = (1 - α) × SRTT + α × RTT_sample (α 通常 = 1/8)
DevRTT = (1 - β) × DevRTT + β × |RTT_sample - SRTT| (β 通常 = 1/4)
RTO = SRTT + 4 × DevRTT
关键点:RTO 不仅是 RTT 的指数平滑,还加上了 RTT 的偏差(DevRTT)乘以 4。这确保了在网络抖动较大时 RTO 足够宽松,减少误判。
思考:RTO 为什么要「加一个偏差」而不是直接用平均 RTT?因为网络时延不是恒定的,直接用平均值会在网络抖动大时频繁误判丢包。加上 4 倍 DevRTT,等于给 RTO 留了缓冲:抖动越大、超时越宽松,宁可晚一点重传,也不要乱重传去给拥塞的网络雪上加霜。
指数退避
如果重传后仍然超时,RTO 翻倍(每次翻倍,直到收到确认或达到上限)。这种指数退避策略避免了在网络严重拥塞时雪上加霜地发送更多重传包。
机制四:流量控制
流量控制防止发送方发送得太快导致接收方缓冲区溢出。每次 TCP 通讯中,接收方通过 TCP 首部的窗口大小字段(16 位)告知发送方自己还有多少接收缓冲区可用:
rwnd = 接收缓冲区总大小 - (最后收到的字节 - 最后被应用读取的字节)
发送方保证”已发送但未确认”的数据量不超过 min(cwnd, rwnd)。
零窗口与窗口探测
如果接收方缓冲区满了,窗口大小变为 0,发送方停止发送。当接收方应用读取了数据后,会发送携带新窗口大小的 ACK。但这个 ACK 可能丢失——如果发送方一直等 ACK,接收方一直等发送方发数据,就形成了死锁。
TCP 的解决方案是窗口探测(Window Probe):当窗口为 0 时,发送方周期性发送 1 字节的探测报文段(persist timer),强制接收方回复当前窗口大小。
💭 思考:窗口为 0 时,双方都在等对方,为什么不会永远死锁?——因为 TCP 没有“干等”,而是加了一根“探测针”。发送方在窗口为 0 时仍周期性发一个 1 字节的探测包,逼接收方回一句“我现在窗口多大”。这根针专门用来戳破“你在等我、我也在等你”的僵局:即使通告新窗口的 ACK 丢了,探测包也会把最新窗口顶出来。可见可靠协议里,很多“看似多余的机制”其实都是在为“丢包导致的死锁”兜底。
糊涂窗口综合征(Silly Window Syndrome)
如果接收方每次只读取 1 字节(窗口只扩大 1 字节),发送方立即发送 1 字节数据(首部 20 字节 + 数据 1 字节,效率极低)。这就是糊涂窗口综合征。
TCP 通过两个策略避免:
- 接收方:窗口扩大到 MSS 的一半或足够大时才通告新窗口(Clark 方案)
- 发送方:可以发送的数据必须至少为 MSS(Nagle 算法)
机制五:拥塞控制
流量控制管的是”对方还能收多少”,拥塞控制管的是”网络能承受多少”。拥塞控制通过四个算法协同运作:
思考:流量控制和拥塞控制都「限速」,区别在哪?看管的对象不同——流量控制管的是「对方还能收多少」(rwnd,防止接收方缓冲区溢出),是端到端的点对点约束;拥塞控制管的是「网络能承受多少」(cwnd,防止整条路径雪崩),是对全局公共资源的保护。一个盯着对端,一个盯着沿途。
慢启动:拥塞窗口(cwnd)从 1 MSS 开始,每收到一个 ACK 翻倍。指数增长,探测网络可用带宽。
拥塞避免:cwnd 达到慢启动阈值(ssthresh)后,改为线性增长(每 RTT 增加 1 MSS)。保守地逼近但不超出网络容量。
快速重传:收到 3 个重复 ACK 时立即重传(不等超时),说明后续包还在到达,只是丢了一个——网络没有严重拥塞。
快速恢复:重传后不降到慢启动(cwnd = 1),而是将 cwnd 减半(或 CUBIC 用更复杂的曲线),直接进入拥塞避免阶段。
机制六:校验和
TCP 使用 16 位校验和(Internet checksum),对 TCP 伪首部 + TCP 首部 + 数据全部计算。虽然 CRC 更强大,但 16 位校验和在 1970 年代足够低开销且足以检测常见错误。现代链路层(Ethernet CRC32)和传输层多层校验覆盖了长尾的比特错误场景。
六大机制的协同全景
把这些机制画成全景图:
发送方 网络 接收方
应用 write(数据)
→ 序列号分配(机制二)
→ 校验和计算(机制六)
→ 检查:cwnd 允许?(机制五)
→ 检查:rwnd 允许?(机制四)
→ 发送 + 启动重传定时器(机制三)
│
[数据报文段] ───────────→ 网络 ───────────→ [数据报文段]
│ │
│ 校验和验证(机制六)
│ 序列号检查(机制二)
│ 去重 + 排序(机制二)
│ 累积确认(机制二)
│ │
│ ←─────────── 网络 ←──── [ACK]
│ │
收到 ACK → 应用 read()
清除重传定时器 通告新 rwnd
更新 RTT 样本 → 调整 RTO(机制三)
更新 cwnd(机制五)
发送方确认全部 ACK → 累计确认完整 → 知道所有数据已送达(机制二)
这张图揭示了 TCP 可靠性的秘诀:不是某一个机制很强,而是六大机制环环相扣、层层递进。连接管理建立上下文,序列号提供命名空间,确认应答提供反馈,超时重传兜底,流量控制防接收方溢出,拥塞控制防网络雪崩。缺失任何一个,系统都会出现致命的可靠性漏洞。
章末提问
Q1:TCP 的可靠性靠哪些机制保证?
结论先行:靠六大机制协同——连接管理、序列号+确认应答、超时重传、流量控制、拥塞控制、校验和。因为「可靠」被拆成无差错(校验和)、不丢失(确认+重传)、不重复(序列号去重)、按序(序列号+排序缓冲区)四个目标,再叠加流控防接收方溢出、拥塞控制防网络雪崩,缺一不可。
Q2:流量控制和拥塞控制的区别是什么?
结论先行:流量控制防止「发送方太快导致接收方溢出」,管的是 rwnd;拥塞控制防止「发送方太多导致网络拥堵」,管的是 cwnd。因为前者是端到端的点对点约束(接收方通告窗口),后者是对整条路径的全局保护(慢启动/拥塞避免/快速重传/快速恢复),两者作用对象完全不同。
Q3:为什么快速重传要等 3 个重复 ACK 而不是 1 个?
结论先行:因为单个重复 ACK 可能是乱序造成的误判,3 个才能较可靠地确认「确实丢了包」。因为网络中报文乱序很常见,1-2 个重复 ACK 可能是「后发的包先到了」;连续 3 个重复 ACK 说明发送方已收到 3 个「期待同一序号」的反馈,丢失概率足够高,此时重传不会造成大量无谓重发。