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

TCP/UDP 网络协议教程 / 08-TCP 选项机制

08 - TCP 选项机制

8.1 TCP 选项格式

TCP 选项格式:
┌────────┬────────┬─────────────────────┐
│ Kind   │ Length │ Data                │
│ 1 byte │ 1 byte │ 0-40 bytes          │
└────────┴────────┴─────────────────────┘

特殊选项:
• Kind=0: EOL(选项结束)
• Kind=1: NOP(无操作,用于填充对齐)

所有选项必须在 40 字节以内(头部长度最大 60 - 固定头部 20)

8.2 MSS (Maximum Segment Size)

MSS = TCP 数据部分的最大长度
不包括 TCP 和 IP 头部

典型值计算:
以太网 MTU = 1500 字节
IP 头部 = 20 字节
TCP 头部 = 20 字节
MSS = 1500 - 20 - 20 = 1460 字节
MSS 协商过程:
┌──────────────────────────────────────────────────────────┐
│                    三次握手                                │
│                                                          │
│  Client ──SYN, MSS=1460──→ Server                      │
│  Client ←──SYN+ACK, MSS=1452── Server (PPPoE)          │
│                                                          │
│  实际使用 MSS = min(1460, 1452) = 1452 字节             │
└──────────────────────────────────────────────────────────┘
网络类型MTUMSS
以太网15001460
PPPoE14921452
IPv6 最小12801220
Wi-Fi23042264
import struct

def parse_mss_option(data):
    """解析 MSS 选项"""
    # Kind=2, Length=4, Value=2 bytes
    if len(data) >= 4 and data[0] == 2 and data[1] == 4:
        mss = struct.unpack('!H', data[2:4])[0]
        return mss
    return None

# 解析示例
mss_option = bytes([2, 4, 0x05, 0xB4])  # MSS = 1460
mss = parse_mss_option(mss_option)
print(f"MSS: {mss}")  # MSS: 1460

MSS 与路径 MTU 发现 (PMTUD)

PMTUD 工作原理:

