第16天:TCP可靠传输机制
今日目标
- 深入理解TCP序列号和确认号机制
- 掌握TCP滑动窗口原理
- 理解超时重传机制
- 掌握快速重传和快速恢复算法
- 理解TCP流量控制
- 理解TCP拥塞控制算法
- 实现TCP可靠性分析工具
1. TCP可靠传输概述
1.1 什么是可靠传输?
可靠传输的保证:
1. 数据无差错
└─ 通过校验和检测
2. 数据不丢失
└─ 通过确认和重传
3. 数据不重复
└─ 通过序列号去重
4. 数据按序到达
└─ 通过序列号排序
5. 流量控制
└─ 防止发送方发送过快
6. 拥塞控制
└─ 防止网络过载
1.2 可靠传输的核心机制
┌─────────────────────────────────────────┐
│ TCP可靠传输机制 │
├─────────────────────────────────────────┤
│ 1. 序列号 (Sequence Number) │
│ └─ 标识每个字节 │
│ │
│ 2. 确认应答 (ACK) │
│ └─ 确认收到的数据 │
│ │
│ 3. 超时重传 (Retransmission) │
│ └─ 超时未收到ACK则重传 │
│ │
│ 4. 滑动窗口 (Sliding Window) │
│ └─ 流量控制和效率提升 │
│ │
│ 5. 快速重传 (Fast Retransmit) │
│ └─ 收到3个重复ACK立即重传 │
│ │
│ 6. 拥塞控制 (Congestion Control) │
│ └─ 慢启动、拥塞避免、快速恢复 │
└─────────────────────────────────────────┘
2. 序列号和确认号
2.1 序列号机制
序列号(Sequence Number):
作用:标识发送的每个字节在字节流中的位置
特点:
- 32位,范围:0 ~ 4,294,967,295 (约4GB)
- 超过最大值会回绕(wraparound)
- 初始序列号(ISN)在连接时随机选择
示例:
┌─────────────────────────────────────────┐
│ 字节流:HELLO WORLD │
├─────────────────────────────────────────┤
│ 位置: 0 1 2 3 4 5 6 7 8 9 10 │
│ 字符: H E L L O W O R L D │
└─────────────────────────────────────────┘
如果ISN = 1000:
- 'H' 的序列号:1000
- 'E' 的序列号:1001
- 'L' 的序列号:1002
- ...
- 'D' 的序列号:1010
TCP段的序列号:
TCP段的序列号 = 该段第一个字节的序列号
示例:
发送 "HELLO WORLD" (11字节)
方案1:一次发送
┌──────────────────────────────┐
│ SEQ=1000, Data="HELLO WORLD" │
│ 长度:11字节 │
└──────────────────────────────┘
方案2:分两次发送
┌──────────────────────────────┐
│ SEQ=1000, Data="HELLO " │ (6字节)
│ SEQ=1006, Data="WORLD" │ (5字节)
└──────────────────────────────┘
下一个段的SEQ:
= 当前SEQ + 当前段数据长度
= 1000 + 11 = 1011
2.2 确认号机制
确认号(Acknowledgment Number):
作用:告诉对方期望接收的下一个字节的序列号
含义:
ACK = n 表示 "我已经收到了序列号 < n 的所有数据"
特点:
- 累积确认(Cumulative ACK)
- 确认号之前的所有数据都已收到
确认示例:
发送方 接收方
| |
| SEQ=1000, Len=100 ("数据1") |
|─────────────────────────────────────────>|
| |
| ACK=1100 |
|<─────────────────────────────────────────|
| (已收到1000-1099,期望1100) |
| |
| SEQ=1100, Len=50 ("数据2") |
|─────────────────────────────────────────>|
| |
| ACK=1150 |
|<─────────────────────────────────────────|
| (已收到1100-1149,期望1150) |
累积确认的问题:
发送方发送了3个段:
段1: SEQ=1000, Len=100
段2: SEQ=1100, Len=100
段3: SEQ=1200, Len=100
情况1:全部收到
接收方回复:ACK=1300 ✅
情况2:段2丢失
段1到达 → 回复 ACK=1100
段3到达 → 还是回复 ACK=1100(期望1100)
问题:发送方不知道段3是否收到
解决:SACK(选择性确认)扩展
2.3 序列号回绕问题
序列号是32位:0 ~ 4,294,967,295 (约4GB)
高速网络问题:
- 10 Gbps 网络
- 发送速度:1.25 GB/s
- 序列号耗尽时间:4GB ÷ 1.25GB/s ≈ 3.2秒
解决方案:
1. PAWS (Protection Against Wrapped Sequence numbers)
└─ 使用时间戳选项
2. TCP窗口扩大选项
└─ 减少数据段数量
3. 滑动窗口机制
3.1 为什么需要滑动窗口?
停止等待协议的问题:
停止等待 (Stop-and-Wait):
1. 发送一个段
2. 等待ACK
3. 收到ACK后发送下一个段
效率:非常低
示例:
RTT = 100ms
数据段大小 = 1KB
时间线:
t=0ms: 发送段1
t=100ms: 收到ACK1
t=100ms: 发送段2
t=200ms: 收到ACK2
...
吞吐量 = 1KB / 100ms = 10KB/s = 80Kbps
即使带宽是1Gbps,也只能用到80Kbps!❌
滑动窗口解决方案:
连续发送多个段,不用等待ACK
示例(窗口大小=4):
t=0ms: 发送段1、段2、段3、段4
t=100ms: 收到ACK1,窗口滑动,发送段5
t=105ms: 收到ACK2,窗口滑动,发送段6
...
吞吐量大幅提升!✅
3.2 发送窗口
发送窗口结构:
字节流:
┌────────────────────────────────────────────────────┐
│ 已发送 │ 已发送 │ 可以发送 │ 不能发送 │
│ 已确认 │ 未确认 │ (窗口内) │ (窗口外) │
└────────────────────────────────────────────────────┘
↑ ↑ ↑ ↑
P1 P2 P3 P4
P1: 已发送并已确认的最后一个字节
P2: 已发送但未确认的最后一个字节
P3: 允许发送的最后一个字节
P4: 未来的数据
发送窗口 = P3 - P1
可用窗口 = P3 - P2
已发送未确认 = P2 - P1
窗口滑动过程:
初始状态(窗口大小=6):
┌────┬────┬────┬────┬────┬────┬────┬────┬────┐
│ 1 │ 2 │ 3 │ 4 │ 5 │ 6 │ 7 │ 8 │ 9 │
└────┴────┴────┴────┴────┴────┴────┴────┴────┘
└────────窗口────────┘
可发送:1,2,3,4,5,6
步骤1:发送1,2,3
┌────┬────┬────┬────┬────┬────┬────┬────┬────┐
│ 1* │ 2* │ 3* │ 4 │ 5 │ 6 │ 7 │ 8 │ 9 │
└────┴────┴────┴────┴────┴────┴────┴────┴────┘
└────────窗口────────┘
*=已发送
步骤2:收到ACK for 1,2
┌────┬────┬────┬────┬────┬────┬────┬────┬────┐
│ 1✓ │ 2✓ │ 3* │ 4 │ 5 │ 6 │ 7 │ 8 │ 9 │
└────┴────┴────┴────┴────┴────┴────┴────┴────┘
└────────窗口────────┘
窗口向右滑动2格
步骤3:可以发送7,8
┌────┬────┬────┬────┬────┬────┬────┬────┬────┐
│ 1✓ │ 2✓ │ 3* │ 4 │ 5 │ 6 │ 7 │ 8 │ 9 │
└────┴────┴────┴────┴────┴────┴────┴────┴────┘
└────────窗口────────┘
发送7,8
3.3 接收窗口
接收窗口结构:
字节流:
┌────────────────────────────────────────────────────┐
│ 已接收 │ 可以接收 │ 不能接收 │
│ 已确认 │ (窗口内) │ (窗口外) │
└────────────────────────────────────────────────────┘
↑ ↑ ↑
Q1 Q2 Q3
Q1: 已接收并已确认的最后一个字节
Q2: 期望接收的最后一个字节
Q3: 不能接收的第一个字节
接收窗口 = Q3 - Q1
乱序到达的处理:
期望接收顺序:1, 2, 3, 4, 5
实际到达顺序:1, 3, 2, 5, 4
处理:
1. 收到1 → 交付应用层,ACK=2
2. 收到3 → 缓存(等待2),ACK=2
3. 收到2 → 交付1,2,3,ACK=4
4. 收到5 → 缓存(等待4),ACK=4
5. 收到4 → 交付4,5,ACK=6
策略:
- 按序交付给应用层
- 乱序数据暂存在缓冲区
- 累积确认
3.4 窗口大小的影响
带宽延迟积(BDP):
BDP = 带宽 × RTT
示例:
带宽 = 100 Mbps = 12.5 MB/s
RTT = 100 ms = 0.1 s
BDP = 12.5 MB/s × 0.1s = 1.25 MB
含义:
- 在收到ACK前,可以发送1.25MB数据
- 窗口大小应该 ≥ BDP
- 否则无法充分利用带宽
窗口大小限制:
TCP头部窗口字段:16位
最大窗口大小:2^16 = 65536 字节 = 64 KB
问题:
高速长延迟网络(如卫星链路)需要更大窗口
解决:
窗口扩大选项 (Window Scale Option)
实际窗口 = 窗口字段值 × 2^Scale
最大Scale = 14
最大窗口 = 65536 × 2^14 = 1 GB
4. 超时重传机制
4.1 超时重传原理
发送方行为:
1. 发送数据段
2. 启动重传计时器
3. 如果超时仍未收到ACK
└─ 重传该数据段
4. 收到ACK
└─ 取消计时器
超时重传示例:
发送方 接收方
| |
| SEQ=1000, Len=100 |
|─────────────────────────────────────────>|
| [启动计时器] |
| |
| ❌ 数据丢失
| |
| [超时!] |
| SEQ=1000, Len=100 (重传) |
|─────────────────────────────────────────>|
| |
| ACK=1100 |
|<─────────────────────────────────────────|
| ✅ 收到ACK,取消计时器 |
4.2 重传超时时间(RTO)的计算
问题:RTO设置多少合适?
RTO太小:
- 不必要的重传
- 浪费带宽
- 加重网络负担
RTO太大:
- 重传延迟大
- 影响性能
- 用户体验差
理想:RTO 略大于 RTT
RTO计算算法(Jacobson算法):
1. 测量RTT样本(每次收到ACK时)
RTT_sample = ACK到达时间 - 发送时间
2. 计算平滑RTT(SRTT)
SRTT = (1 - α) × SRTT + α × RTT_sample
α = 0.125 (1/8)
3. 计算RTT变化(RTTVAR)
RTTVAR = (1 - β) × RTTVAR + β × |SRTT - RTT_sample|
β = 0.25 (1/4)
4. 计算RTO
RTO = SRTT + 4 × RTTVAR
最小值:200ms(Linux默认)
最大值:120秒
示例:
假设:
RTT样本序列:100ms, 110ms, 90ms, 105ms
初始:SRTT=100ms, RTTVAR=0
第1次:RTT=110ms
SRTT = 0.875×100 + 0.125×110 = 101.25ms
RTTVAR = 0.75×0 + 0.25×|100-110| = 2.5ms
RTO = 101.25 + 4×2.5 = 111.25ms
第2次:RTT=90ms
SRTT = 0.875×101.25 + 0.125×90 = 99.84ms
RTTVAR = 0.75×2.5 + 0.25×|101.25-90| = 4.69ms
RTO = 99.84 + 4×4.69 = 118.6ms
4.3 重传歧义问题
问题:重传后收到ACK,这个ACK是对原始段还是重传段的确认?
场景1:ACK是对原始段的确认
发送方 接收方
| SEQ=1000 |
|─────────────────────────────────────────>|
| [启动计时器] |
| |
| ACK=1100 (延迟) |
| [超时!重传] ↓ |
| SEQ=1000 (重传) ↓ |
|──────────────> ↓ |
| ↓ |
|<──────────────────────────┘ |
| 收到ACK |
RTT计算:如果用重传时间,会高估RTT ❌
场景2:ACK是对重传段的确认
发送方 接收方
| SEQ=1000 |
|─────────────> ❌ 丢失 |
| [启动计时器] |
| [超时!重传] |
| SEQ=1000 (重传) |
|─────────────────────────────────────────>|
| |
| ACK=1100 |
|<─────────────────────────────────────────|
RTT计算:如果用原始发送时间,会高估RTT ❌
Karn算法解决方案:
规则:
1. 重传的段不用于RTT计算
2. 只用成功传输(未重传)的段计算RTT
缺点:
如果连续丢包,无法更新RTO
解决:
结合指数退避(Exponential Backoff)
每次重传,RTO翻倍
RTO_new = RTO_old × 2
5. 快速重传与快速恢复
5.1 快速重传(Fast Retransmit)
问题:超时重传太慢
传统超时重传:
- 等待RTO超时(可能几百毫秒)
- 影响传输效率
快速重传原理:
规则:
收到3个重复ACK,立即重传,不等超时
为什么是3个?
- 1个重复ACK:可能是乱序到达
- 2个重复ACK:可能还是乱序
- 3个重复ACK:很可能是丢包
快速重传示例:
发送方 接收方
| |
| SEQ=1000, Len=100 |
|─────────────────────────────────────────>|
| ACK=1100 |
|<─────────────────────────────────────────|
| |
| SEQ=1100, Len=100 |
|─────────> ❌ 丢失 |
| |
| SEQ=1200, Len=100 |
|─────────────────────────────────────────>|
| ACK=1100 (重复1) |
|<─────────────────────────────────────────|
| |
| SEQ=1300, Len=100 |
|─────────────────────────────────────────>|
| ACK=1100 (重复2) |
|<─────────────────────────────────────────|
| |
| SEQ=1400, Len=100 |
|─────────────────────────────────────────>|
| ACK=1100 (重复3) |
|<─────────────────────────────────────────|
| |
| 🚀 收到3个重复ACK,立即重传! |
| SEQ=1100, Len=100 (快速重传) |
|─────────────────────────────────────────>|
| ACK=1500 |
|<─────────────────────────────────────────|
| ✅ 成功恢复 |
5.2 SACK(选择性确认)
累积ACK的局限:
发送:段1(1000), 段2(1100), 段3(1200), 段4(1300)
接收:段1✓, 段2✗, 段3✓, 段4✓
累积ACK只能发送:ACK=1100
发送方不知道段3和段4是否收到
问题:
- 发送方可能重传段2、3、4(浪费)
- 只需要重传段2即可
SACK解决方案:
SACK选项格式:
┌─────────────────────────────────────────┐
│ Kind=5 │ Length │ Left Edge │Right Edge │
└─────────────────────────────────────────┘
示例:
接收到:1000-1099, 1200-1299, 1300-1399
SACK选项:
ACK=1100
SACK: 1200-1300, 1300-1400
含义:
- 期望收到1100
- 但已经收到1200-1399的数据
发送方行为:
- 只重传1100-1199
- 不重传1200-1399 ✅
5.3 快速恢复(Fast Recovery)
快速恢复算法:
触发:收到3个重复ACK时
步骤:
1. 拥塞窗口减半
cwnd = cwnd / 2
ssthresh = cwnd
2. 快速重传丢失的段
3. 每收到一个重复ACK
cwnd = cwnd + 1 (临时膨胀)
4. 收到新的ACK
cwnd = ssthresh (恢复正常)
进入拥塞避免阶段
目的:
- 快速恢复,不回到慢启动
- 保持较高的发送速率
6. 流量控制
6.1 流量控制的目的
问题:
发送方发送速度 > 接收方处理速度
接收方缓冲区溢出,数据丢失
解决:
接收方通过窗口大小告诉发送方还能接收多少数据
6.2 流量控制机制
接收窗口通告:
接收方在每个ACK中通告窗口大小
示例:
接收缓冲区大小:8KB
已接收未读:2KB
可用空间:6KB
发送ACK:
ACK=1000, Window=6144 (6KB)
发送方:
- 最多再发送6KB数据
- 发送后等待ACK和新的窗口通告
零窗口问题:
场景:
1. 接收方缓冲区满,发送Window=0
2. 发送方停止发送
3. 接收方处理数据,缓冲区有空间
4. 但如果ACK丢失,发送方不知道可以继续发送
解决:零窗口探测
发送方定期发送零窗口探测段(1字节数据)
询问接收方是否有可用窗口
糊涂窗口综合症(Silly Window Syndrome):
问题:
接收方每次只读取很少数据
导致频繁发送小窗口通告
发送方发送大量小数据段
效率低下
示例:
缓冲区8KB,应用每次读1字节
Window序列:0, 1, 0, 1, 0, 1...
效率:极低 ❌
解决方案:
1. 接收方(Clark算法)
- 缓冲区空间 < MSS时,通告Window=0
- 直到有足够空间(MSS或缓冲区一半)
2. 发送方(Nagle算法)
- 等待数据累积到MSS大小再发送
- 或收到之前数据的ACK再发送
7. 拥塞控制
7.1 拥塞控制 vs 流量控制
流量控制(Flow Control):
- 目标:防止接收方过载
- 机制:接收窗口(rwnd)
- 控制:接收方控制发送方
拥塞控制(Congestion Control):
- 目标:防止网络过载
- 机制:拥塞窗口(cwnd)
- 控制:发送方自己判断
发送窗口 = min(rwnd, cwnd)
7.2 拥塞控制算法
1. 慢启动(Slow Start)
目的:
连接初期探测网络容量
算法:
1. 初始cwnd = 1 MSS (或更大,如10 MSS)
2. 每收到一个ACK,cwnd += 1
3. 效果:cwnd指数增长(每个RTT翻倍)
示例:
RTT 0: cwnd=1, 发送1个段
RTT 1: cwnd=2, 发送2个段
RTT 2: cwnd=4, 发送4个段
RTT 3: cwnd=8, 发送8个段
...
终止条件:
- cwnd >= ssthresh(慢启动阈值)
→ 进入拥塞避免
- 发生丢包
→ 进入拥塞避免或快速恢复
2. 拥塞避免(Congestion Avoidance)
目的:
小心翼翼地增加窗口,避免拥塞
算法:
每收到一个ACK:
cwnd = cwnd + 1/cwnd
效果:每个RTT,cwnd增加1
示例:
RTT 0: cwnd=16
RTT 1: cwnd=17
RTT 2: cwnd=18
RTT 3: cwnd=19
...
特点:线性增长(比慢启动慢得多)
3. 拥塞发生
情况1:超时(丢包严重)
行为:
- ssthresh = cwnd / 2
- cwnd = 1 MSS
- 重新慢启动
情况2:3个重复ACK(丢包较轻)
行为:
- ssthresh = cwnd / 2
- cwnd = ssthresh + 3
- 快速重传 + 快速恢复
4. 快速恢复(Fast Recovery)
算法:
1. ssthresh = cwnd / 2
2. cwnd = ssthresh + 3
3. 重传丢失的段
4. 每收到重复ACK,cwnd += 1(临时膨胀)
5. 收到新ACK时,cwnd = ssthresh
6. 进入拥塞避免
目的:
- 快速恢复,不回到慢启动
- 维持较高吞吐量
7.3 拥塞控制状态图
[连接建立]
↓
┌──────────┐
│ 慢启动 │ cwnd指数增长
│ │ 每ACK: cwnd += 1
└──────────┘
↓
cwnd >= ssthresh
↓
┌──────────┐
│拥塞避免 │ cwnd线性增长
│ │ 每RTT: cwnd += 1
└──────────┘
↓
发生丢包
↙ ↘
超时 3个重复ACK
↓ ↓
┌──────────┐ ┌──────────┐
│ 慢启动 │ │快速恢复 │
│cwnd=1 │ │cwnd=ssthresh/2+3│
└──────────┘ └──────────┘
↓ ↓
└───────┬───────┘
↓
┌──────────┐
│拥塞避免 │
└──────────┘
7.4 拥塞控制变体
TCP Reno(经典算法):
特点:
- 慢启动 + 拥塞避免 + 快速重传 + 快速恢复
- 最经典的TCP实现
问题:
- 对多个丢包处理不好
- 每个RTT只能恢复一个丢包
TCP NewReno:
改进:
- 改进的快速恢复
- 可以在一个RTT内恢复多个丢包
特点:
- 部分ACK触发重传
- 直到所有丢失数据恢复才退出快速恢复
TCP CUBIC(Linux默认):
特点:
- 窗口增长函数:cwnd = C(t - K)^3 + W_max
- 更适合高带宽长延迟网络
优点:
- 快速探测可用带宽
- 公平性更好
- 高速网络性能更好
TCP BBR(Google):
理念:
- 不基于丢包,基于RTT和带宽
- 主动探测瓶颈带宽和RTT
特点:
- 4个状态:Startup, Drain, ProbeBW, ProbeRTT
- 更高吞吐量
- 更低延迟
8. 实战项目:TCP可靠性分析工具
8.1 项目功能
实现工具分析:
- TCP连接的重传统计
- RTT和RTO监控
- 窗口大小变化
- 拥塞控制状态
8.2 工具演示
# 监控TCP重传
python day16_tcp_reliability.py --monitor-retrans
# 分析RTT
python day16_tcp_reliability.py --analyze-rtt
# 模拟拥塞控制
python day16_tcp_reliability.py --simulate-congestion
# 窗口大小分析
python day16_tcp_reliability.py --window-analysis
9. 今日练习
练习1:观察重传
使用Wireshark:
- 访问一个网站
- 筛选:
tcp.analysis.retransmission - 观察重传的段
- 分析重传原因(超时/快速重传)
练习2:计算RTO
给定RTT样本:100ms, 120ms, 90ms, 110ms
计算:
1. SRTT (Smoothed RTT)
2. RTTVAR (RTT Variation)
3. RTO (Retransmission Timeout)
使用公式:
SRTT = (1-α) × SRTT + α × RTT_sample
RTTVAR = (1-β) × RTTVAR + β × |SRTT - RTT_sample|
RTO = SRTT + 4 × RTTVAR
练习3:模拟拥塞控制
场景:
- 初始cwnd = 1 MSS
- ssthresh = 16 MSS
- MSS = 1460 字节
问题:
1. 经过多少个RTT,cwnd达到16?
2. 如果在cwnd=20时发生超时,新的cwnd和ssthresh是多少?
3. 如果在cwnd=20时收到3个重复ACK,cwnd和ssthresh是多少?
练习4:查看TCP统计
# Linux查看TCP统计
netstat -s | grep -i tcp
ss -ti # 查看TCP详细信息
# 查看重传统计
netstat -s | grep retrans
# 查看拥塞控制算法
sysctl net.ipv4.tcp_congestion_control
10. 常见问题
Q1:为什么是3个重复ACK触发快速重传,而不是2个或4个?
A:这是经验值的平衡:
- 2个:可能误判(乱序到达)
- 3个:较可靠地判断丢包
- 4个:太保守,影响效率
Q2:TCP如何区分网络拥塞和接收方慢?
A:
- 网络拥塞:超时或3个重复ACK
- 接收方慢:Window字段变小
- TCP会相应调整发送速度
Q3:为什么慢启动要指数增长?
A:
- 快速探测可用带宽
- 避免初期过于保守
- 遇到拥塞时能快速反应
Q4:选择性确认(SACK)是必须的吗?
A:
- 不是必须,但强烈推荐
- 没有SACK时效率较低
- 现代TCP实现基本都支持SACK
11. 总结
今天我们深入学习了:
核心知识点
序列号和确认号:
- 序列号标识字节位置
- 确认号表示期望接收的下一个字节
- 累积确认机制
滑动窗口:
- 提高传输效率
- 发送窗口和接收窗口
- 窗口大小 = min(rwnd, cwnd)
超时重传:
- RTO动态计算(Jacobson算法)
- Karn算法解决重传歧义
- 指数退避
快速重传和快速恢复:
- 3个重复ACK触发
- 不等超时,立即重传
- SACK提高效率
流量控制:
- 防止接收方过载
- 零窗口处理
- 糊涂窗口综合症
拥塞控制:
- 慢启动(指数增长)
- 拥塞避免(线性增长)
- 快速恢复
- 多种算法变体
重点回顾
TCP可靠传输 = 序列号 + 确认 + 重传 + 窗口
效率优化:
- 滑动窗口(批量发送)
- 快速重传(不等超时)
- SACK(选择性确认)
网络友好:
- 流量控制(保护接收方)
- 拥塞控制(保护网络)
明天预告
第17天:UDP协议详解
内容预览:
- UDP报文格式
- UDP vs TCP深入对比
- UDP应用场景
- UDP编程实战
- 基于UDP实现可靠传输
继续加油! 🚀
今天我们学习了TCP如何实现可靠传输,这是TCP最核心的功能。理解这些机制不仅能帮助你更好地使用TCP,还能在遇到网络问题时快速定位原因!