---
title: TCP
url: https://doc.liz6.com/networking/03-l4-transport-layer/01-tcp
locale: zh
area: networking
tags:
- networking
- L4传输层
date: 2026-06-30
modified: 2026-07-16
description: 互联网最复杂的协议——三次握手建连接、滑动窗口做流控、CUBIC/BBR 管拥塞、SACK 减少重传。TCP 在不可靠的 IP 之上造了一个可靠的字节流抽象,每个机制都是几十年工程实践的沉淀。
---

# TCP

> 互联网最复杂的协议——三次握手建连接、滑动窗口做流控、CUBIC/BBR 管拥塞、SACK 减少重传。TCP 在不可靠的 IP 之上造了一个可靠的字节流抽象,每个机制都是几十年工程实践的沉淀。

## 概述

TCP 是互联网最基础的可靠传输协议，1981 年由 RFC 793 定义，承载了绝大多数互联网流量（HTTP、SSH、SMTP 等）。它在一个不可靠的 IP 层之上提供：有序字节流、丢包重传、流控和拥塞控制。

理解 TCP 有一条主线：**它要同时保护两个东西**。滑动窗口保护的是*对端*（别把接收方的 buffer 灌爆），拥塞控制保护的是*路上的网络*（别把中间路由器的队列灌爆）。前者有明确的信号（对端直接告诉你窗口多大），后者没有——网络不会主动汇报"我快满了"，发送方只能靠猜。TCP 的核心创新不是"保证可靠"，而正是这个猜的艺术：拥塞控制。过去 40 年拥塞算法从 Tahoe/Reno 演进到 CUBIC（Linux 默认）再到 BBR（Google 2016），反映了互联网从铜线到移动网络到数据中心的变迁。

## TCP Header

```
 0                   1                   2                   3
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|          Source Port          |       Destination Port        |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                        Sequence Number                        |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                    Acknowledgment Number                      |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Data  |       |U|A|P|R|S|F|                               |
|Offset | Rsrvd |R|C|S|S|Y|I|            Window             |
| (4)   | (3)  |G|K|H|T|N|N|           (16 bits)            |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|           Checksum            |         Urgent Pointer        |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                    Options (if Data Offset > 5)               |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
```

几个关键字段的语义：

- **Sequence Number**：此 segment 第一个 data byte 的序号。初始序号 ISN 随机化（防序号预测攻击）。
- **Acknowledgment Number**：期望收到的*下一个* byte 的序号——这是**累计 ACK**：ack=1000 表示 999 及之前全部收到。
- **Data Offset**：TCP header 长度，以 4 字节为单位，最小 5（20 字节无选项头）。
- **Flags**：CWR、ECE（显式拥塞通知 ECN 用）、URG、ACK、PSH、RST、SYN、FIN。
- **Window**：接收方当前剩余 buffer 大小。原始字段只有 16 位（最大 64KB），现代 TCP 靠 window scale 选项扩展（见下）。

### 关键 Options

| Option | kind | 出现时机 | 作用 |
|---|---|---|---|
| MSS | 2 | 仅 SYN | 声明本端能接收的最大 segment，通常 = MTU(1500) − IP(20) − TCP(20) = **1460** |
| Window Scale | 3 | 仅 SYN | 窗口值左移 N 位（最大 14），突破 64KB 上限 |
| SACK Permitted | 4 | 仅 SYN | 声明支持选择性确认 |
| SACK | 5 | 数据传输中 | 报告收到的非连续区间，让重传只补洞 |
| Timestamp | 8 | 每个包 | RTT 测量（RTTM）+ 序号回绕保护（PAWS） |

两个值得展开的：

**Window Scale 为什么必要**——没有它，窗口最大 64KB。而发送方能"填在路上"的数据量上限是 `min(窗口, cwnd)`，链路的容量是 BDP（带宽 × RTT）。一条 1Gbps、RTT 50ms 的链路 BDP ≈ 6.25MB，64KB 的窗口只能跑出 `64KB / 50ms ≈ 10Mbps`——带宽再大也跑不满。有了 scale（如 window=65535, scale=7 → 实际 8MB），窗口才追得上现代链路。

