21. 传输层与 TCP 深入

深入传输层核心:TCP 报文段格式与标志位、三次握手四次挥手、状态机、序列号/ACK/重传/滑动窗口可靠传输、拥塞控制(慢启动/拥塞避免/快速重传恢复)、UDP 与 QUIC、TIME_WAIT 与连接排障调优。

1. 传输层职责与端口

1.1 传输层解决的问题

传输层在网络层提供的主机到主机通信之上,实现**端到端(进程到进程)**的可靠或尽力交付,核心抽象是端口号 + 传输协议。TCP 提供可靠、有序、面向字节流的服务;UDP 提供不可靠、无序、面向数据报的服务。

应用层    HTTP(80)   DNS(53)   SSH(22)         ← 进程(端口)
传输层    TCP(可靠/有序)     UDP(尽力/无连接)
网络层    IP(主机到主机, 尽力而为)
链路层    以太网/无线(帧)
传输服务TCPUDP
连接面向连接(三次握手)无连接
可靠性可靠(重传/确认)尽力交付
有序性字节流有序数据报无序
流量/拥塞控制有无
首部开销20 字节8 字节
典型应用HTTP/HTTPS、FTP、SSHDNS、音视频、游戏、QUIC 底层

1.2 端口与 Socket

端口号范围 0-65535:0-1023 为知名端口(Well-Known,HTTP 80、HTTPS 443、DNS 53、SSH 22、MySQL 3306),1024-49151 为注册端口,49152-65535 为动态/临时端口。一个连接由四元组 (src_ip, src_port, dst_ip, dst_port) 唯一标识。

# Python 建立 TCP 连接的 Socket 示例
import socket

def fetch_http(host, port=80):
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.settimeout(5)
    sock.connect((host, port))              # 触发三次握手
    sock.sendall(b"GET / HTTP/1.1\r\nHost: %s\r\nConnection: close\r\n\r\n" % host.encode())
    data = b""
    while True:
        chunk = sock.recv(4096)
        if not chunk:
            break
        data += chunk
    sock.close()                            # 触发四次挥手
    return data

2. TCP 报文段格式

2.1 首部字段

TCP 首部最小 20 字节,最大 60 字节(含选项)。各字段均为真实 RFC 793/7323 定义。

 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|         源端口 (16)           |        目的端口 (16)            |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                    序号 Sequence Number (32)                   |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                   确认号 Acknowledgment Number (32)            |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 数据偏移(4)|保留(3)|N|C|E|U|A|P|R|S|F|     窗口大小 (16)       |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|          校验和 (16)           |       紧急指针 (16)           |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                          选项 (可选)                           |
/* Linux 内核 uapi 中的 TCP 首部结构(字段与 RFC 一致) */
struct tcphdr {
    __be16  source;      /* 源端口 */
    __be16  dest;        /* 目的端口 */
    __be32  seq;         /* 序列号 */
    __be32  ack_seq;     /* 确认号 */
    __u16   res1:4,      /* 保留 */
            doff:4,      /* 数据偏移(首部长度 / 4) */
            fin:1, syn:1, rst:1, psh:1, ack:1, urg:1, ece:1, cwr:1;
    __be16  window;      /* 窗口大小(配合窗口缩放选项可达 1GB) */
    __be16  check;       /* 校验和 */
    __be16  urg_ptr;     /* 紧急指针 */
};

2.2 标志位速查

标志位名称含义
SYNSynchronize建立连接,同步初始序列号
ACKAcknowledgment确认号有效
FINFinish主动关闭连接
RSTReset异常复位连接
PSHPush立即交付应用层,不等缓冲填满
URGUrgent紧急指针有效(极少用)
ECE/CWRECN 相关显式拥塞通知(RFC 3168)
选项用途
MSS通告最大报文段长度(默认 536/1460 字节)
Window Scale窗口缩放因子,支持 >64KB 窗口
SACK选择性确认,允许只重传丢失段
TimestampRTT 测量与 PAWS 防回绕

3. 三次握手与四次挥手

3.1 三次握手(建立连接)

为什么是三次?第一次确认客户端的发送能力,第二次确认服务端收到并能回复,第三次确认客户端知道服务端准备好了——同时保证双方都确认了彼此的收发能力,且能同步初始序列号,防止历史连接报文干扰。

