type
status
date
slug
summary
tags
category
icon
password
我们已经走到了哪一步?
此时,我们已经实现了一个功能最小但可运行的 用户态 TCP/IP 协议栈,并且能够通过它:
- ✅ 与其他主机在互联网上通信
- ✅ 完成三次握手建立连接
- ✅ 进行简单的数据发送与接收
- ✅ 提供应用层可用的 socket 接口
这一切看起来已经具备了“TCP 的样子”——但还缺失了最重要的一环:可靠性(Reliability)。
当前版本的 TCP 存在的核心问题
虽然我们的 TCP 实现能够发送数据,但它并没有做到:
- ❌ 确认(ACK)丢失的重传
- ❌ 超时控制与指数回退
- ❌ 丢包后的恢复能力
- ❌ 对乱序数据的处理与排序
- ❌ 断点续传与拥塞控制
甚至,握手本身如果某个数据包丢失,连接也无法成功建立。
下一阶段:实现 TCP 的可靠性核心机制
接下来,我们将专注于 TCP 的真正“灵魂”部分:
目标模块 | 作用说明 |
✅ 重传机制(RTO + 快速重传) | 保证数据不会因为丢包而永远丢失 |
✅ 窗口管理 | 控制流量,避免接收端缓冲区溢出 |
✅ ACK 确认与延迟 ACK | 保证双方状态一致性 |
✅ 拥塞控制(可选) | 面对网络负载时自适应收发速率 |
✅ 乱序缓冲与重组 | 提高实际吞吐能力 |
自动重传请求(Automatic Repeat reQuest,ARQ)
在几乎所有 可靠传输协议 中,有一个核心机制被广泛采用 —— ARQ(自动重传请求)。
TCP 就是典型的基于 ARQ 的协议。
什么是 ARQ?
ARQ 的基本原理:
接收方收到数据后发送 ACK(确认)发送方维护一个重传队列如果超时未收到 ACK,则自动重传该数据段
TCP 是如何实现 ARQ 的?
TCP 的 ARQ 实现包含以下步骤:
- 发送数据段
- 把数据放入重传队列
- 启动 定时器(RTO)
- 等待确认
- 如果在定时器过期前收到了 ACK:
- 移除对应数据段,更新
SND.UNA
- 如果超时未收到 ACK:
- 重新发送数据段(重传)
- 更新重传超时(RTO)
- 根据 RTT 样本动态调整下一次超时时间
但问题来了:ARQ 并不简单
比如:
“发送方应等待多久才算超时?”答案不是固定的,而是基于 RTT 动态估算出的 RTO 值。
此外:
- 如果 ACK 丢失怎么办?
- 如果网络拥塞,会不会造成大量重传?
- 如果接收方收到了乱序数据,能不能提前确认?
- 多段数据连续丢失,会不会影响吞吐性能?
TCP 的进阶优化:SACK(选择性确认)
标准 TCP 只支持“累计确认” —— ACK 表示“我收到了某个序列号之前的所有数据”。
这样如果中间掉了一个包,后面的就都无法确认。
SACK(Selective Acknowledgment) 扩展允许接收方告诉发送方:
我收到了 哪些数据段(片段),即使它们是乱序的。
✅ 优势:
- 避免不必要的重传
- 提高高丢包网络下的吞吐率
- 尤其对 大窗口 + 高速链路 的 TCP 连接效果显著
小结:ARQ 是 TCP 可靠性的核心
模块 | 作用 |
RTO 计时器 | 控制超时时间 |
重传队列 | 保留未被确认的数据 |
ACK 确认机制 | 表示接收方收到了哪些数据 |
SACK 扩展 | 高效处理乱序接收场景 |
TCP 重传机制(TCP Retransmission)
在 TCP 协议中,重传是实现可靠性的重要机制。
基本机制:重传队列 + 定时器
当 TCP 发送一个数据段时,会将其副本放入重传队列中,并启动一个定时器(RTO)。如果在定时器超时前未收到对应 ACK,则重新发送该段,并重启定时器。
现代 RTO 算法(Jacobson/Karn RTT 估算)
为适应复杂多变的网络环境,RFC 6298 建议使用如下变量:
变量 | 含义 |
srtt | 平滑后的 RTT(Smoothed RTT) |
rttvar | RTT 的均方差(RTT Variation) |
rto | 最终计算得出的重传超时时间(单位 ms) |
G | 系统时钟精度,推荐 1ms(现代系统) |
初始状态(第一次测量前)
第一次测量后(RTT = R)
后续测量(每收到一个 ACK)
示例:C 语言伪实现
为什么 RTO 这么重要?
问题场景 | 没有 RTO 会发生什么? |
ACK 丢包 | 发送方永远不知道数据是否成功到达 |
中间路由器拥塞 | 未设置回退可能加剧拥堵 |
ACK 被延迟 | 错误重传 → 浪费资源 / 触发 SACK |
实际系统的优化(如 Linux)
- 默认最小 RTO 为 200ms(不是 1s)
- 支持快速重传(3 个重复 ACK)
- 精度为 1ms(时钟粒度
G
)
小结
元素 | 说明 |
重传队列 | 保存未确认段,记录 seq/time/data |
RTO 算法 | 控制等待时间,避免过早重传 |
srtt / rttvar | 动态估算 RTT,并调节超时时间 |
Karn 算法 | 避免用重传段的 ACK 更新 RTT |
快速重传 | 提高性能,减少依赖超时机制 |
Karn’s Algorithm(卡恩算法):避免 RTT 测量误差
在 TCP 协议的重传处理中,RTT(往返时间)估算是核心指标之一。
但重传机制会带来一个关键问题:
如果我们测量的是重传段的 RTT,那这个 RTT 可能是错的。
为了解决这个问题,Karn 和 Partridge 在 1987 年提出了著名的 ——
Karn’s Algorithm
为什么 RTT 不能测量重传段?
来看一个场景:
你收到的 ACK 到底是回应:
- 原始发送的 Segment 1,还是
- 重传的 Segment 1?
❌ 答案无法确定。
如果你用这个 ACK 来计算 RTT,结果将是不可靠甚至完全错误的。
Karn’s Algorithm 的核心规则
只使用未重传数据段的 ACK 来更新 RTT。
换句话说:
- 如果某个数据段被重传过,就不对它的 ACK 做 RTT 采样。
Karn 算法在实现中的体现
补充说明:带时间戳的 TCP(Timestamp Option)
如果启用了 TCP 的 Timestamp Option(RFC 7323),可以为每个段打上发送时间戳:
- 接收方会将时间戳原样“回显”在 ACK 中
- 这样就无论是否重传,都能准确测量 RTT(不依赖 ACK 顺序)
Timestamp 选项是现代 TCP 性能优化的关键组成部分,我们将在后续博客中详细讨论。
小结:Karn’s Algorithm 的意义
问题 | 解决方式 |
ACK 到底是响应哪个副本? | Karn 算法:不测量重传段的 RTT |
重传影响 RTT 准确性? | 是的,必须跳过测量 |
所有 ACK 都能估 RTT 吗? | 否,除非你启用了 Timestamp 选项 |
管理 TCP 的 RTO 定时器(Managing the RTO Timer)
RTO 定时器(Retransmission Timeout Timer) 是实现 TCP 自动重传(ARQ)的核心组成部分。
RFC 6298 为我们提供了推荐的管理方式。
RTO 定时器的管理逻辑(官方建议)
当发送数据段时:
- 如果 RTO 定时器尚未运行:
→ 启动定时器,超时值为当前的
rto
当所有待确认数据都已被 ACK:
- → 关闭定时器
当收到一个对新数据的 ACK(累计确认)时:
- → 重启定时器,超时时间仍为当前
rto
值
当 RTO 超时时(未收到 ACK):
- 重新发送 最早未被确认的数据段(SND.UNA)
- 将
rto
乘以 2(指数回退机制):
rto = rto * 2
- 重启定时器,使用回退后的新
rto
附加建议:
- 如果连续多次超时重传后,又重新收到一次有效 ACK,可以考虑:
- 重置
srtt
和rttvar
- 或者使用 新的 RTT 样本重新估算
rto
- 这样可以快速从网络恢复中恢复 RTO 的值,避免持续使用不合理的大超时时间
实现框架伪代码(C 风格)
小贴士:RTO 超时 ≠ 一定出错
- 网络瞬时抖动、ACK 丢包 都可能导致超时重传
- 不要过度依赖第一次超时的推断,应结合 RTT 曲线分析网络状态
- Linux 默认 RTO 最小值为 200ms,最大可达 60s+
总结
场景 | RTO 定时器动作 |
发送首个段 | 启动定时器 |
所有数据 ACK | 停止定时器 |
收到新数据的 ACK | 重启定时器 |
超时 | 重传 + RTO*2 + 重启 |
请求重传(Requesting Retransmission)
在 TCP 中,并不是只有发送方的超时机制(RTO)才负责发现丢包,
接收方也可以通过特定的确认方式告诉发送方:有数据段丢了,赶快重传!
方法一:重复 ACK(Duplicate ACK)
当接收方收到一个 乱序的数据段(即中间有缺失),它会:
- 不丢弃数据
- 继续发送一个 重复的 ACK
- ACK 的值仍然是最后一个按序接收的字节序号
如果发送方连续收到 3 个重复 ACK,则可判断:
有段丢失,接收方收到了后续数据但没收到中间的某个段 → 应该重传缺失段
快速重传(Fast Retransmit)
- 三个重复 ACK 是一个启发式判断:丢了!
- 立即重传丢失段,而不是等 RTO 超时
- 提高重传反应速度
- 避免不必要的等待和吞吐下降
示例:
方法二:SACK(Selective Acknowledgment)
SACK 是更强大的解决方案,属于 TCP 可选项(TCP Option),允许接收方:
- 告诉发送方:“我收到了哪些数据段,即使是乱序的”
- 不只报告“到哪儿收到了”,还能列出完整接收范围
- 发送方可精准地只重传真正缺失的部分
优势:
- 避免不必要的重传
- 适用于高带宽 / 高丢包 / 大窗口网络
- 和窗口滑动逻辑更好结合
我们将在后续专门讲解 TCP SACK 实现。
总结
方法 | 特点 |
重复 ACK | 快速、简单,只能指出一个段丢失 |
SACK | 精准、灵活,可以报告多个缺失段(更高效) |
实现建议(现在可以做的):
- 在接收端记录
rcv_nxt
之外的段(乱序)
- 每次乱序接收,发重复 ACK(即
ack = rcv_nxt
)
- 发送端记录最近的 ACK 序号及重复次数:
实操尝试:看看 TCP 重传在线上的表现
我们已经讲解了 TCP 的基本概念和算法,现在来实际观察 TCP 重传在网络中的行为。
1️⃣ 设置防火墙规则,模拟连接中断
我们先修改防火墙规则,在连接建立后丢弃所有经过
tap0
接口的后续数据包:然后我们尝试用
curl
获取 Hacker News 首页:2️⃣ 使用 tcpdump 观察数据包重传
通过
tcpdump
抓包,可以看到 TCP 发送 GET
请求后,每次重传间隔都差不多是上次的两倍:部分结果(摘录)如下:
🔁 TCP 正确地执行了**指数回退(Exponential Backoff)**策略。
3️⃣ 模拟只丢弃第六个数据包,测试部分数据段丢失情况
我们使用更精确的
iptables
规则,只丢弃第 6 个数据包(模拟 6000 字节的限制):然后发送一个大的 HTTP POST(大约 6009 字节):
4️⃣ 分析抓包输出
部分精简输出如下,并附有 TCP 内部状态说明:
小结
- TCP 能识别部分段丢失,并进行指数回退
- Karn 算法确保 RTT 测量不受重传干扰
- 网络恢复后,TCP 能自动降低 RTO 值
总结
TCP 重传机制是构建一个健壮网络协议栈不可或缺的一部分。
一个优秀的 TCP 实现必须能在网络环境变化时保持 健壮性与性能,例如:
- 网络延迟突然上升
- 网络路径暂时中断
- ACK 丢失或数据包失序
重传逻辑(基于 RTO + 重复 ACK + SACK 等)帮助 TCP 适应这些复杂环境,确保数据最终可靠送达。
下一步预告
接下来,我们将深入探索 TCP 拥塞控制(Congestion Control),它的目标是:
- 最大化吞吐量
- 同时避免“网络过载”
- 在稳定与收敛之间寻找平衡
我们会介绍并实现:
- 拥塞窗口(
cwnd
)和慢启动(slow start)
- 拥塞避免(congestion avoidance)
- 拥塞检测与恢复(AIMD、Fast Retransmit)