**SACK 解决什么**——累计 ACK 只能表达"连续收到了多少"。丢了 byte 2001-2999 时，即使 3000-4000 已经收到，ACK 也只能停在 2001，发送方可能把后面已收到的数据也重发一遍。SACK 让接收方明说"我收到了 1000-2000 和 3000-4000"，发送方只补 2001-2999 这一个洞。

## 连接建立：三次握手

```mermaid
sequenceDiagram
    participant C as Client
    participant S as Server
    Note over C: CLOSED → SYN_SENT
    Note over S: LISTEN
    C->>S: SYN, seq=x
    Note over S: SYN_RCVD
    S->>C: SYN+ACK, seq=y, ack=x+1
    C->>S: ACK, seq=x+1, ack=y+1
    Note over C,S: 双方 ESTABLISHED
```

**为什么是三次，两次不行？** 握手的目标是*双方*都确认"双向通道可用"。两次的话：Client 发 SYN、Server 回 SYN+ACK——此时 Client 确认了双向可达，但 Server 不知道自己的 SYN+ACK 有没有到达。第三次的 ACK 就是给 Server 的这个确认。少一次，就有一方对通道状态存疑。

**为什么 SYN 要消耗一个序号？** SYN 和 FIN 都必须"可被确认"——如果它们不占序号，丢了就没法用 ACK 机制发现并重发。SYN(seq=x) 收到 ACK(x+1)，才证明"你的 SYN 我收到了"。这也是握手图里 ack 都是 x+1、y+1 的原因。

顺带一提：`SYN_RCVD` 状态要占服务端资源（半连接队列），这是 SYN flood 攻击的靶点——Linux 用 syncookies 把状态编码进 ISN 返还给客户端，做到无状态应答。

## 连接关闭：四次挥手

```mermaid
sequenceDiagram
    participant A as 主动关闭方
    participant P as 被动关闭方
    A->>P: FIN, seq=u
    Note over A: FIN_WAIT_1
    P->>A: ACK, ack=u+1
    Note over P: CLOSE_WAIT — 应用层还没 close
    Note over A: FIN_WAIT_2
    P->>A: FIN, seq=v
    Note over P: LAST_ACK
    A->>P: ACK, ack=v+1
    Note over A: TIME_WAIT — 等 2MSL 后 CLOSED
    Note over P: CLOSED
```

挥手是四次而不是三次，因为关闭是**两个独立的半关闭**：收到 FIN 只说明"对方不再发了"，本方可能还有数据要发（所以 ACK 和自己的 FIN 之间隔着一个 CLOSE_WAIT，长短取决于应用什么时候调 close）。

**TIME_WAIT 为什么要等 2MSL**（约 120s）：

1. **保证最后一个 ACK 送达**——如果它丢了，对端会重发 FIN；主动关闭方必须还"活着"才能重新应答，否则对端永远卡在 LAST_ACK。
2. **让旧连接的迷路包在网络中过期**——防止上一条连接的延迟报文，被误认为属于紧接着复用同一 5 元组的新连接。

TIME_WAIT 大量堆积（常见于高并发短连接的客户端侧）的缓解手段：

| 手段 | 作用范围 | 说明 |
|---|---|---|
| `tcp_tw_reuse=1` | 客户端 | 新连接可复用 TIME_WAIT socket，依赖 timestamp 单调递增判定安全 |
| `SO_REUSEADDR` | 服务端 | 重启后立即 bind 端口，不等 TIME_WAIT 排空 |
| `tcp_fin_timeout` | 孤儿连接 | 只影响 orphan socket 的 FIN_WAIT_2 超时，常被误当 MSL 调 |
| 连接池 | 架构层 | 根本方案——复用长连接，从源头少产生 TIME_WAIT |