客户端                                   服务端
  │         1. SYN(seq=x)               │
  │────────────────────────────────────►│  LISTEN → SYN-RECEIVED
  │   2. SYN(seq=y) + ACK(ack=x+1)      │
  │◄────────────────────────────────────│
  │         3. ACK(ack=y+1)             │
  │────────────────────────────────────►│  ESTABLISHED
  ESTABLISHED

SYN Flood:攻击者只发 SYN 不完成握手,耗尽服务端半连接队列(backlog)。防御:SYN Cookies——把连接信息编码进 ISN,不用占用队列。

3.2 四次挥手(关闭连接)

为什么是四次?TCP 是全双工的,双方各自独立关闭自己的发送方向,因此需要两对 FIN/ACK。收到对方 FIN 后进入 CLOSE_WAIT,此时本端仍可继续发送未完成的数据。

客户端                                 服务端
  │        1. FIN(seq=u)              │
  │──────────────────────────────────►│  客户端 FIN-WAIT-1
  │      2. ACK(ack=u+1)              │     服务端 CLOSE-WAIT
  │◄──────────────────────────────────│  客户端 FIN-WAIT-2
  │        3. FIN(seq=v)              │
  │◄──────────────────────────────────│  服务端 LAST-ACK
  │      4. ACK(ack=v+1)              │
  │──────────────────────────────────►│
  TIME-WAIT (2×MSL)                     CLOSED

4. TCP 状态机

4.1 完整状态迁移

TCP 连接的整个生命周期由 RFC 793 状态机管理。理解状态迁移是排查网络故障的基础(netstat/ss 输出正是这些状态)。

                    +---------+  +--------+
      主动打开  SYN    V         V
CLOSED ─────────► SYN-SENT ──► SYN-RECEIVED ──► ESTABLISHED
  ▲                   │    SYN+ACK         │
  │                   │                    │  FIN 发送
  +────────── CLOSING ◄── FIN-WAIT-1 ◄─────┼──────────┐
  │  RST/超时       │        │ ACK        │ FIN      │
  │                V        V             V          V
  │            CLOSE-WAIT  FIN-WAIT-2    CLOSING    (数据继续)
  │                │ ACK     │ 收到 FIN              │
  │                V        V                       │
  │             LAST-ACK  TIME-WAIT ────────────────┘
  │                 │ ACK      │ 2×MSL 超时
  │                 V          V
  └────────────── CLOSED ◄─────┘
状态含义排查要点
LISTEN服务端监听ss -lntp 查看端口
SYN-SENT客户端已发 SYN若长期停留,多为对端无响应/被丢包
ESTABLISHED连接建立正常数据传输态
FIN-WAIT-2己方已 FIN,等对方 FIN大量堆积通常应用未关闭连接
CLOSE-WAIT己方已收到对端 FIN,待本端关闭大量 CLOSE-WAIT = 应用层泄漏
TIME-WAIT等待 2×MSL 后彻底关闭高并发短连接常见,见第 9 节
# 排查命令
ss -tan 'sport = :80'              # 查看 80 端口 TCP 状态分布
ss -tan state time-wait            # 只看 TIME-WAIT 连接
netstat -an | awk '/ESTABLISHED/{print $6}' | sort | uniq -c

5. 可靠传输:序列号/ACK/重传/滑动窗口

5.1 序列号与累计确认

TCP 通过序列号为字节流编号:每段报文携带首字节序号 seq,接收方回复累计确认 ack = 期望收到的下一个字节序号(即已正确收到 ack-1 之前的所有字节)。发送方据此重传丢失数据。

机制作用说明
序列号保证有序与去重初始序列号 ISN 随机化防伪造
ACK累计确认ack 表示"ack 之前全收到了"
超时重传 RTO丢失恢复RTO 由 RTT 估算自适应(Karn 算法)
快速重传丢包早期恢复收到 3 个重复 ACK 即重传,不等超时
SACK选择性确认精确告知哪些区间已到,避免全部重传

5.2 滑动窗口

发送方维护发送窗口(由接收方通告的 rwnd 与拥塞窗口 cwnd 决定),窗口内的数据可连续发送无需等待 ACK:

