运输层

基本概念

运输层(Transport Layer)为运行在不同主机上的应用进程提供逻辑通信能力:

  • 发送端:将应用层报文分割为(segment)并传递给网络层
  • 接收端:重组分段为完整报文并递交给应用层
  • 典型协议:TCP 和 UDP (User Datagram Protocol, 用户数据报协议)

协议对比

特性 TCP UDP
可靠性 可靠传输 尽最大努力交付
连接方式 面向连接 无连接
流量控制
传输单位 字节流 数据报文

多路复用与分解

  • 多路复用(Multiplexing, Mux):将多个应用进程的数据合并为一个传输层报文
  • 分解(Demultiplexing, Demux):将传输层报文分发给对应的应用进程
graph LR
    subgraph "多路复用"
        A["数据流 A"] --> Mux["多路复用器"]
        B["数据流 B"] --> Mux
        C["数据流 C"] --> Mux
        Mux --> Combined["组合数据流"]
    end

    subgraph "解复用"
        Combined --> Demux["解复用器"]
        Demux --> A1["数据流 A'"]
        Demux --> B1["数据流 B'"]
        Demux --> C1["数据流 C'"]
    end

    linkStyle 0,1,2 stroke:#00AA00,stroke-width:2px;
    linkStyle 4,5,6 stroke:#AA0000,stroke-width:2px;
    style Mux fill:#ccf,stroke:#88f,stroke-width:2px
    style Demux fill:#fcc,stroke:#f88,stroke-width:2px

UDP 是最小复用的传输层协议,仅提供多路复用与分解功能,不保证可靠传输。在如视频会议等实时应用场景中,UDP 通常优于 TCP。

而 TCP 提供了可靠传输、流量控制、拥塞控制等功能,适用于如文件传输等对可靠性要求较高的场景。

端口与套接字

  • 端口(Port):16 位标识符(0-1023 为知名端口)
  • 套接字(Socket):应用程序进程与操作系统(中的传输层)交换网络消息的软件抽象。
    • UDP:<本地 IP, 本地端口>
    • TCP:<本地 IP, 本地端口, 远端 IP, 远端端口>
graph LR
    subgraph "应用程序"
        A["应用 A<br>端口 1234"] --> Mux["多路复用器"]
        B["应用 B<br>端口 5678"] --> Mux
        C["应用 C<br>端口 9012"] --> Mux
    end
    
    Mux --> Channel["共享通道<br>(例如:网络连接)"]
    
    style Mux fill:#ccf,stroke:#888,stroke-width:2px
    linkStyle 0,1,2 stroke:#0aa,stroke-width:2px,stroke-dasharray: 5 5
    style Channel fill:#ffc,stroke:#888,stroke-width:2px,text-align:center
    linkStyle 3 stroke:#aa0,stroke-width:3px

    S["多路复用策略<br>(例如:端口号、协议)"] --> Mux
    style S fill:#fcc,stroke:#888
    linkStyle 4 stroke-dasharray: 2 2,stroke:#a0a

classDef App fill:#eee,stroke:#888
class A,B,C App

解复用过程

  1. 主机根据数据包头部的目标端口号定位对应套接字
  2. UDP 使用二元组(目标 IP + 端口)
  3. TCP 使用四元组(源/目标 IP + 源/目标端口)
graph LR
    Channel["共享通道<br>(例如:网络连接)"] --> Demux["解复用器"]
    
    subgraph "应用程序"
        Demux --> A["应用 A<br>端口 1234"]
        Demux --> B["应用 B<br>端口 5678"]
        Demux --> C["应用 C<br>端口 9012"]
    end

    style Demux fill:#ccf,stroke:#888,stroke-width:2px
    linkStyle 0 stroke:#aa0,stroke-width:3px
    style Channel fill:#ffc,stroke:#888,stroke-width:2px,text-align:center
    linkStyle 1,2,3 stroke:#0aa,stroke-width:2px,stroke-dasharray: 5 5

    S["解复用策略<br>(例如:端口号、协议)"] --> Demux
    style S fill:#fcc,stroke:#888
    linkStyle 4 stroke-dasharray: 2 2,stroke:#a0a

classDef App fill:#eee,stroke:#888
class A,B,C App

可靠传输设计