## 滑动窗口与流控

流控回答的问题是：**发送方怎么知道对端还能接多少？** 答案简单直接——接收方在每个 ACK 里通告自己剩余的 buffer（Window 字段），发送方保证"已发未确认"的数据量不超过它。

发送方视角的序号空间：

```
        已确认          已发送未确认        可以发送        窗口外
  ──────────────┬──────────────────┬────────────────┬─────────────
                SND.UNA            SND.NXT          SND.UNA+SND.WND
```

发送条件就是 `SND.NXT < SND.UNA + SND.WND`。

两个经典的坑，都出在"窗口变小"上：

- **零窗口死锁**：接收方 buffer 满，通告 window=0，发送方停发。之后接收方腾出空间发 window update——如果这个 update 丢了，双方互相等，死锁。所以 TCP 有 **persist timer**：发送方定期发 1 字节的 window probe，强迫接收方回 ACK 带上最新窗口。
- **Silly Window Syndrome**：接收方每空出 1 字节就通告 window=1，发送方就发 1 字节——无尽的小包，头部开销远超载荷。两端各有缓解：发送方用 Nagle 算法（攒够 MSS 或收到 ACK 才发），接收方用 Clark 方案（窗口没腾出足够空间前不通告小窗口）。

## 拥塞控制：CUBIC → BBR

流控有对端明确报数，拥塞控制没有——**网络不会告诉你它快满了**，发送方只能从旁证推断：丢包了？延迟涨了？这个"用什么信号推断拥塞"的选择，就是各代算法的分野：CUBIC 把*丢包*当信号，BBR 把*测得的带宽和 RTT* 当信号。

发送速率由拥塞窗口 cwnd 控制，实际能发的量是 `min(cwnd, 对端窗口)`。

### CUBIC（Linux 默认）

CUBIC 的增长曲线是一个三次函数，以上次丢包时的窗口 W_max 为"记忆"：

```
cwnd = C × (t − K)³ + W_max        K = ³√(W_max × β / C),  β = 0.7
```

直觉上：丢包后 cwnd 砍到 0.7 倍，然后**先快速逼近 W_max，接近时放缓（曲线平台期），确认没事后再加速探测更高水位**——三次曲线天然给出这个"快-慢-快"的形状，且增长只和距上次丢包的时间 t 有关、和 RTT 无关（长肥管道上比 Reno 的每 RTT 线性加一快得多）。

```mermaid
flowchart LR
    SS["🚀 <b>Slow Start</b><br><small>cwnd 每 RTT 翻倍</small>"] -->|"到达 ssthresh"| CA["📈 <b>Congestion Avoidance</b><br><small>沿 CUBIC 曲线增长</small>"]
    SS -.->|"丢包"| MD["💥 <b>Decrease</b><br><small>cwnd ×0.7,记录 W_max</small>"]
    CA -.->|"丢包"| MD
    MD ==> CA
    classDef grow fill:#3fb9501f,stroke:#3fb950,stroke-width:2px
    classDef steady fill:#4493f81f,stroke:#4493f8,stroke-width:2px
    classDef loss fill:#f851491f,stroke:#f85149,stroke-width:2px
    class SS grow
    class CA steady
    class MD loss
```

CUBIC 的问题在它的信号本身：**丢包是一个太迟、且不总是对的信号**。

- 在 bufferbloat 链路上（路由器 buffer 巨大），队列填满才丢包——此时延迟已经涨到几百毫秒，CUBIC 还在踩油门。
- 在高带宽 × 长 RTT（大 BDP）链路上，一次丢包砍 30% 之后爬回来很慢。
- 在有随机丢包的链路（WiFi、跨洋）上，把噪声误判成拥塞，白白降速。

### BBR（Bottleneck Bandwidth and RTT，Google 2016）