发送窗口(rwnd 决定)
│◄───────── 已发送并已确认 ─────────│── 已发送未确认 ──│── 可发送但未发送 ──│── 不可发送 ──│
                                     ▲                 ▲
                               SND.UNA(最老未确认)   SND.NXT(下一个要发)
# 滑动窗口发送逻辑(示意:GBN 与选择性重传的窗口管理)
def sliding_window_send(segments, window_size):
    sent = {}
    next_to_send = 0
    base = 0
    while base < len(segments):
        # 填充窗口:最多发送到 base + window_size
        while next_to_send < min(base + window_size, len(segments)):
            send(segments[next_to_send])        # 发送段
            sent[next_to_send] = None           # 记录未确认
            next_to_send += 1
        ack = wait_ack()                        # 收到累计确认
        base = ack                              # 窗口滑动

接收窗口 rwnd 通过 TCP 首部 window 字段通告(配合 Window Scale 选项),上限可达约 1GB。窗口太小则吞吐受限:吞吐上限 ≈ rwnd / RTT,即带宽时延积 BDP。


6. 流量控制

6.1 接收方主导的流控

流量控制防止发送方过快淹没接收方:接收方在 ACK 中通告剩余接收缓冲区 rwnd,发送方的发送窗口 min(rwnd, cwnd)。rwnd=0 时发送方停止,并周期性发 窗口探测(Zero Window Probe) 确认接收方恢复。

机制主动方目的
滑动窗口接收方匹配接收缓冲能力
拥塞控制发送方匹配网络承载能力
延迟确认 Delayed ACK接收方合并 ACK 减少开销(常与 Nagle 联动)
Nagle 算法发送方小包聚合,避免大量微小报文

经典互锁问题:Nagle(发送方等 ACK 聚合小包)与延迟确认(接收方等数据再 ACK)相互作用会造成 40ms 级延迟。对交互式应用(如 SSH、低延迟 RPC)应关闭 Nagle(TCP_NODELAY)。

# 设置 TCP_NODELAY 关闭 Nagle
import socket
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)   # 低延迟

7. 拥塞控制:慢启动/拥塞避免/快速重传恢复

7.1 四阶段核心机制

拥塞控制由发送方通过 cwnd(拥塞窗口)实现,与接收方无关,目标是感知并适配网络承载能力。经典 Tahoe/Reno 框架(Linux 默认 CUBIC 是其演进):

阶段行为目的
慢启动每 RTT 翻倍:cwnd = cwnd×2指数探测可用带宽
拥塞避免每 RTT 加 1:cwnd = cwnd + 1接近容量时线性增长
快速重传收到 3 个重复 ACK 即重传丢失段快速恢复(不等超时)
快速恢复拥塞避免进入 cwnd 减半(乘法减)AIMD 收敛公平
cwnd
 │          ssthresh
 │    /|        |
 │   / |   /|   |
 │  /  |  / |   |   拥塞避免(+1/RTT)
 │ /   | /  |  _|__
 │/    |/   | /    (丢包: cwnd = ssthresh, 重新慢启动)
 └──────────────────→ RTT
# Reno 拥塞控制状态机(示意伪代码)
def reno_cwnd_update(event, cwnd, ssthresh):
    if event == "ACK" and cwnd < ssthresh:
        return cwnd * 2                      # 慢启动:指数增长
    if event == "ACK" and cwnd >= ssthresh:
        return cwnd + 1                      # 拥塞避免:加性增长
    if event == "3_dup_ack":                 # 快速恢复:乘法减
        return cwnd // 2
    if event == "timeout":                   # 超时:回到慢启动
        return 1
判据处理
收到 3 个重复 ACK网络轻度拥塞,cwnd 减半,快速恢复
RTO 超时网络严重拥塞,ssthresh=cwnd/2,cwnd 回到 1

现代算法演进:CUBIC(Linux 默认,丢包后凹函数增长)、BBR(Google,基于带宽与延迟测量而非丢包)、Vegas/NewReno/SACK 等。BBR 尤其适合高带宽高延迟链路。

7.2 公平性:AIMD

所有 TCP 流都遵循 AIMD(加性增、乘性减),多个连接会收敛到公平分享带宽的平衡点。这正是拥塞控制"非线性博弈"的核心性质。


