模块二 · 运输层
TCP 可靠传输
IP 协议什么都不保证 —— 可能丢、可能乱序、可能重复。那么 TCP 凭什么说自己"可靠"?本章讲序号、确认、超时重传这三件事,并且用本机的真实统计算出你机器上的重传率。
本章要解决的问题:IP 协议什么都不保证 —— 可能丢、可能乱序、可能重复。那么 TCP 凭什么说自己"可靠"?本章讲序号、确认、超时重传这三件事,并且用本机的真实统计算出你机器上的重传率。
本章的证据来源:历史实测记录(netstat -s 的 TCP 段统计、Get-NetTCPSetting 的重传超时参数)。机制出自 RFC 9293(TCP)。
先从一个例子理解概念
你把三份内容发出,对方收到了第一份和第三份。怎样知道中间缺了什么,又怎样补回来?先认识编号、确认和等待。
发送什么位置:序号
TCP按字节编号;序号指出这一段数据的位置,而不是简单给每个包排个第几号。连接开始时选的初始序号叫ISN。
连续收到了哪里:累计确认
累计确认报告下一个期待序号,意思是前面连续的内容已收到。后来的确认可覆盖先前确认,但不表示有业务人员阅读了数据。
等多久再补发:重传与RTO
没按预期收到确认时,发送端可能重传相关字节。用于超时恢复的定时值叫RTO(重传超时),不是所有连接都共用一个固定秒数。
补充不连续的已收范围:SACK
SACK(选择性确认)告诉发送端哪些不连续字节范围也已到达,使恢复更有依据。它报告已收内容,不是要求把剩余全部重传。
下面再用这些概念看原理、命令和结果。规范与官方资料列在本章末尾,类比只帮助理解,不代替协议规则。
TCP 要在一个不可靠的通道上做事
先把矛盾摆清楚:
| 保证 | |
|---|---|
| IP 层提供的 | 尽力而为。可能丢包、可能乱序、可能重复、可能出错 |
| TCP提供的 | 可靠有序字节流;连接可能失败,不承诺故障后必达或业务已处理 |
TCP 无法让底层变得可靠 —— 它只能在不可靠之上重建可靠。手段就是三件事:
- 序号 —— 给每个字节编号,这样能发现丢和乱序
- 确认 —— 让对方告诉我收到了哪些
- 重传 —— 没被确认的,我重新发
序号:每个字节都有编号
这是 TCP 设计里最关键的一个决定:
假设你要发 3000 字节的数据:
第 1 个段:序号 1 ~ 1000
第 2 个段:序号 1001 ~ 2000
第 3 个段:序号 2001 ~ 3000为什么按字节编号? 因为这样能精确定位"到底丢了哪几百个字节",重传时只补这一段。按字节编号使重传边界可以独立于原来的分段;其他协议按包编号也可以进行选择性恢复,不宜据此说所有包编号机制低效。
ISN不是每次固定从1开始。 规范结合时间与连接相关的安全生成函数等要求,以降低旧报文混淆和序号预测风险,而非简单随机抽一个数。原因和上一章讲 ARP 缓存类似:防止上一次连接延迟到达的报文被误认为本次连接的数据。
确认:累积确认
TCP 的确认是累积的:
ACK = 2001 的意思是:"2001 之前的每一个字节我都收到了"累积确认有个很好的副作用:如果中间的某个 ACK 报文丢了,不要紧 —— 后面的 ACK 会把它覆盖掉。比如:
ACK 1001 → 丢了
ACK 2001 → 收到了 ← 这一条同时确认了前面 2000 个字节代价是:如果只丢了中间一小段,接收方无法准确告诉发送方"我缺的是哪一段"。所以后来加了 SACK 机制(本章后面讲)。
超时重传:RTO 该设多长
发送方发出一个段,要等多久没收到确认才认为"丢了,重传"?这个等待时间叫 RTO(Retransmission Timeout)。
这是一个两难:
| RTO 设得 | 后果 |
|---|---|
| 太短 | 明明只是网络慢了一点,你却重传了 → 浪费带宽,还可能加剧拥塞 |
| 太长 | 真的丢了却要等很久才重传 → 传输变慢 |
所以 RTO 不能是固定值,必须动态算。 TCP 的做法是:
- 持续测量每一个连接的往返时间(RTT)
- 同时测量 RTT 的波动程度(有的包 10ms,有的 200ms —— 波动大的话 RTO 要更保守)
- 用"平均 RTT + 若干倍的波动"算出 RTO
实测:本机的 RTO 参数
Get-NetTCPSetting实测结果(只列关键字段):
| 设置档 | 拥塞算法 | InitialRto | MinRto | MaxSynRetransmissions |
|---|---|---|---|---|
| InternetCustom | CUBIC | 1000 ms | 300 ms | 4 |
| DatacenterCustom | CUBIC | 1000 ms | 60 ms | 4 |
| Compat | NewReno | 1000 ms | 300 ms | 4 |
两个关键值:
InitialRto = 1000 ms—— 第一次重传前先等 1 秒。这是初始值,之后连接会根据自己的 RTT 测量动态调整。MinRto = 300 ms—— 这份Internet模板报告的MinRto为300ms,不证明所有连接或其他恢复机制都等待至少300ms。这是个下限保护:如果算出来的 RTO 太小,稍微一点抖动就会触发无谓的重传。- 注意
DatacenterCustom档的 MinRto 只有 60 ms —— 因为数据中心内部网络延迟极低且稳定,可以用更激进的参数。这说明 RTO 的取值是跟环境相关的。
指数退避:重传的等待时间会翻倍
如果重传之后还是没确认,TCP 不会一直用同一个 RTO,而是每次都翻倍:
第 1 次重传:等 1 秒 (InitialRto)
第 2 次重传:等 2 秒
第 3 次重传:等 4 秒
第 4 次重传:等 8 秒
……这叫指数退避。目的很明确:如果对方真的不可达,不要一直用密集的重传把网络拖垮。
MaxSynRetransmissions = 4 的意思就是:建立连接时的 SYN 最多重传 4 次,之后放弃,connect() 返回失败。这解释了一个日常现象 —— 连一个不可达的地址,要等十几秒才报错。
实测:算出本机的真实重传率
netstat -s -p tcp历史输出为Segments Sent=549576029,Segments Retransmitted=1946592。直接比值约0.354%,可作为该统计口径下的重传指标,不是测得的IP丢包概率,也没有“低于1%就健康”的通用标准。
Windows的MIB_TCPSTATS将输出段计数与重传段计数分别定义;应核对所用工具对应口径,不能说重传必然是Sent的子集。诊断时采集相关时间窗口的差值,并结合目标连接、RTT、吞吐、乱序和应用影响。Microsoft MIB_TCPSTATS
快速重传:不必等到超时
基于RTO的恢复要等相应定时器到期;具体值取决于连接、模板和版本,其他恢复机制可能更早行动。太慢了。
有没有办法更快发现丢了?有 —— 靠"重复的 ACK"。
发送方发: 段 1 段 2 段 3 段 4 段 5
接收方收到: 段 1 段 3 段 4 段 5 (段 2 丢了)
接收方每次都回:"我还在等 段 2"(因为累积确认停在段 2 之前)
→ 收到段 3 时回 ACK 2
→ 收到段 4 时回 ACK 2 ← 重复
→ 收到段 5 时回 ACK 2 ← 重复
发送方看到 3 个重复的 ACK → 不等超时,立刻重传段 2为什么是 3 个? 因为 1~2 个重复 ACK 可能只是乱序(段 2 还在路上,后面几个先到了),不一定是丢。连续 3 个重复 ACK 强烈暗示段 2 真的丢了,这时立即重传比等超时划算得多。
这套机制叫快速重传(Fast Retransmit)。
SACK:只重传缺的那一段
SACK告诉发送端哪些字节区间已经到达。 接收端的累计ACK仍报告连续收到的边界,SACK块补充不连续的已收区间,尤其帮助一个窗口内多个丢失段的恢复。
没有SACK时,经典快速重传也可先重传推定缺失的段,接收端可以缓存乱序数据;不是必须把丢失点之后全部重发。SACK不直接“请求重传”,最终选择由发送端恢复算法决定。RFC 2018 §3–4、RFC 5681 §3.2
注册表未设置SackOpts不证明某条连接已成功协商SACK;确认需要看握手里的SACK-Permitted及之后报文。
实测:一个偏高但我没查清的数字
上面那张统计表里有一行需要单独说:
Failed Connection Attempts = 2870804 建立失败的连接
Active Opens = 11501824 主动发起的连接两个历史计数的比值约25%,但尚未证明它就是主动连接失败率。 Failed Connection Attempts可包含不同状态转换,需核对计数语义和统计窗口,再选择对应分母。
不能由这个累计比值推出“主动发起的连接四分之一失败”,也不能给出未经验证的健康结论。
但我没有查明原因。 有几个可能的解释,都不能确认:
| 可能的原因(都是推测) | 能否验证 |
|---|---|
| 本教程自己做的实验(连接保留地址、不存在的端口) | 量级远远不够,只占极小一部分 |
| 代理软件在后台探测大量不可达的节点 | 有可能,但没有证据 |
| 某些应用反复重试失败的连接 | 有可能,但没有证据 |
| 应用程序先尝试 IPv6 失败再回退 IPv4 | 有可能(本机 IPv6 不可用,《网络层》模块实测过) |
所以我把这一条标成"观察到但未解释",而不是给它编一个说法。
常见误解
误解一:"TCP 序号是给每个包编号的"
是给每个字节编号。 这样才能精确定位丢失的范围,只重传缺的那几百字节,而不是整包重来。
误解二:"TCP 重传说明网络坏了"
重传是正常机制的一部分。 底层 IP 本来就不保证送达,TCP 的重传就是在处理这件事。不能仅靠固定百分比判定健康,需结合统计口径、RTT、业务和时间趋势。 真正要担心的是重传率持续偏高 —— 那说明链路或拥塞有问题。
误解三:"RTO 是个固定值"
是动态计算的,基于每个连接实测的 RTT 和波动。历史实测记录的 InitialRto = 1000ms 只是初始值,MinRto = 300ms 是下限保护。而且这两个值还随网络类型变化(数据中心档的 MinRto 只有 60ms)。
误解四:"丢包就等超时重传"
大部分丢包靠快速重传解决,不用等超时。 收到 3 个重复 ACK 就立即重传了。超时重传是兜底手段,用于那些"连重复 ACK 都收不到"的情况(比如连续丢包、或者最后一个包丢失)。
本章小结
| 结论 | 性质 |
|---|---|
| TCP按字节序号、确认和恢复机制提供可靠有序字节流 | RFC 9293 |
| ACK报告下一期待序号,累计确认可覆盖前面确认 | 协议机制 |
| RTO根据RTT和变化估计,超时退避 | RFC 6298 |
| 经典快速重传可先补缺失段,不要求启用SACK | RFC 5681 |
| SACK补充已经收到的区间,帮助多个丢失段恢复 | RFC 2018 |
| 累计计数器比值不直接等于丢包概率或主动连接失败率 | 计数边界 |
| 健康判断需要对应连接、时间窗口和业务指标 | 诊断方法 |
自测题
7 题 · 先自己答,再展开对照
为什么 TCP 的序号是给字节编号,而不是给报文段编号?
字节编号让确认、重分段和恢复不依赖最初包边界;不能因此说所有按包编号的协议都只能无选择地重传。TCP 的初始序号为什么不用固定的 1,而是随机选?
ISN生成需防止旧连接报文混淆并降低序号可预测性;不是固定从1开始,也不应简化成唯一的纯随机生成规则。收到
ACK = 5001是什么意思?如果上一个ACK = 4001的报文丢了,会有什么后果?意思是"5001 之前的每一个字节我都收到了"(累积确认)。如果ACK = 4001那条报文丢了,不会造成问题 —— 因为ACK = 5001已经隐含确认了前面所有字节,后面的确认会覆盖前面的。这正是累积确认的好处。RTO 设得太短或太长分别有什么问题?为什么它必须是动态的?
太短:网络只是稍微抖动一下就触发重传,白白浪费带宽,还可能加剧拥塞。太长:真的丢包时要等很久才重传,传输变慢。
必须动态的原因:不同链路、不同时刻的 RTT 差别很大(局域网 1ms、跨国 200ms),而且波动程度也不同。固定值不可能在所有情况下都合适,所以要根据实测的 RTT 和它的波动动态算。历史实测记录的InitialRto=1000ms、MinRto=300ms就是这套机制的参数。你在本机执行
netstat -s -p tcp,看到Segments Sent = 1000000、Segments Retransmitted = 25000。重传率是多少?这个网络健康吗?该计数比值为2.5%。须核对Sent是否包含重传、统计窗口和连接范围;它不是通用健康阈值,不能单独确认拥塞或链路损坏。接收方连续回了 3 个相同的 ACK。发送方会怎么做?为什么要等到 3 个而不是 1 个?
发送方会立即重传那个被跳过的段,不等 RTO 超时 —— 这叫快速重传。等到 3 个是因为 1~2 个重复 ACK 可能只是乱序(丢失的那个段还在路上,后面的先到了),这时重传是多余的。连续 3 个重复 ACK 强烈暗示真的丢了,此时立即重传的收益远大于误判的代价。SACK 解决了什么问题?
SACK报告非连续的已收字节区间,提高发送端恢复决策的信息量。无SACK也能快速重传缺失段,不是必须重发丢失点之后的全部数据。
参考资料与适用范围
- RFC 9293 §3.3–3.10:TCP状态、建立关闭、字节流与接口
- RFC 2018 §3–4:SACK报告已收字节区间
- RFC 5681 §3:经典拥塞控制与快速恢复
- RFC 6298 §2、§5:RTO估计和超时退避
- Microsoft MIB_TCPSTATS_W2K:输出段、重传段和失败尝试的口径
修订核对日期:2026-10-13。规则按所列协议与实现范围理解;文中的原始命令输出是历史观察,不表示本次重新实测。跨版本、代理/隧道及不同捕获位置需要另行核对。