核心挑战:

  • 数据包可能:损坏、丢失、延迟、乱序、重复
  • 解决方法矩阵:
    • 校验和(Checksums)\to 检测损坏
    • 序列号(Sequence Numbers)\to 识别数据包
    • 确认机制(ACK/NACK[1]\to 反馈接收状态
    • 超时重传(Retransmission)\to 处理丢失

停等协议

停等协议(Stop-and-Wait Protocol):发送方发送一个数据包后等待接收方的确认,再发送下一个数据包。

sequenceDiagram
    Sender->>Receiver: 发送数据帧 0
    activate Sender
    activate Receiver
    Receiver-->>Sender: 确认 ACK0
    deactivate Receiver
    deactivate Sender

    Sender->>Receiver: 发送数据帧 1
    activate Sender
    activate Receiver
    Receiver-->>Sender: 确认 ACK1
    deactivate Receiver
    deactivate Sender

    Note right of Receiver:  每次发送一帧,等待确认后再发送下一帧。

超时与重传:

sequenceDiagram
    Sender->>Receiver: 发送数据帧 0
    activate Sender
    Note right of Sender: 启动计时器
    activate Receiver
    Receiver--xSender: 确认 ACK0 丢失
    deactivate Receiver
    Sender->>Receiver: 超时,重传数据帧 0
    activate Receiver
    Receiver-->>Sender: 确认 ACK0
    deactivate Receiver
    deactivate Sender
    Note right of Receiver:  处理超时和重传机制。
  • 优点:实现简单
  • 缺点:信道利用率低(U=L/RRTT+L/RU = \frac{L/R}{\mathrm{RTT} + L/R}

流水线协议

流水线协议(Pipelining Protocol):发送方连续发送多个数据包,接收方连续确认。

sequenceDiagram
    Sender->>Receiver: 发送数据帧 0
    Sender->>Receiver: 发送数据帧 1
    Sender->>Receiver: 发送数据帧 2
    activate Sender
    activate Receiver

    Receiver-->>Sender: 确认 ACK0
    Receiver-->>Sender: 确认 ACK1
    Receiver-->>Sender: 确认 ACK2

    deactivate Receiver
    deactivate Sender

    Note right of Receiver:  连续发送多帧,无需等待确认。

滑动窗口机制

graph LR
    subgraph 发送方
        A[发送窗口] --> B(已发送未确认)
        B --> C(可发送)
    end
    subgraph 接收方
        D[接收窗口] --> E(已接收已确认)
        E --> F(可接收)
    end

    A -- 数据传输 --> D
    D -- 确认ACK --> A

    style A fill:#ccf,stroke:#888,stroke-width:2px
    style D fill:#ccf,stroke:#888,stroke-width:2px
  • 发送窗口:允许连续发送 n 个未确认数据包
  • 接收窗口:缓存乱序到达的数据包

确认的方式:

  • 累计确认(Cumulative Acknowledgement):接收方通过发送最高连续接收到的字节序列号来确认数据。
    • 例如,若接收方已收到字节 1-1000 和 2001-3000,但 1001-2000 丢失,则它仍会发送 ACK=1001(即「期望下一个收到 1001」)。
    • 优点:
      • 实现简单,默认在 TCP 中使用。
      • 减少 ACK 报文数量(一个 ACK 可确认多个数据包)。
    • 缺点:
      • 重传效率低:若中间有数据包丢失,发送方会重传所有未被确认的包(即使后续包已到达)。
      • 例如:发送方发送包 1, 2, 3, 4, 5,若包 3 丢失,接收方 ACK=3;即使包 4, 5 已到达,发送方仍需重传包 3, 4, 5。
    • 应用场景:传统 TCP,低丢包率网络
  • 选择确认(Selective Acknowledgement):接收方通过 SACK 选项明确告知发送方哪些非连续的数据块已成功接收
    • 例如,若接收方收到字节 1-1000 和 2001-3000,但 1001-2000 丢失,它会发送:ACK=1001(累计确认)+ SACK 块=2001-3000(选择确认)。
    • 优点:
      • 重传效率高:发送方仅重传丢失的包(如 1001-2000),无需重传已收到的数据(2001-3000)。
      • 显著减少冗余重传,提升高丢包率网络下的性能。
    • 缺点:
      • 实现复杂,需双方协商启用(TCP 选项)。
      • ACK 报文略大(需携带 SACK 块信息)。
    • 应用场景:高丢包率网络(如无线网络)、需要高效重传的现代 TCP 实现(如 Linux 默认启用 SACK)。

相应有重传策略:

  • GBN (Go-Back-N):重传窗口内所有未确认数据包
  • SR (Selective Repeat):仅重传丢失数据包
机制 Go-Back-N (GBN) Selective Repeat (SR)
重传策略 重传所有未确认包 仅重传丢失包
接收方处理 丢弃乱序包 缓存乱序包
确认方式 累计确认 选择确认
适用场景 低误码率环境 高误码率环境

GBN 示例:

sequenceDiagram
    Sender->>Receiver: 发送帧 0, 1, 2
    activate Sender
    activate Receiver
    Receiver-->>Sender: ACK1 丢失
    Receiver->>Sender: ACK2
    deactivate Receiver
    Note right of Sender:  收到乱序 ACK, 触发重传从帧 1 开始的所有帧
    Sender->>Receiver: 重传帧 1, 2
    activate Receiver
    Receiver-->>Sender: ACK1, ACK2
    deactivate Receiver
    deactivate Sender

SR 示例:

sequenceDiagram
    Sender->>Receiver: 发送帧 0, 1, 2
    activate Sender
    activate Receiver

    Receiver-->>Sender: ACK0 丢失
    Receiver->>Sender: ACK1, ACK2
    deactivate Receiver

    Note right of Sender: 只重传丢失的帧 0

    Sender->>Receiver: 重传帧 0
    activate Receiver
    Receiver-->>Sender: ACK0
    deactivate Receiver
    deactivate Sender

性能优化

  • 理想吞吐量:Throughput=min(n×LRTT,R)\text{Throughput} = \min(\frac{n \times L}{\mathrm{RTT}}, R)
  • 窗口大小与 RTT 关系:noptimal=RTT×RLn_{\text{optimal}} = \lceil \frac{\mathrm{RTT} \times R}{L} \rceil

定时器管理

  • 每个未确认包维护独立定时器(SR)
  • 仅首个未确认包维护定时器(GBN)

超时重传示例

当发送方在 200 ms\pu{200ms} 内未收到 ACK,触发重传机制:
Timeout=EstimatedRTT+4×DevRTT\pu{Timeout} = \text{EstimatedRTT} + 4 \times \text{DevRTT}

流量控制

  • 接收方通过通告窗口(Advertised Window)控制发送速率
  • 窗口大小动态调整:W=min(CongWin,AdvWin)W = \min(\mathrm{CongWin}, \mathrm{AdvWin})

UDP

UDP (User Datagram Protocol, 用户数据报协议) 是一种轻量级的传输层协议,提供了一种不可靠、无连接的数据传输服务。

特点

  • 轻量级通信:避免了为保证顺序和可靠性带来的开销和延迟。
  • 无连接
    • UDP 发送方和接收方之间没有握手过程。
    • 每个 UDP 段的处理都独立于其他段。
  • 尽最大努力服务:UDP 报文段可能丢失或乱序到达。
  • RFC 768:UDP 协议在 1980 年发布的 RFC 768 中描述。

UDP 使用场景

  • 流媒体应用(容忍丢失,对速率敏感)
  • DNS(域名系统)
  • SNMP(简单网络管理协议)

为什么需要 UDP?

  • 无需建立连接:减少了连接建立的延迟。
  • 简单:发送方和接收方无需维护连接状态。
  • 头部开销小:UDP 头部较小,减少了数据传输的额外开销。
  • 无拥塞控制:UDP 可以尽可能快地发送数据。

UDP 报文段格式

1
2
3
4
5
6
7
8
9
10
11
12
 0                                31
+--------+--------+--------+--------+
| Source | Destination |
| Port | Port |
+--------+--------+--------+--------+
| Length | Checksum |
+--------+--------+--------+--------+
| |
| Application Data |
| (message) |
| |
+-----------------------------------+
  • Source Port:源端口号(16 位)
  • Destination Port:目标端口号(16 位)
  • Length:长度,包括头部和数据的总长度,以字节为单位(16 位)
  • Checksum:校验和(16 位)
    • 用于检测传输过程中产生的错误(例如,位翻转)
    • 计算方法:将报文段内容(包括头部字段)视为 16 位整数序列,进行加法运算,并将结果的补码作为校验和。
    • 发送方将校验和值放入 UDP 校验和字段。
    • 接收方计算接收到的报文段的校验和,并与校验和字段的值进行比较。如果相等,则认为没有检测到错误;如果不相等,则认为检测到错误。
    • 校验和也可以设置为 0,表示不进行校验。

UDP 校验和示例

假设有两个 16 位整数:

1
2
1110011001100110
1101010101010101

进行加法运算:

1
2
3
4
 1110011001100110
1101010101010101
------------------
11011101110111011

由于最高位有进位,需要将进位加到结果的最低位:

1
2
3
4
1011101110111011
1
------------------
1011101110111100

取反得到校验和:

0100010001000011

在进行加法运算时,如果最高位产生进位,需要将进位加到结果的最低位。

TCP

TCP (Transmission Control Protocol, 传输控制协议) 是一种面向连接、可靠的传输层协议,提供了一种可靠的、有序的字节流传输服务。

TCP 抽象

TCP 提供了一种可靠的、有序的字节流(reliable, in-order, byte stream)服务:

  • 可靠性:TCP 通过重传丢失的数据包来保证可靠性。
    • 直到超时并关闭连接
  • 有序性:TCP 仅将连续的数据块传递给应用程序。
  • 字节流:TCP 将数据视为字节流,并尝试将其传递给应用程序。

TCP 头部

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
 0                                31
+--------+--------+--------+--------+
| Source | Destination |
| Port | Port |
+--------+--------+--------+--------+
| Sequence Number |
+--------+--------+--------+--------+
| Acknowledgment Number |
+-----+---+-------+--------+--------+
| Hdr | | Flags | Advert |
| Len | 0 | | Window |
+-----+---+-------+--------+-+------+
| Checksum | Urgent Pointer |
+--------+--------+--------+--------+
| Options (variable) |
+-----------------------------------+
| |
| Application Data |
| (message) |
| |
+-----------------------------------+
  • Source Port:源端口号(16 位)
  • Destination Port:目标端口号(16 位)
  • Sequence Number:序列号(32 位)
    • 表示该报文段中数据的第一个字节在字节流中的偏移量。
    • 由于 TCP 是字节流协议,因此序列号是字节偏移量,而不是报文段 ID。
  • Acknowledgment Number:确认号(32 位)
    • 表示期望接收的下一个字节的序列号。
    • 接收方发送的确认号是其已按序接收到的最后一个字节的序列号加 1。
  • HdrLen:头部长度(4 位)
    • 表示 TCP 头部的长度,以 4 字节为单位。
    • 如果头部没有选项字段,则头部长度为 5(即 20 字节)。
  • Flags:标志位(6 位)
    • SYN:同步标志,用于建立连接。
    • ACK:确认标志,用于确认接收到的数据。
    • FIN:结束标志,用于关闭连接。
    • RST:复位标志,用于重置连接。
    • PSH:推送标志,用于指示接收方应立即将数据推送给应用程序。
    • URG:紧急标志,用于指示报文段中包含紧急数据。
  • Advertised Window:通告窗口(16 位)
    • 用于流量控制,表示接收方当前能够接收的字节数。
  • Checksum:校验和(16 位)
    • 计算范围包括 TCP 头部、数据以及伪头部。
  • Urgent Pointer:紧急指针(16 位)
    • 当 URG 标志位置 1 时有效,指示紧急数据在报文段中的偏移量。
  • Options:选项(可变长度)
    • 用于协商 MSS、窗口缩放等。

Source Port 与 Destination Port 用来多路复用(Mux)和分解数据流(Demux),Sequence Number 和 Acknowledgment Number 用来实现可靠传输。

TCP 报文段、数据包和帧

  • IP 数据包(IP packet):
    • 最大长度不能超过最大传输单元(Maximum Transmission Unit, MTU)。
    • 例如,以太网的 MTU 通常为 1500 字节。
  • TCP 数据包(TCP packet):
    • 包含 TCP 头部和数据的 IP 数据包。
    • TCP 头部长度至少为 20 字节。
  • TCP 报文段(TCP segment):
    • 最大长度不能超过最大报文段长度(Maximum Segment Size, MSS)。
    • 例如,以太网环境下,MSS 通常为 1460 字节(1500 - 20 - 20)。
    • MSS = MTU - IP 头部长度 - TCP 头部长度

序列号(Sequence Numbers)

ISN(Initial Sequence Number):初始序列号,在建立连接时随机生成。

  • 序列号表示报文段中第一个字节在字节流中的位置。
graph LR
    subgraph Host A
        A[字节流] --> B(ISN)
        B --> C[k bytes]
        C --> D[报文段]
        D --> E["Sequence number = ISN + k"]
    end

    style A fill:#ccf,stroke:#888,stroke-width:2px
    style D fill:#ccf,stroke:#888,stroke-width:2px

为什么要有 ISN?为什么不直接从 0 开始编号?这是为了安全和避免混淆。

  • 防止旧连接的干扰:如果每次都从 0 开始,那么如果网络中残留了之前连接的延迟报文段,这些旧报文段的序列号可能会和新连接的序列号冲突,导致数据错乱。
  • 提高安全性:随机的 ISN 使得攻击者更难猜测序列号,从而增加伪造 TCP 报文段的难度。
操作 发送字节 当前序列号计算 报文段序列号
发送 SYN - ISN = 1000 1000
发送 "Hello" 5 1000 + 1 + 5 = 1006 1001
发送 "World" 5 1006 + 5 = 1011 1006
发送 FIN - 1011 → 结束于1012 1011

SYN 和 FIN 各占 1 个序列号位置,即使不携带数据。

确认号和序列号

  1. 发送方发送数据包
    • 数据从序列号 X 开始。
    • 数据包包含 B 字节的数据:[X, X+1, X+2, …, X+B-1]。
  2. 接收方接收到数据包后,发送 ACK
    • 如果 X 之前的所有数据都已按序接收:
      • ACK 确认 X+B(因为这是下一个期望的字节)。
    • 如果接收到的最高有序字节是 Y,且 (Y+1) < X:
      • ACK 确认 Y+1。
      • 即使之前已经确认过 Y+1,也会再次确认。
graph LR
    subgraph Host A
        A[字节流] --> B(ISN)
        B --> C[k bytes]
        C --> D[TCP Data]
        D --> E[TCP HDR]
        E --> F["Sequence number = ISN + k"]
    end

    subgraph Host B
        G[TCP Data] --> H[TCP HDR]
        H --> I["ACK sequence number = next expected byte = seqno + length(data)"]
    end

    D -- 数据传输 --> G
    H -- ACK --> E

    style A fill:#ccf,stroke:#888,stroke-width:2px
    style D fill:#ccf,stroke:#888,stroke-width:2px
    style G fill:#ccf,stroke:#888,stroke-width:2px

典型操作

  • Sender: seqno=X, length=B
  • Receiver: ACK=X+B
  • Sender: seqno=X+B, length=B
  • Receiver: ACK=X+2B
  • Sender: seqno=X+2B, length=B

下一个数据包的序列号与上一个 ACK 字段相同。

TCP 采用的可靠数据传输机制

  • 校验和(Checksum):用于检测数据是否损坏。
  • 序列号(Sequence numbers):字节偏移量,用于识别数据包。
  • 累计确认(Cumulative acknowledgements):类似 GBN,接收方发送累计确认。
  • 选择重传:接收方可以缓存乱序的数据包(类似 SR)。
  • 单一重传计时器:发送方维护一个单一的重传计时器(类似 GBN)。
  • 快速重传(Fast retransmit):通过重复 ACK 触发提前重传。
    • 重复 ACK 是孤立丢包的信号。
      • ACK 没有进展,意味着 500 还没有被传递。
      • ACK 流意味着一些数据包正在被传递。
    • 收到 k 个重复 ACK 后触发重传。
      • TCP 使用 k=3。
      • 比等待超时更快。
  • 超时重传
    • 如果发送方在超时时间内没有收到 ACK,则重传窗口中的第一个数据包。
    • 如何选择超时值?
      • 太长:连接吞吐量低。
      • 太短:重传的数据包可能只是延迟了。
      • 解决方案:使超时时间与 RTT 成比例。
      • 如何测量 RTT?

RTT 估计:迭代式估计

EstimatedRTT=(1α)×EstimatedRTT+α×SampleRTT\text{EstimatedRTT} = (1 - \alpha) \times \text{EstimatedRTT} + \alpha \times \text{SampleRTT}

TCP 连接管理

TCP 连接建立

TCP 使用三次握手(three-way handshake)建立连接:

  1. 客户端向服务器发送一个 SYN 报文段(SYN=1,seq=x)。
    • SYN:同步标志,用于发起连接。
    • seq:客户端的初始序列号。
  2. 服务器收到 SYN 后,回复一个 SYN-ACK 报文段(SYN=1,ACK=1,seq=y,ack=x+1)。
    • SYN:同步标志。
    • ACK:确认标志。
    • seq:服务器的初始序列号。
    • ack:确认客户端的 SYN,值为客户端的初始序列号 + 1。
  3. 客户端收到 SYN-ACK 后,回复一个 ACK 报文段(ACK=1,seq=x+1,ack=y+1)。
    • ACK:确认标志。
    • seq:客户端的序列号 + 1。
    • ack:确认服务器的 SYN,值为服务器的初始序列号 + 1。
sequenceDiagram
    participant Client
    participant Server

    Note over Client,Server: 正常情况
    Client->>Server: SYN (seq=x)
    activate Server
    Server->>Client: SYN-ACK (seq=y, ack=x+1)
    activate Client
    Client->>Server: ACK (seq=x+1, ack=y+1)
    deactivate Client
    deactivate Server

    Note over Client,Server: 异常情况 1:SYN 丢失
    Client->>Server: SYN (seq=x)
    Client->>Client: 超时重传 SYN
    Client->>Server: SYN (seq=x)
    activate Server
    Server->>Client: SYN-ACK (seq=y, ack=x+1)
    activate Client
    Client->>Server: ACK (seq=x+1, ack=y+1)
    deactivate Client
    deactivate Server

    Note over Client,Server: 异常情况 2:SYN-ACK 丢失
    Client->>Server: SYN (seq=x)
    activate Server
    Server->>Client: SYN-ACK (seq=y, ack=x+1)
    deactivate Server
    Server->>Server: 超时重传 SYN-ACK
    Server->>Client: SYN-ACK (seq=y, ack=x+1)
    activate Client
    Client->>Server: ACK (seq=x+1, ack=y+1)
    deactivate Client

    Note over Client,Server: 异常情况 3:ACK 丢失
    Client->>Server: SYN (seq=x)
    activate Server
    Server->>Client: SYN-ACK (seq=y, ack=x+1)
    activate Client
    Client->>Server: ACK (seq=x+1, ack=y+1)
    deactivate Client
    deactivate Server
    Client->>Client: 数据传输
    Server->>Server: 可能超时重传数据, 认为连接未建立
  • 三次握手的作用:确认双方的 SYN 和序号。

如果 SYN 数据包丢失怎么办?

  • 假设 SYN 数据包丢失:数据包被网络丢弃或服务器繁忙。
  • 最终,没有 SYN-ACK 到达。
  • 发送方在超时时重传 SYN。
  • TCP 发送方应该如何设置计时器?
    • 发送方不知道接收方有多远。
    • 很难猜测一个合理的等待时间。
    • RFC 1122 和 2988 规定默认值为 3 秒。
    • 一些实现使用 6 秒。

TCP 连接关闭

TCP 使用四次挥手关闭连接:

  1. 客户端向服务器发送一个 FIN 报文段(FIN=1)。
    • FIN:结束标志,用于关闭连接。
  2. 服务器收到 FIN 后,回复一个 ACK 报文段。
    • 此时,连接处于半关闭状态,服务器仍然可以向客户端发送数据。
  3. 服务器准备好关闭连接后,向客户端发送一个 FIN 报文段(FIN=1)。
  4. 客户端收到 FIN 后,回复一个 ACK 报文段。
    • 客户端进入 TIME_WAIT 状态,等待一段时间以确保服务器收到 ACK。
    • 如果 ACK 丢失,服务器将重传 FIN。
sequenceDiagram
    participant A
    participant B
    A->>B: FIN
    activate B
    B-->>A: ACK
    Note over A,B: 连接半关闭
    B->>A: FIN
    activate A
    A-->>B: ACK
    deactivate A
    deactivate B
    Note over A: TIME_WAIT
    Note over A,B: 连接关闭
  • 正常终止,一次关闭一端:
    • 使用 Finish (FIN) 关闭并接收剩余字节。
      • FIN 在序列空间中占用一个字节。
    • 另一方确认该字节以确认。
    • 关闭连接的 A 端,但不关闭 B 端。
      • 直到 B 同样发送 FIN。
      • 然后 A 确认。
  • 正常终止,两端同时关闭:与之前相同,但 B 在确认 A 的 FIN 的同时设置 FIN。
sequenceDiagram
    participant Client
    participant Server

    Note over Client,Server: Client 主动关闭连接
    Client->>Server: FIN (seq=x)
    activate Server
    Server->>Client: ACK (seq=y, ack=x+1)
    Server->>Server: 准备关闭
    Server->>Client: FIN (seq=z)
    activate Client
    Client->>Server: ACK (seq=x+1, ack=z+1)
    deactivate Client
    deactivate Server

    Note over Client,Server: Server 主动关闭连接
    Server->>Client: FIN (seq=x)
    activate Client
    Client->>Server: ACK (seq=y, ack=x+1)
    Client->>Client: 准备关闭
    Client->>Server: FIN (seq=z)
    activate Server
    Server->>Client: ACK (seq=x+1, ack=z+1)
    deactivate Client
    deactivate Server

    Note over Client,Server: 同时关闭
    Client->>Server: FIN (seq=x)
    activate Server
    Server->>Client: FIN (seq=z)
    activate Client
    Client->>Server: ACK (seq=y, ack=z+1)
    Server->>Client: ACK (seq=x+1, ack=z+1)
    deactivate Client
    deactivate Server
  • 突然终止:
    • A 向 B 发送 RESET (RST)。
      • 例如,因为 A 上的应用程序崩溃。
    • B 不确认 RST。
      • 因此,RST 不会可靠地传递,并且任何正在传输的数据都会丢失。
      • 但是:如果 B 发送更多内容,将引发另一个 RST。
sequenceDiagram
    participant Client
    participant Server

    Note over Client,Server: 情况 1:连接不存在,收到数据
    Client->>Server: 数据包 (seq=x)
    activate Server
    Server->>Client: RST 
    deactivate Server

    Note over Client,Server: 情况 2:收到期望之外的包
    Client->>Server: SYN (seq=x)
    activate Server
    Server->>Client: SYN-ACK (seq=y, ack=x+1)
    activate Client
    Client->>Client: 客户端异常
    Client->>Server: RST
    deactivate Client
    deactivate Server

    Note over Client,Server: 情况 3:超时或其他错误导致连接异常
    Client->>Server: 数据包 (seq=x)
    activate Server
    Server->>Server: 超时/其他错误
    Server->>Client: RST
    deactivate Server
    Client->>Client: 连接中断

    Note over Client,Server: 情况 4:端口不存在/未监听
    Client->>Server: SYN (seq=x)
    activate Server
    Server->>Server: 端口未监听
    Server->>Client: RST
    deactivate Server
    Client->>Client: 连接被拒绝

RST 表示重置连接,通常表示有严重错误发生,连接需要立即中断。

  1. 连接不存在,收到数据

    • Server 收到一个数据包,但是找不到对应的连接,说明可能连接已经关闭或者从来没有建立。
    • Server 回复 RST 包,通知 Client 连接不存在。
  2. 收到期望之外的包

    • Client 与 Server 建立连接的过程中,Client 发生异常 (例如应用程序崩溃)。
    • Client 发送 RST 包,通知 Server 重置连接。
  3. 超时或其他错误导致连接异常

    • Client 发送数据包后,Server 因为某些原因 (超时、资源不足等) 无法处理。
    • Server 发送 RST 包,中断连接。
  4. 端口不存在/未监听

    • Client 尝试连接 Server 的某个端口,但该端口没有程序监听。
    • Server (或者网络设备) 发送 RST 包,拒绝连接。

TCP 状态转换图

客户端状态转换图

stateDiagram
    [*] --> CLOSED
    CLOSED --> SYN_SENT: Send SYN
    SYN_SENT --> ESTABLISHED: Receive SYN-ACK, Send ACK
    ESTABLISHED --> FIN_WAIT_1: Send FIN
    FIN_WAIT_1 --> FIN_WAIT_2: Receive ACK, Send Nothing
    FIN_WAIT_2 --> TIME_WAIT: Receive FIN, Send ACK
    TIME_WAIT --> CLOSED: Wait 30 sec

服务器端状态转换图

stateDiagram
    [*] --> CLOSED
    CLOSED --> LISTEN: Create a listen socket
    LISTEN --> SYN_RCVD: Receive SYN, Send SYN-ACK
    SYN_RCVD --> FIN_WAIT_1: Receive ACK, Send Nothing
    FIN_WAIT_1 --> CLOSE_WAIT: Receive FIN, Send ACK
    CLOSE_WAIT --> LAST_ACK: Send FIN
    LAST_ACK --> CLOSED: Receive ACK, Send Nothing

TCP 流量控制

流量控制(Flow Control)是 TCP 的一项关键功能,旨在防止发送方发送速率过快,导致接收方缓冲区溢出。

滑动窗口回顾

TCP 使用滑动窗口(Sliding Window)机制进行流量控制。发送方和接收方各自维护一个窗口:

  • 发送窗口
    • 左边界:第一个未确认字节的序列号。
    • 右边界:左边界 + 窗口大小。
    • 窗口大小:限制了发送方在收到确认前可以发送的未确认数据量。
  • 接收窗口
    • 左边界:期望接收的下一个字节的序列号。
    • 右边界:左边界 + 窗口大小。
    • 窗口大小:接收方缓冲区可用空间的大小。
graph LR
    subgraph 发送方
        A[发送窗口] --> B(已发送未确认)
        B --> C(可发送)
    end
    subgraph 接收方
        D[接收窗口] --> E(已接收已确认)
        E --> F(可接收)
    end

    A -- 数据传输 --> D
    D -- 确认ACK --> A

    style A fill:#ccf,stroke:#888,stroke-width:2px
    style D fill:#ccf,stroke:#888,stroke-width:2px

通告窗口

  • 通告窗口(Advertised Window, RWND[2]):接收方在 ACK 报文中告知发送方其当前可接收的字节数。
    • RWND = 接收方缓冲区大小 - (最后接收字节 - 最后读取字节)
    • 接收方通过在 ACK 中包含 RWND 值来动态调整发送方的发送速率。
  • 发送方根据 RWND 调整发送窗口大小,确保未确认字节数不超过 RWND。

接收方的滑动窗口:

发送方的滑动窗口:

graph TB
    %% 发送方子图
    subgraph "发送方(Sender)"
        SendingProcess["发送进程<br>(Sending Process)"] --> TCP["TCP 缓冲区<br>(TCP Buffer)"]
        TCP -- "滑动窗口限制<br>(≤ RWND)" --> LastByteWritten["最后写入字节<br>(Last Byte Written)"]
        FirstUnACKedByte["第一个未确认字节<br>(First UnACKed Byte)"] -->|数据等待确认| LastByteWritten
        LastByteSent["最后发送字节<br>(Last Byte Sent)"] -->|可发送范围| LastByteCanSend["可发送的最后字节<br>(Last Byte Can Send)"]
    end

    %% 接收方子图
    subgraph "接收方(Receiver)"
        LastByteRead["最后读取字节<br>(Last Byte Read)"] -->|可用缓冲区| BufferSize["缓冲区大小(B)<br>(Buffer Size)"]
        NextByteNeeded["下一个需要的字节<br>(Next Byte Needed)"] -->|接收窗口| LastByteReceived["最后接收字节<br>(Last Byte Received)"]
        RWND["接收窗口大小<br>RWND = B - (LastByteReceived - LastByteRead)"] -->|动态计算| NextByteNeeded
    end

    %% 链接发送方和接收方
    LastByteCanSend -->|基于 RWND 限制| NextByteNeeded

    %% 样式定义
    classDef sender fill:#e6f3ff,stroke:#333,stroke-width:2px
    classDef receiver fill:#ffe6e6,stroke:#333,stroke-width:2px
    classDef key fill:#f9f9f9,stroke:#333,stroke-width:4px,font-weight:bold

    %% 应用样式
    class SendingProcess,TCP,LastByteWritten,FirstUnACKedByte,LastByteSent,LastByteCanSend sender
    class LastByteRead,BufferSize,NextByteNeeded,LastByteReceived,RWND receiver
    class RWND key
  • 发送方窗口随着新数据被确认而前进。
  • 接收方窗口随着接收进程消耗数据而前进。
  • 接收方通告发送方接收窗口的当前右边界。
  • 发送方保证发送的数据量不超过此限制。

由于 UDP 没有流量控制,故可能因缓冲区溢出而丢包。

TCP 拥塞控制

拥塞控制(Congestion Control)是 TCP 的另一项核心功能,旨在避免网络拥塞,提高网络利用率和公平性。

拥塞控制概述

  • 目标
    • 发现可用带宽
    • 适应带宽变化
    • 公平共享带宽
  • 挑战
    • 网络状况复杂多变
    • 缺乏全局信息
    • 需要分布式、自适应的解决方案

拥塞控制设计考虑

  1. 如何感知拥塞?
    • 隐式信号:
      • 数据包延迟:网络拥塞时,数据包在路由器缓冲区中的排队时间增加,导致往返时间(RTT)增大。
      • 数据包丢失:当路由器缓冲区溢出时,会丢弃数据包。
    • 显式信号:
      • ECN (Explicit Congestion Notification):路由器在数据包头部设置拥塞指示位,通知发送方网络拥塞。
  2. 谁负责拥塞控制?
    • 主要由端主机(发送方和接收方)负责。
    • 路由器可以提供辅助信息(如 ECN)。
  3. 如何处理拥塞?
    • 持续自适应:根据网络状况动态调整发送速率。

拥塞控制抽象模型

graph LR
    A["发送主机 A"] --> Buffer["路由器缓冲区"]
    Buffer --> B["接收主机 B"]

    style A fill:#ccf,stroke:#888,stroke-width:2px
    style Buffer fill:#fcc,stroke:#888,stroke-width:2px
    style B fill:#ccf,stroke:#888,stroke-width:2px
  • 忽略路由器内部结构,将其建模为特定输入输出对的单个队列。
  • 拥塞控制的目标是使发送速率与瓶颈带宽相匹配
    • 发送速率过低:带宽未充分利用。
    • 发送速率过高:导致拥塞,数据包丢失。

多流共享与带宽分配

graph LR
    A1["A1"] --> Buffer["缓冲区"]
    A2["A2"] --> Buffer
    A3["A3"] --> Buffer
    Buffer -->|"BW(t)"| Node[" "]
    Node --> B1["B1"]
    Node --> B2["B2"]
    Node --> B3["B3"]

    style A1 fill:#ccf,stroke:#888,stroke-width:2px
    style A2 fill:#ccf,stroke:#888,stroke-width:2px
    style A3 fill:#ccf,stroke:#888,stroke-width:2px
    style Buffer fill:#fcc,stroke:#888,stroke-width:2px
    style B1 fill:#ccf,stroke:#888,stroke-width:2px
    style B2 fill:#ccf,stroke:#888,stroke-width:2px
    style B3 fill:#ccf,stroke:#888,stroke-width:2px
  • 多个流共享同一瓶颈链路时,需要考虑:
    • 总发送速率应与可用带宽匹配。
    • 如何在多个流之间分配带宽。

拥塞控制方法

  1. 不关心
    • 可能导致大量丢包。
  2. 预留
    • 预先协商带宽分配。
    • 需要事先协商。
    • 可能导致低利用率。
  3. 定价
    • 根据出价决定是否丢包。
    • 需要支付模型。
  4. 动态调整
    • 主机推断拥塞程度并调整。
    • 网络向主机报告拥塞程度,主机调整。
    • 结合以上方法。
    • 实现简单但可能次优,动态复杂。

动态调整的通用性已被证明非常强大:

  • 不预设商业模式、流量特征、应用需求
  • 但假设良好的合作精神

TCP 拥塞控制基本问题

  1. 发送方如何检测拥塞?
  2. 发送方如何调整发送速率?

仍然是为实现上面提及的三个目的:

  • 发现可用带宽
  • 适应带宽变化
  • 公平共享带宽

检测拥塞

方式:

  • 数据包延迟
    • 棘手:嘈杂信号(延迟通常变化很大)。
  • 路由器告知端主机:当它们拥塞时。
  • 数据包丢失
    • TCP 已经必须检测的故障安全信号。
    • 复杂性:非拥塞性丢失(例如,校验和错误)。

丢失的类型也不尽相同:

  • 重复 ACK:孤立丢失
    • 仍在接收 ACK
  • 超时:更严重
    • 没有足够的重复 ACK
    • 必须遭受多次丢失

需要针对每种情况以不同方式调整速率。

速率调整

基本结构

  • 收到 ACK(新数据):增加速率
  • 检测到丢失:降低速率

如何增加/减少速率取决于我们所处的拥塞控制阶段:

  • 发现可用瓶颈带宽(慢启动)
  • 适应带宽变化(拥塞避免:AIMD)

慢启动(Slow Start)

慢启动(Slow Start)是 TCP 拥塞控制的初始阶段,旨在快速探测可用带宽。

  • 目标:估计可用带宽
    • 缓慢启动(为了安全)
    • 快速提升(为了效率)
  • 原理
    • 初始拥塞窗口(Congestion Window, CWND)设置为 1 个 MSS(Maximum Segment Size),即「最大数据包大小」。
    • 每收到一个 ACK,CWND 增加 1 个 MSS。
    • 每个 RTT,CWND 翻倍(指数增长)。
  • 何时停止
    • 发生丢包(超时或 3 个重复 ACK)。
    • CWND 达到慢启动阈值(ssthresh)。
sequenceDiagram
    participant Sender
    participant Receiver

    Note over Sender: CWND = 1 MSS, ssthresh = 16 MSS

    Sender->>Receiver: 1 MSS
    activate Sender
    activate Receiver
    Receiver-->>Sender: 1 ACK
    deactivate Receiver
    Note over Sender: CWND = 2 MSS

    Sender->>Receiver: 2 MSS
    activate Receiver
    Receiver-->>Sender: 2 ACK
    deactivate Receiver
    Note over Sender: CWND = 4 MSS

    Sender->>Receiver: 4 MSS
    activate Receiver
    Receiver-->>Sender: 4 ACK
    deactivate Receiver
    Note over Sender: CWND = 8 MSS

    Sender->>Receiver: 8 MSS
    activate Receiver
    Receiver-->>Sender: 8 ACK
    deactivate Receiver
    Note over Sender: CWND = 16 MSS
    deactivate Sender

AIMD(Additive Increase Multiplicative Decrease)

当 CWND 达到 ssthresh 时,TCP 进入拥塞避免(Congestion Avoidance)阶段,采用 AIMD 算法调整 CWND:

  • 加性增(Additive Increase):
    • 每收到一个 ACK,CWND 增加 1/CWND 个 MSS。
    • 每个 RTT,CWND 约增加 1 个 MSS(线性增长)。
  • 乘性减(Multiplicative Decrease):
    • 检测到丢包(3 个重复 ACK):
      • ssthresh = CWND / 2
      • CWND = ssthresh
      • 进入拥塞避免状态
    • 检测到超时:
      • ssthresh = CWND / 2
      • CWND = 1
      • 重新进入慢启动状态
sequenceDiagram
    participant Sender
    participant Receiver

    Note over Sender, Receiver: 拥塞避免阶段
    Sender->>Receiver: CWND 个 MSS
    activate Sender
    activate Receiver
    Receiver-->>Sender: ACK
    deactivate Receiver
    Note over Sender: CWND += 1/CWND

    Note over Sender, Receiver: 收到 3 个重复 ACK
    Sender->>Receiver: CWND 个 MSS
    activate Receiver
    Receiver-->>Sender: 重复 ACK
    Receiver-->>Sender: 重复 ACK
    Receiver-->>Sender: 重复 ACK
    deactivate Receiver
    Note over Sender: ssthresh = CWND / 2, CWND = ssthresh

    Note over Sender, Receiver: 超时
    Sender->>Receiver: CWND 个 MSS
    activate Receiver
    Note right of Sender: 超时
    deactivate Receiver
    Note over Sender: ssthresh = CWND / 2, CWND = 1, 慢启动
    deactivate Sender

TCP 锯齿行为

AIMD 导致 TCP 拥塞窗口呈现锯齿状:

  • 慢启动阶段快速探测带宽。
  • 拥塞避免阶段在平衡点附近震荡。
  • 丢包导致窗口减半。

为什么选择 AIMD?

  • 效率(Efficiency):充分利用可用带宽。
  • 公平性(Fairness):多个 TCP 流共享瓶颈链路时,趋向于平分带宽。

AIMD 的公平性模型:

两个用户,速率分别为 x1x_1x2x_2

  • x1+x2>1x_1 + x_2 > 1 时发生拥塞。
  • x1+x2<1x_1 + x_2 < 1 时未充分利用。
  • x1=x2x_1 = x_2 时实现公平。

AIMD 的收敛性:

  • 增加:x+aIx + a_I
  • 减少:x×bDx \times b_D
  • 收敛到公平

快速恢复(Fast Recovery)

快速恢复是对 AIMD 的优化,旨在更快地从丢包中恢复:

  • 触发条件:收到 3 个重复 ACK。
  • 操作
    • ssthresh = CWND / 2
    • CWND = ssthresh + 3 (考虑到 3 个数据包已离开网络)
    • 每收到一个重复 ACK,CWND 增加 1 个 MSS。
    • 收到新 ACK 后,CWND = ssthresh,进入拥塞避免状态。
sequenceDiagram
    participant Sender
    participant Receiver

    Note over Sender, Receiver: 收到 3 个重复 ACK
    Sender->>Receiver: CWND 个 MSS
    activate Sender
    activate Receiver
    Receiver-->>Sender: 重复 ACK
    Receiver-->>Sender: 重复 ACK
    Receiver-->>Sender: 重复 ACK
    deactivate Receiver
    Note over Sender: ssthresh = CWND / 2, CWND = ssthresh + 3

    Note over Sender, Receiver: 收到更多重复 ACK
    Sender->>Receiver: (CWND + 1) 个 MSS
    activate Receiver
    Receiver-->>Sender: 重复 ACK
    deactivate Receiver
    Note over Sender: CWND += 1

    Note over Sender, Receiver: 收到新 ACK
    Sender->>Receiver: (CWND + 1) 个 MSS
    activate Receiver
    Receiver-->>Sender: 新 ACK
    deactivate Receiver
    Note over Sender: CWND = ssthresh, 进入拥塞避免
    deactivate Sender

TCP 状态机

  • 状态
    • 慢启动(Slow Start)
    • 拥塞避免(Congestion Avoidance)
    • 快速恢复(Fast Recovery)
  • 转换条件
    • 超时(timeout)
    • 3 个重复 ACK(dupACK=3)
    • 新 ACK(new ACK)
    • CWND > ssthresh

TCP 不同版本

  • TCP Tahoe
    • 3 个重复 ACK 或超时:CWND = 1
  • TCP Reno
    • 超时:CWND = 1
    • 3 个重复 ACK:CWND = CWND / 2
  • TCP NewReno
    • TCP Reno + 改进的快速恢复
  • TCP SACK
    • 支持选择确认

路由器辅助的拥塞控制

TCP 的问题

  1. 误判非拥塞性丢失
  2. 填满队列导致高延迟
  3. 短流在发现可用容量之前完成
  4. 高速链路下 AIMD 不实用
  5. 锯齿发现对某些应用来说过于不稳定
  6. 不同 RTT 下不公平
  7. 与可靠性机制紧密耦合
  8. 终端主机可以作弊

1~2 可以靠路由器通知终端解决;3~6 可以靠路由器通知终端发送速率解决;7~8 可以靠路由器强制公平共享解决。

路由器辅助

ECN (Explicit Congestion Notification, 显式拥塞通知):

  • 路由器在数据包头部设置拥塞指示位。
  • 如果数据包设置了该位,则 ACK 也设置 ECN 位。
  • 路由器何时设置该位的多种选择:
    • 链路利用率和数据包延迟之间的权衡。
  • 拥塞语义可以与丢包完全相同:
    • 即,终端主机反应就像看到丢包一样。

  1. ACK (Acknowledgement) / NACK (Negative Acknowledgement),即「确认」「否认」。 ↩︎

  2. 即 Receive Window,通常缩写为 RWND。WND 即 Window,想我之前写 AHK 时见过 HWND 就是 Handle Window 的意思,一开始我读成「华文xx」…… ↩︎