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 字节 │
└──────────────────────────────────────────────────────────┘
| 网络类型 | MTU | MSS |
|---|---|---|
| 以太网 | 1500 | 1460 |
| PPPoE | 1492 | 1452 |
| IPv6 最小 | 1280 | 1220 |
| Wi-Fi | 2304 | 2264 |
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 头部、无连接特性、多播