8. UDP 与 QUIC

8.1 UDP 特点

UDP 首部仅 8 字节:源端口、目的端口、长度、校验和。无连接、无重传、无拥塞控制,由应用自己控制时机与可靠性,因而延迟低、实现简单。

UDP 首部
| 源端口(16) | 目的端口(16) |
| 长度(16)   | 校验和(16)   |
应用为什么用 UDP
DNS单次查询请求/响应,无连接成本
实时音视频容忍丢包,拒绝重传造成的延迟抖动
游戏低延迟优先,状态同步可预测
QUIC在 UDP 之上自建可靠传输(见下)

8.2 QUIC(HTTP/3 底层)

QUIC(RFC 9000)基于 UDP 实现类 TCP 的可靠传输,解决 TCP 的两大痛点:队头阻塞(HOL,TCP 单流丢包阻塞所有数据)与握手延迟(TLS 需要额外往返)。

特性TCP+TLSQUIC
握手1-2 个 RTT(TLS1.3 前更多)1 个 RTT(0-RTT 恢复)
队头阻塞有(字节流单队列)无(多流独立)
连接迁移四元组绑定,换 IP 即断连接 ID 不依赖 IP
实现内核用户态(库实现,易迭代)

生产上 HTTP/3 即基于 QUIC;Linux 内核尚未原生实现 QUIC,实际由 ngtcp2/quic-go/lsquic 等库在用户态完成。


9. TIME_WAIT 与性能调优

9.1 TIME_WAIT 存在的意义

主动关闭方进入 TIME_WAIT,持续 2×MSL(默认 60s)。两个目的:1)保证最后的 ACK 丢失时能重发;2)让旧连接的报文在网络中彻底消失,避免其污染使用相同四元组的新连接。

现象原因处理
大量 TIME_WAIT高并发短连接(如 nginx 反向代理)开启端口复用,调大 net.ipv4.tcp_max_tw_buckets
大量 CLOSE-WAIT应用未调用 close()(连接泄漏)查应用层代码,非内核问题
大量 SYN_RECV半连接队列满 / SYN Flood调大 tcp_max_syn_backlog,开启 SYN Cookies
RST 复位对端不可达或应用主动拒绝检查防火墙与监听状态

9.2 Linux 内核调优参数

# 常用 TCP 内核参数
sysctl -w net.ipv4.tcp_tw_reuse=1          # 允许复用 TIME_WAIT 中的连接(新连接)
sysctl -w net.ipv4.tcp_max_syn_backlog=1024   # 半连接队列上限
sysctl -w net.core.somaxconn=1024          # 全连接队列(listen backlog 上限)
sysctl -w net.ipv4.tcp_fin_timeout=30      # FIN-WAIT-2 超时(秒)
sysctl -w net.ipv4.tcp_sack=1              # 选择性确认
sysctl -w net.ipv4.tcp_rmem="4096 87380 6291456"   # 接收缓冲区自动调优
调优参数场景说明
tcp_tw_reuse高并发短连接新连接可复用 TIME_WAIT 四元组(需时间戳开启)
tcp_fin_timeout大量 FIN-WAIT-2缩短异常连接回收时间
tcp_rmem/wmem高 BDP 链路窗口大小影响吞吐上限
somaxconn + backlog连接排队应用层 listen(backlog) 须同步调大

9.3 性能模型与工具

关键公式:吞吐上限 = min(发送窗口, 拥塞窗口) / RTT。高延迟高带宽场景(如跨洲链路)必须用 BDP 计算窗口:窗口 ≥ 带宽 × RTT,否则链路利用率不足。

# 性能测量工具
ss -tin 'sport = :443'       # 查看 rtt、cwnd、mss、sack 等指标
iperf3 -c <host> -P 4 -w 1m  # 测吞吐,指定窗口
netstat -s                   # 协议栈统计(重传率、丢包、RST)

排障思路:先看协议栈统计定位丢包/重传;再看状态分布定位连接堆积;最后用抓包(tcpdump/Wireshark)确认握手与重传细节。


参考文章

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「计算机基础」更多文章

  1. 22. CPU 缓存与一致性
  2. 20. 编译原理基础
  3. 19. 数据库原理基础