Let's code a TCP/IP stack, 5: TCP Retransmission
2025-3-31
| 2025-4-4
Words 3771Read Time 10 min
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 实现包含以下步骤:
  1. 发送数据段
      • 把数据放入重传队列
      • 启动 定时器(RTO)
  1. 等待确认
      • 如果在定时器过期前收到了 ACK
        • 移除对应数据段,更新 SND.UNA
      • 如果超时未收到 ACK:
        • 重新发送数据段(重传)
  1. 更新重传超时(RTO)
      • 根据 RTT 样本动态调整下一次超时时间

但问题来了:ARQ 并不简单

比如:
“发送方应等待多久才算超时?”
答案不是固定的,而是基于 RTT 动态估算出的 RTO 值。
此外:
  • 如果 ACK 丢失怎么办?
  • 如果网络拥塞,会不会造成大量重传?
  • 如果接收方收到了乱序数据,能不能提前确认?
  • 多段数据连续丢失,会不会影响吞吐性能?

TCP 的进阶优化:SACK(选择性确认)

标准 TCP 只支持“累计确认” —— ACK 表示“我收到了某个序列号之前的所有数据”。
这样如果中间掉了一个包,后面的就都无法确认。
SACK(Selective Acknowledgment) 扩展允许接收方告诉发送方:
我收到了 哪些数据段(片段),即使它们是乱序的。
✅ 优势:
  • 避免不必要的重传
  • 提高高丢包网络下的吞吐率
  • 尤其对 大窗口 + 高速链路 的 TCP 连接效果显著

小结:ARQ 是 TCP 可靠性的核心

模块
作用
RTO 计时器
控制超时时间
重传队列
保留未被确认的数据
ACK 确认机制
表示接收方收到了哪些数据
SACK 扩展
高效处理乱序接收场景

TCP 重传机制(TCP Retransmission)


在 TCP 协议中,重传是实现可靠性的重要机制。
从最早的 RFC 793 到后来的 RFC 6298重传超时(RTO)算法已经历多次改进和优化。

基本机制:重传队列 + 定时器

当 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):

  1. 重新发送 最早未被确认的数据段(SND.UNA)
  1. rto 乘以 2(指数回退机制):
    1. rto = rto * 2
  1. 重启定时器,使用回退后的新 rto

附加建议:

  • 如果连续多次超时重传后,又重新收到一次有效 ACK,可以考虑:
    • 重置 srttrttvar
    • 或者使用 新的 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)
  • tcp/ip
  • 网络
  • Everything you need to know about Python 3.13 – JIT and GIL went up the hillLet's code a TCP/IP stack, 4: TCP Data Flow & Socket API
    Loading...
    Catalog
    0%