1. 发送方设置 IP 头部 DF 位(Don't Fragment)
2. 发送 MSS 大小的段
3. 如果路径上某路由器 MTU < MSS:
   - 路由器丢弃包
   - 返回 ICMP "需要分片" 消息
4. 发送方减小 MSS,重试

问题:
• 某些防火墙阻止 ICMP → PMTUD 失败 → 连接卡住
• 解决方案:TCP MSS Clamping(中间设备修改 MSS)

8.3 窗口缩放 (Window Scale)

问题:
TCP 头部窗口字段只有 16 位
最大值 = 65535 字节 = 64KB
对于高延迟高带宽网络太小

解决方案:
窗口缩放选项,在握手时协商缩放因子

┌────────────┬────────────┬────────────┐
│ Kind=3     │ Length=3    │ Shift Count│
│ 1 byte     │ 1 byte     │ 1 byte     │
└────────────┴────────────┴────────────┘

实际窗口 = 窗口字段值 << Shift Count
最大 Shift Count = 14
最大窗口 = 65535 × 2^14 ≈ 1GB
def window_scale_calculation():
    """窗口缩放计算示例"""
    
    # 握手时协商的缩放因子
    sender_scale = 7   # 发送方的缩放因子
    receiver_scale = 8  # 接收方的缩放因子
    
    # 窗口字段值(16 位)
    window_field = 65535
    
    # 发送方的窗口(使用接收方通告的缩放因子)
    actual_window = window_field << receiver_scale
    print(f"接收方窗口: {actual_window:,} 字节 = {actual_window/1024/1024:.2f} MB")
    
    # 接收方的窗口(使用发送方通告的缩放因子)
    actual_window = window_field << sender_scale
    print(f"发送方窗口: {actual_window:,} 字节 = {actual_window/1024/1024:.2f} MB")

window_scale_calculation()

窗口缩放与 BDP

def calculate_bdp(bandwidth_mbps, rtt_ms):
    """计算带宽延迟积 (BDP)"""
    # BDP = 带宽 × RTT
    bandwidth_bytes = bandwidth_mbps * 1_000_000 / 8
    rtt_sec = rtt_ms / 1000
    bdp = bandwidth_bytes * rtt_sec
    return bdp

def required_window_scale(bandwidth_mbps, rtt_ms):
    """计算需要的窗口缩放因子"""
    bdp = calculate_bdp(bandwidth_mbps, rtt_ms)
    
    # 需要的缩放因子
    import math
    if bdp <= 65535:
        return 0
    scale = math.ceil(math.log2(bdp / 65535))
    return min(scale, 14)

# 不同场景的窗口需求
scenarios = [
    ("局域网", 1000, 1),      # 1Gbps, 1ms
    ("宽带", 100, 20),        # 100Mbps, 20ms
    ("跨省", 100, 50),        # 100Mbps, 50ms
    ("跨洋", 1000, 200),      # 1Gbps, 200ms
]

print("场景 | 带宽 | RTT | BDP | 需要缩放")
print("-" * 50)
for name, bw, rtt in scenarios:
    bdp = calculate_bdp(bw, rtt)
    scale = required_window_scale(bw, rtt)
    print(f"{name:6} | {bw:4}Mbps | {rtt:3}ms | {bdp:12,.0f}B | {scale}")

8.4 时间戳 (Timestamp)

时间戳选项格式:
┌────────────┬────────────┬────────────────┬────────────────┐
│ Kind=8     │ Length=10   │ TS Value (TSV) │ TS Reply (TSR) │
│ 1 byte     │ 1 byte     │ 4 bytes        │ 4 bytes        │
└────────────┴────────────┴────────────────┴────────────────┘

TS Value: 发送方的当前时间戳
TS Reply: 回显对方的时间戳

时间戳的两个用途

用途 1:RTT 测量
发送方发送:TSV=1000
接收方回复:TSR=1000(回显), TSV=5000
发送方计算:RTT = 当前时间 - TSR = 当前时间 - 1000

用途 2:PAWS (Protection Against Wrapped Sequences)
防止序列号回绕问题(在高速网络中可能发生)
class TimestampRTT:
    """使用时间戳测量 RTT"""
    
    def __init__(self):
        self.sent_time = {}  # {tsv: send_time}
    
    def send_segment(self, tsv):
        """发送段时记录时间戳"""
        import time
        self.sent_time[tsv] = time.time()
    
    def receive_ack(self, tsr):
        """收到 ACK 时计算 RTT"""
        import time
        if tsr in self.sent_time:
            rtt = time.time() - self.sent_time[tsr]
            del self.sent_time[tsr]
            return rtt
        return None

# 使用示例
rtt_calc = TimestampRTT()
rtt_calc.send_segment(1000)
# ... 网络传输 ...
rtt = rtt_calc.receive_ack(1000)
if rtt:
    print(f"RTT: {rtt*1000:.2f} ms")

8.5 SACK 选项详解

SACK Permitted 选项

在握手中协商 SACK 支持:
┌────────────┬────────────┐
│ Kind=4     │ Length=2    │
│ 1 byte     │ 1 byte     │
└────────────┴────────────┘

只在 SYN 包中出现,表示支持 SACK

SACK 块格式

SACK 块(在数据传输中使用):
┌────────────┬────────────┬────────────────┬────────────────┐
│ Kind=5     │ Length      │ Left Edge      │ Right Edge     │
│ 1 byte     │ 1 byte     │ 4 bytes        │ 4 bytes        │
├────────────┴────────────┼────────────────┼────────────────┤
│                         │ Left Edge 2    │ Right Edge 2   │
│                         │ 4 bytes        │ 4 bytes        │
└─────────────────────────┴────────────────┴────────────────┘

每个 SACK 块 8 字节,最多 4 个块(加上 SACK Permitted 共 40 字节)
import struct

def encode_sack_block(left, right):
    """编码 SACK 块"""
    return struct.pack('!II', left, right)

def decode_sack_blocks(data):
    """解码 SACK 块列表"""
    blocks = []
    for i in range(0, len(data) - 7, 8):
        left, right = struct.unpack('!II', data[i:i+8])
        blocks.append((left, right))
    return blocks

# 示例
blocks = [
    (1000, 2000),  # 已收到 1000-1999
    (4000, 5000),  # 已收到 4000-4999
]

# 编码
encoded = b''.join(encode_sack_block(l, r) for l, r in blocks)
print(f"编码后: {encoded.hex()}")

# 解码
decoded = decode_sack_blocks(encoded)
print(f"解码后: {decoded}")  # [(1000, 2000), (4000, 5000)]

8.6 NOP 与 EOL

NOP (No Operation):
• Kind = 1
• 用于 4 字节对齐
• 不携带数据

EOL (End of Option List):
• Kind = 0
• 表示选项结束
• 后面的字节不再解析

填充示例:
┌──────┬──────┬──────┬──────┐
│ MSS  │ NOP  │ WS   │ NOP  │
│ 4B   │ 1B   │ 3B   │ 1B   │ = 9 字节
└──────┴──────┴──────┴──────┘

需要对齐到 4 字节:
┌──────┬──────┬──────┬──────┬──────┐
│ MSS  │ NOP  │ WS   │ NOP  │ NOP  │
│ 4B   │ 1B   │ 3B   │ 1B   │ 1B   │ = 10 字节
└──────┴──────┴──────┴──────┴──────┘

还是不对齐,需要再加:
┌──────┬──────┬──────┬──────┬──────┬──────┐
│ MSS  │ NOP  │ WS   │ NOP  │ NOP  │ NOP  │
│ 4B   │ 1B   │ 3B   │ 1B   │ 1B   │ 1B   │ = 12 字节 ✓
└──────┴──────┴──────┴──────┴──────┴──────┘

8.7 选项解析完整实现

def parse_tcp_options(data):
    """解析所有 TCP 选项"""
    options = []
    i = 0
    
    while i < len(data):
        kind = data[i]
        
        if kind == 0:  # EOL
            break
        
        elif kind == 1:  # NOP
            options.append({'type': 'NOP'})
            i += 1
        
        elif i + 1 < len(data):
            length = data[i + 1]
            
            if kind == 2:  # MSS
                if length == 4 and i + 4 <= len(data):
                    mss = struct.unpack('!H', data[i+2:i+4])[0]
                    options.append({'type': 'MSS', 'value': mss})
            
            elif kind == 3:  # Window Scale
                if length == 3 and i + 3 <= len(data):
                    scale = data[i+2]
                    options.append({'type': 'WindowScale', 'value': scale})
            
            elif kind == 4:  # SACK Permitted
                options.append({'type': 'SACK_Permitted'})
            
            elif kind == 5:  # SACK
                blocks = []
                for j in range(i+2, i+length-7, 8):
                    if j+8 <= i+length:
                        left, right = struct.unpack('!II', data[j:j+8])
                        blocks.append((left, right))
                options.append({'type': 'SACK', 'blocks': blocks})
            
            elif kind == 8:  # Timestamp
                if length == 10 and i + 10 <= len(data):
                    tsv, tsr = struct.unpack('!II', data[i+2:i+10])
                    options.append({'type': 'Timestamp', 'tsv': tsv, 'tsr': tsr})
            
            else:
                options.append({'type': f'Unknown({kind})', 'length': length})
            
            i += length
        else:
            break
    
    return options

# 示例
# MSS(4) + NOP(1) + WScale(3) + NOP(1) + SACK_OK(2) + NOP(1) + TS(10) = 22 bytes
options_data = bytes([
    2, 4, 0x05, 0xB4,          # MSS = 1460
    1,                          # NOP
    3, 3, 7,                    # Window Scale = 7
    1,                          # NOP
    4, 2,                       # SACK Permitted
    1,                          # NOP
    8, 10, 0, 0, 0, 100, 0, 0, 0, 0  # Timestamp
])

parsed = parse_tcp_options(options_data)
for opt in parsed:
    print(opt)

8.8 TCP Fast Open (TFO)

TCP Fast Open 目的:
减少一个 RTT 的连接建立延迟

正常三次握手:
Client ──SYN──→ Server      RTT 1
Client ←──SYN+ACK── Server
Client ──ACK──→ Server      RTT 2
Client ──DATA──→ Server     RTT 3

TFO:
首次连接(收集 Cookie):
Client ──SYN + TFO Cookie Request──→ Server
Client ←──SYN+ACK + Cookie── Server

后续连接:
Client ──SYN + Cookie + DATA──→ Server
Client ←──SYN+ACK + Response── Server

省掉一个 RTT!
# 启用 TFO
$ sysctl -w net.ipv4.tcp_fastopen=3
# 0: 禁用
# 1: 客户端启用
# 2: 服务器启用
# 3: 双方启用

# 查看 TFO 状态
$ cat /proc/net/netstat | grep TcpExtTCPFastOpen

8.9 注意事项

⚠️ 选项长度限制:所有选项总计不超过 40 字节,需要合理安排

⚠️ MSS 不可变:MSS 在握手时协商,连接期间不能更改

⚠️ 时间戳开销:每个段增加 10 字节开销,小包场景需权衡

⚠️ 窗口缩放协商:只在 SYN 包中协商,中间代理可能不转发

8.10 扩展阅读


下一章09 - UDP 协议详解 - UDP 头部、无连接特性、多播