BBR 换了信号：不等丢包，而是**直接给链路建模**。把路径看成一根管道，测出两个参数——瓶颈带宽 BW（近期交付速率的最大值）和传播时延 min RTT（近期 RTT 的最小值），管道的容量就是 `BDP = max(BW) × min(RTT)`。把 in-flight 数据量钉在 BDP 附近：既跑满带宽，又不在路由器里排队。

```mermaid
flowchart LR
    S["🚀 <b>Startup</b><br><small>每轮翻倍<br>快速填满管道</small>"] -->|"交付速率<br>不再增长"| D["🫗 <b>Drain</b><br><small>排空超出 BDP<br>的排队数据</small>"]
    D -->|"inflight 降回 BDP"| P["⚖️ <b>ProbeBW</b><br><small>稳态:增益循环<br>轻探新带宽</small>"]
    P -->|"每 10s"| R["📏 <b>ProbeRTT</b><br><small>压到 4 包<br>重测 min RTT</small>"]
    R ==>|"拿到干净的 min RTT"| P
    classDef grow fill:#3fb9501f,stroke:#3fb950,stroke-width:2px
    classDef warn fill:#d299221f,stroke:#d29922,stroke-width:2px
    classDef steady fill:#4493f826,stroke:#4493f8,stroke-width:2.5px
    classDef neutral fill:#64748b1f,stroke:#64748b,stroke-width:2px
    class S grow
    class D warn
    class P steady
    class R neutral
```

- **Startup**：类似慢启动，每轮翻倍，快速填管道。
- **Drain**：填满后 inflight 会超出 BDP（多出的在排队），先排空。
- **ProbeBW**：稳态。用增益循环 `[5/4, 3/4, 1, 1, 1, 1, 1, 1]` 周期性轻踩油门探测新带宽、再让出来。
- **ProbeRTT**：每 10s 把 inflight 压到极低，腾空队列测真实传播时延——不然自己制造的队列会污染 min RTT 的测量。

实际效果（Google YouTube 部署数据）：平均延迟降约 33%，重传降约 20%。局限也真实存在：BBR v1 对 CUBIC 流不公平（probe 太激进，会挤压共存的丢包型流量），v2（配合 RFC 9438 时代的改进）加入了对丢包和 ECN 的响应、改善了公平性。

### 一张表对比

| | CUBIC | BBR |
|---|---|---|
| 拥塞信号 | 丢包 | 测量的 BW + RTT 模型 |
| bufferbloat 链路 | 填满队列，延迟飙升 | inflight ≈ BDP，低延迟 |
| 随机丢包链路 | 误判拥塞，吞吐塌 | 不因丢包自动降速，吞吐稳 |
| 公平性 | 同类间收敛良好 | v1 对丢包型流不公平，v2 改善 |
| 部署 | Linux 默认 | `sysctl net.ipv4.tcp_congestion_control=bbr` |

拥塞控制的这套思路并不专属 TCP——QUIC 在用户态重新实现了全套（见 [UDP 与 QUIC 传输层](/networking/03-l4-transport-layer/02-udp-and-quic-transport-layer.md)），换算法不用改内核。调优实操见[网络性能与调优](/networking/13-network-diagnostics/03-network-performance-and-tuning.md)。

## 参考

- **RFC**: 793, 7323 (timestamp/window scale), 2018 (SACK), 7413 (TFO), 5681, 8312 (CUBIC), 9438 (BBRv2)
- **源码**: `net/ipv4/tcp_input.c` (tcp_rcv_established), `net/ipv4/tcp_cubic.c`, `net/ipv4/tcp_bbr.c`
- **工具**: `ss -tianp`, `nstat -a | grep Tcp`, `bpftrace -e 'k:tcp_retransmit_skb { @[comm] = count(); }'`

*Keywords: TCP, three-way handshake, TIME_WAIT, sliding window, CUBIC, BBR, SACK, TFO, Window Scale, congestion control, BDP, bufferbloat*
