TCP/UDP 网络协议教程 / 05-TCP 可靠传输
05 - TCP 可靠传输
5.1 可靠传输机制概览
TCP 可靠传输的核心机制:
┌──────────────────────────────────────────────────────┐
│ 应用层数据 │
└──────────────┬───────────────────────────┬───────────┘
│ │
▼ ▼
┌──────────────────────┐ ┌──────────────────────┐
│ 序列号 (Seq) │ │ 确认应答 (ACK) │
│ 保证数据顺序 │ │ 保证数据到达 │
└──────────────────────┘ └──────────────────────┘
│ │
▼ ▼
┌──────────────────────┐ ┌──────────────────────┐
│ 滑动窗口 │ │ 超时重传 │
│ 提高传输效率 │ │ 丢失恢复 │
└──────────────────────┘ └──────────────────────┘
│ │
▼ ▼
┌──────────────────────┐ ┌──────────────────────┐
│ SACK │ │ 校验和 │
│ 选择性确认 │ │ 错误检测 │
└──────────────────────┘ └──────────────────────┘
5.2 确认应答机制 (ACK)
累积确认
发送方 接收方
│ │
│── Seq=1, 100B ──────────────────→ │
│── Seq=101, 100B ────────────────→ │
│── Seq=201, 100B ──X (丢失) │
│ │
│←──────── ACK=101 ──────────────│ │ 已收到 1-100
│←──────── ACK=101 ──────────────│ │ 重复 ACK
│ │
│ (接收方缓存了 101-200,等待 201) │
累积确认的特点:
• ACK=N 表示:已正确收到 N 之前的所有数据
• 即使后面的数据先到达,ACK 也不会跳过
延迟确认 (Delayed ACK)
# 延迟确认的工作方式
# 接收方不会立即发送 ACK,而是等待一段时间
"""
延迟确认的目的:
1. 减少网络中的小包数量
2. 可以和数据一起发送(ACK 搭载数据)
延迟时间:
• 通常 40-200ms
• 最多等待 2 个段
问题:可能与 Nagle 算法互相等待
解决方案:TCP_NODELAY 选项
"""
5.3 超时重传
RTO 计算
def calculate_rto(srtt, rttvar):
"""
计算重传超时时间 (RTO)
基于 RFC 6298 算法:
SRTT = 平滑 RTT
RTTVAR = RTT 偏差
"""
# RTO = SRTT + max(G, 4*RTTVAR)
# G = 时钟粒度(通常 1ms)
G = 0.001
rto = srtt + max(G, 4 * rttvar)
# 限制范围:1秒 - 60秒
return max(1.0, min(60.0, rto))
def update_rtt(srtt, rttvar, measured_rtt):
"""更新 RTT 估计值"""
alpha = 0.125 # 1/8
beta = 0.25 # 1/4
# RTTVAR = (1 - beta) * RTTVAR + beta * |SRTT - R|
rttvar = (1 - beta) * rttvar + beta * abs(srtt - measured_rtt)
# SRTT = (1 - alpha) * SRTT + alpha * R
srtt = (1 - alpha) * srtt + alpha * measured_rtt
return srtt, rttvar
# 示例
srtt = 0.1 # 初始 RTT 100ms
rttvar = 0.05 # 初始偏差 50ms
for measured in [0.1, 0.12, 0.09, 0.5, 0.1]: # 模拟 RTT 变化
srtt, rttvar = update_rtt(srtt, rttvar, measured)
rto = calculate_rto(srtt, rttvar)
print(f"RTT={measured:.3f}s, SRTT={srtt:.3f}s, RTO={rto:.3f}s")
重传机制
重传场景分析:
场景1:数据包丢失
发送方 ──Seq=1──X
发送方 ──Seq=1── (重传) → 接收方
接收方 ──ACK=2──→ 发送方
场景2:ACK 丢失
发送方 ──Seq=1──→ 接收方
发送方 ←──X───── ACK=2
发送方 ──Seq=1── (重传) → 接收方
接收方 ──ACK=2──→ 发送方(重复数据,丢弃)
场景3:超时
发送方 ──Seq=1──→ 接收方
(等待 RTO 超时)
发送方 ──Seq=1── (重传) → 接收方
5.4 快速重传 (Fast Retransmit)
正常情况:
发送方 ──Seq=1──→ 接收方
发送方 ←───────── ACK=2
快速重传:
发送方 ──Seq=1──→ 接收方
发送方 ──Seq=2──X (丢失)
发送方 ──Seq=3──→ 接收方 → ACK=2 (重复 ACK #1)
发送方 ──Seq=4──→ 接收方 → ACK=2 (重复 ACK #2)
发送方 ──Seq=5──→ 接收方 → ACK=2 (重复 ACK #3)
↑
收到 3 个重复 ACK
发送方 ──Seq=2── (快速重传) → 接收方
class FastRetransmit:
"""快速重传示例"""
def __init__(self):
self.dup_ack_count = {} # {seq: count}
self.threshold = 3 # 重复 ACK 阈值
def on_ack_received(self, ack_num):
"""收到 ACK 时的处理"""
if ack_num in self.dup_ack_count:
self.dup_ack_count[ack_num] += 1
if self.dup_ack_count[ack_num] >= self.threshold:
# 触发快速重传
return True
else:
# 新的 ACK,重置计数
self.dup_ack_count.clear()
self.dup_ack_count[ack_num] = 1
return False
5.5 SACK (选择性确认)
SACK 工作原理
没有 SACK:
发送方 ──Seq=1000──→ 接收方
发送方 ──Seq=2000──X (丢失)
发送方 ──Seq=3000──→ 接收方 → ACK=1001 (只确认 1000)
发送方 ──Seq=4000──→ 接收方 → ACK=1001 (只确认 1000)
发送方不知道 3000 和 4000 已收到
有 SACK:
发送方 ──Seq=1000──→ 接收方
发送方 ──Seq=2000──X (丢失)
发送方 ──Seq=3000──→ 接收方 → ACK=1001, SACK=3000-4000
发送方 ──Seq=4000──→ 接收方 → ACK=1001, SACK=3000-5000
发送方知道 3000-4999 已收到,只需重传 2000
SACK 选项格式
┌─────────────────────────────────────────────┐
│ Kind=5 │ Length │ Left Edge 1 │ Right Edge 1│
│ 1 byte │ 1 byte │ 4 bytes │ 4 bytes │
├────────┼────────┼─────────────┼─────────────┤
│ Left Edge 2 │ Right Edge 2 │ ... │
│ 4 bytes │ 4 bytes │ │
└─────────────┴──────────────┴───────────────┘
def parse_sack_block(data):
"""解析 SACK 块"""
import struct
blocks = []
for i in range(0, len(data), 8):
if i + 8 <= len(data):
left, right = struct.unpack('!II', data[i:i+8])
blocks.append((left, right))
return blocks
# 示例
sack_data = b'\x00\x00\x0B\xB8\x00\x00\x13\x88' # 3000-5000
blocks = parse_sack_block(sack_data)
print(f"SACK 块: {blocks}") # [(3000, 5000)]
5.6 滑动窗口 (Sliding Window)
窗口概念
发送方窗口:
已确认 可发送(在窗口内) 不可发送
┌────────┬───────────────────┬──────────┐
│▓▓▓▓▓▓▓▓│░░░░░░░░░░░░░░░░░░│ │
└────────┴───────────────────┴──────────┘
↑ ↑
SND.UNA SND.NXT + Window
SND.UNA: 最早未确认的序列号
SND.NXT: 下一个要发送的序列号
Window: 窗口大小
当收到 ACK 后:
┌────────────┬───────────────────┬──────────┐
│▓▓▓▓▓▓▓▓▓▓▓▓│░░░░░░░░░░░░░░░░░░│ │
└────────────┴───────────────────┴──────────┘
↑ 窗口滑动
class SlidingWindow:
"""滑动窗口实现"""
def __init__(self, window_size):
self.window_size = window_size
self.base = 0 # 窗口基序号(最早未确认)
self.next_seq = 0 # 下一个要发送的序列号
self.buffer = {} # 已发送未确认的数据
def can_send(self):
"""是否可以发送数据"""
return self.next_seq < self.base + self.window_size
def send(self, data):
"""发送数据"""
if not self.can_send():
return False
seq = self.next_seq
self.buffer[seq] = data
self.next_seq += len(data)
return seq
def receive_ack(self, ack_num):
"""收到 ACK"""
# 累积确认:释放 ack_num 之前的所有数据
while self.base < ack_num:
if self.base in self.buffer:
del self.buffer[self.base]
self.base += 1
@property
def available_window(self):
"""可用窗口大小"""
return self.window_size - (self.next_seq - self.base)
窗口大小与吞吐量
def calculate_throughput(window_size, rtt):
"""
计算最大吞吐量
吞吐量 = 窗口大小 / RTT
"""
throughput = window_size / rtt
throughput_mbps = throughput * 8 / 1_000_000
return throughput_mbps
# 不同窗口大小和 RTT 的吞吐量
print("窗口大小 | RTT 10ms | RTT 100ms | RTT 200ms")
print("-" * 50)
for window in [16384, 65535, 131072, 1048576]: # 16K, 64K, 128K, 1M
t10 = calculate_throughput(window, 0.01)
t100 = calculate_throughput(window, 0.1)
t200 = calculate_throughput(window, 0.2)
print(f"{window:>8} | {t10:>7.1f} | {t100:>8.1f} | {t200:>8.1f} Mbps")
5.7 Go-Back-N vs Selective Repeat
| 特性 | Go-Back-N | Selective Repeat |
|---|---|---|
| 重传范围 | 丢失帧及之后的所有帧 | 只重传丢失的帧 |
| 接收窗口 | 1 | > 1 |
| 缓存 | 只缓存按序到达的 | 缓存所有到达的 |
| 实现复杂度 | 简单 | 复杂 |
| 效率 | 低(大量重传) | 高 |
| TCP 使用 | 无 SACK 时 | 有 SACK 时 |
Go-Back-N:
发送 1 2 3 4 5
收到 1 2 _ 4 5 (3 丢失)
重传 3 4 5
Selective Repeat (SACK):
发送 1 2 3 4 5
收到 1 2 _ 4 5 (3 丢失)
只重传 3
5.8 重传相关内核参数
# 查看重传统计
$ netstat -s | grep -i retrans
123 segments retransmitted
45 fast retransmits
# 调整重传参数
# 最大重传次数(默认15)
$ sysctl net.ipv4.tcp_retries2
net.ipv4.tcp_retries2 = 15
# 早超时检测(Early Retransmit)
$ sysctl net.ipv4.tcp_early_retrans
net.ipv4.tcp_early_retrans = 3
# RACK(Recent ACKnowledgment)
$ sysctl net.ipv4.tcp_recovery
net.ipv4.tcp_recovery = 1
5.9 注意事项
⚠️ 重传歧义:收到 ACK 无法区分是原始段还是重传段的确认,Karn 算法规定重传段的 RTT 不参与 RTO 计算
⚠️ 失序包处理:网络设备可能导致包失序,TCP 需要区分失序和丢失
⚠️ SACK 支持:需要双方在握手时协商,不是所有实现都支持
5.10 扩展阅读
- RFC 6298 - Computing TCP’s Retransmission Timer
- RFC 2018 - TCP Selective Acknowledgment Options
- RFC 5681 - TCP Congestion Control
下一章:06 - TCP 流量控制 - 窗口大小、零窗口处理