强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

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-NSelective 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 扩展阅读


下一章06 - TCP 流量控制 - 窗口大小、零窗口处理