Files
cs-note/hhs/NETWORK/04-传输层/03-TCP可靠传输机制.md
T
2026-05-24 11:42:38 +08:00

5.3 KiB
Raw Blame History

tags, create time
tags create time
计算机网络
TCP可靠传输
序列号
确认号
重传
2026-05-18 01:50

可靠传输机制

概述

TCP 承诺"可靠交付"——不管中间网络多么不可靠,发送方最终确保每个字节都被接收方完整、有序地收到。这是通过序列号/确认号 + 重传 + 校验组合实现的。

序列号(Sequence Number)

基本原理

每个 TCP 段有一个 32 bit 的序列号,代表该段的第一个数据字节在整个流中的位置。

发送方发出的数据流:
字节:  0     1     2     3     ...   99    100
      ├─────┼─────┼─────┼───┤       └───┤
      │ Seg1│ Seg2│ Seg3│...│         │SegN│
      seq=0 seq=50 seq=100             seq=99

Seq 值范围: 0 ~ 2^32 - 1 = 4,294,967,295 (约 4GB)
回绕: 到达最大值后回到初始 ISN (Initial Sequence Number)

ISN(Initial Sequence Number)生成策略

// Go 的 net包: ISN 使用随机化防止预测攻击
func getISN() uint32 {
    // crypto/rand 或 /dev/urandom
    return randomUint32()
}

Linux 默认 ISN = jiffies + hash(src/dst IP/port),现代内核已改为完全随机化。

确认号(Acknowledgment Number)

含义 说明
ack = N "我已经收到了 N-1 及之前的所有字节"
ack = expected next byte 期望对方下一个收到的字节序号
ACK = 0 的情况 仅在 SYN/FIN 阶段出现(它们不占序列号空间)

SACK(Selective Acknowledgment)

标准 TCP 只能确认"前面的所有字节都到了",SACK 允许接收方告诉发送方"哪些乱序的块我也收到了":

正常 ACK:           SACK:
────────            ────────
收齐了 [0,49]       收到: [0,49] [100,149] [200,249]
ack=50              缺:   [50,99]  [150,199]
                      ↑
                  只重传缺的!
$ sysctl net.ipv4.tcp_sack
net.ipv4.tcp_sack = 1  # Linux 默认开启

超时重传(RTO - Retransmission Timeout)

RTO 计算(Jacobson/Karels 算法)

RTT_sample = T_arrival - T_sent          // 本次往返时间
rtt_variance = (1 - β) * rtt_variance + β * |RTT_sample - RTT_avg|
                                           β = 1/4
RTO = RTT_avg + 4 * rtt_variance
                           α = 3/4

初始 RTO: 至少 1 秒 (RFC 6298)
最大 RTO: 60 秒

快速重传(Fast Retransmit)

如果收到3 个重复 ACK(duplicate ACK),说明某个包很可能丢了——不等 RTO 过期立即重传:

flowchart LR
    A["Segment 1 → recv ✅"] --> B["Segment 2 → recv ✅"]
    B --> C["Segment 3 → LOST ❌"]
    C --> D["DupACK for seg 2 ×3"]
    D -->|"触发快速重传"| E["Re-send Segment 3 immediately ✅"]
    
    style C fill:#FF6B6B,color:#fff
    style E fill:#98FB98,color:#000
传统 RTO 重传 vs 快速重传:
─────────────────────────
传统: 丢包 → 等 RTO(≥1s) → 重传 → 再等一个 RTT → 共 ≥ 2s
快速: 丢包 → 收到3个dup ACK(~1个RTT) → 立即重传 → 共 ~1 RTT

Go 中的 TCP KeepAlive

import "golang.org/x/sys/unix"

// 设置 TCP keepalive (Go stdlib 1.11+)
ln, _ := net.Listen("tcp", ":8080")
conn, _ := ln.Accept()

if tc, ok := conn.(*net.TCPConn); ok {
    // 每 30s 发送一次 keepalive probe
    tc.SetKeepAlive(true)
    tc.SetKeepAlivePeriod(30 * time.Second)
    
    // 最多重试 3 次 (内核参数 net.ipv4.tcp_keepalive_probes)
    // 总超时 = 30s * 3 = 90s
}
# Linux 内核默认值
$ sysctl net.ipv4.tcp_keepalive_time      # 首次探测前的空闲时间
net.ipv4.tcp_keepalive_time = 7200  # 2小时!
$ sysctl net.ipv4.tcp_keepalive_intvl     # 两次探测间隔
net.ipv4.tcp_keepalive_intvl = 75       # 75秒
$ sysctl net.ipv4.tcp_keepalive_probes    # 最多探测次数
net.ipv4.tcp_keepalive_probes = 9        # 9次

[!tip] 为什么 Go http.Server 默认没有 keepalive? http.Transport 的 IdleConnTimeout 默认是 90s,但底层的底层 keepalive 由操作系统控制。高并发服务通常需要手动调小 SetKeepAlivePeriod() 和内核参数。

紧急数据(URG)

[普通数据] [URG flag set] [+1 urgent data byte] [普通数据]
                              ^^^^
                        "请立刻处理这个字节!"

紧急指针指向 URG 标志后的那个字节。但由于许多应用忽略 URG 功能(Nagle 算法可能与 URG 冲突),现代实践中极少使用。

粘包与拆包的根因

⚠️ 注意:这个问题虽然在本节讨论,但它其实是应用层协议的设计问题,详见 hhs/NETWORK/TCP粘包与拆包

为什么会粘包?

发送端 write("HELLO") → TCP 缓冲 → 合并成一个段 "HELLOWORLD" → 接收端 read() 拿到一整串
发送端 write("WORLD") → 因为 Nagle 算法延迟发送,合并了上一段

为什么会拆包?

发送端 write("HELLO WORLD THIS IS LONG") → 超过 MSS → TCP 自动分片
接收端 read(5) → 只读到 "HELLO" → 剩下在缓冲区等待下次 read()

关联笔记