序列号与确认机制
TCP 的字节流抽象
TCP 把应用数据视为无结构的字节流,每个字节都有隐式编号。发送方不保留消息边界——应用层发送 "Hello" 和 "World" 两个消息,TCP 可能合并成一个报文段发送,也可能拆成多个。
序列号空间
序列号是 32 位无符号整数(0 ~ 2^32-1,约 42.9 亿),采用模 2^32 运算处理回绕。
初始序列号(ISN)不是 0!RFC 793 建议基于时钟生成(每 4 微秒 +1),现代系统采用随机化策略防止序列号预测攻击。
ISN 生成(Linux 现代实现):
- 基于微秒级时钟 + 随机偏移
- 每个新连接使用不同的 ISN
- 防止攻击者预测下一个连接的序列号
累积确认机制
接收方发送的 ACK 号 = 期望收到的下一个字节的序号,隐含表示该序号之前的所有数据均已正确接收。
累积确认的优点:
- 减少 ACK 报文数量(一个 ACK 确认多个段)
- 实现简单
累积确认的缺点:
- 对乱序段处理模糊:收到段 1 和段 3,段 2 丢失,只能重复 ACK=段2起始序号
- 发送方不知道段 3 已收到,可能重传段 3(浪费带宽)
序列号回绕与 PAWS
高速网络(如 10Gbps)中,序列号可能在短时间内回绕(Wrap Around):
带宽 10Gbps = 1.25 GB/s
序列号空间 4GB
回绕时间 = 4GB / 1.25GB/s = 3.2 秒
如果旧连接的延迟报文在新连接中到达,且序列号恰好落在接收窗口内,会导致数据混淆!
PAWS(Protection Against Wrapped Sequence numbers,RFC 7323):
利用时间戳选项(Timestamp)区分新旧报文:
- 每个报文携带发送时刻的时间戳 TSval
- 接收方记录最近收到的时间戳
- 如果收到的时间戳小于记录值,判定为旧报文,丢弃
选择确认 SACK(RFC 2018)
SACK 解决累积确认的缺陷,允许接收方精确通告已收到的乱序段边界。
SACK 选项格式:
- 最多通告 3~4 个块(受选项空间 40 字节限制)
- 每个块 = {左边界, 右边界}
- 第一个块是最新收到的乱序段
SACK 的局限:
- 选项空间有限,大量乱序时可能无法全部通告
- 发送方需要实现复杂的重传策略(如 Scoreboard)
现代操作系统(Linux、Windows、macOS)默认启用 SACK。
重复 ACK 与快速重传触发
当接收方收到乱序段时,立即发送重复 ACK(Duplicate ACK):
收到段 1 (Seq=1000) → ACK=1100
收到段 3 (Seq=1200) → ACK=1100 (重复!)
收到段 4 (Seq=1300) → ACK=1100 (重复!)
收到段 5 (Seq=1400) → ACK=1100 (重复!)
发送方收到 3 个重复 ACK(即第 4 个相同的 ACK)时,触发快速重传:立即重传丢失段,无需等待 RTO 超时。
为什么是 3 个? 太少容易误判(网络轻微乱序),太多延迟重传。RFC 5681 建议 3 个,Linux 可通过 net.ipv4.tcp_reordering 调整。
本篇小结
- TCP 把数据视为无结构字节流,每个字节有隐式编号
- 序列号 32 位,模 2^32 运算,ISN 随机生成
- 累积确认:ACK=N 表示 N 之前的所有字节已收到
- 累积确认缺陷:乱序时只能重复 ACK,发送方不知哪些段已收到
- SACK:精确通告乱序段边界,仅重传真正丢失的段
- PAWS:时间戳选项防止高速网络序列号回绕
- 3 个重复 ACK 触发快速重传
动手实践
Wireshark 抓包观察 SACK:
- 过滤
tcp.options.sack或查看 TCP 选项中的 SACK 块 - 模拟丢包(
tc qdisc add dev eth0 root netem loss 5%),观察 SACK 与快速重传
- 过滤
查看 Linux SACK 是否启用:
sysctl net.ipv4.tcp_sack查看 TCP 连接的时间戳选项:
ss -ti | grep ts思考:如果没有 SACK,收到段 1、3、4、5(段 2 丢失),发送方会重传哪些段?有 SACK 时又会重传哪些?