跳转至

运输层

约 4327 个字 14 行代码 4 张图片 预计阅读时间 22 分钟

运输层的作用是实现进程之间的可靠传输

总的来说,这一层的协议主要有两个,TCP与UDP.

  • UDP (用户数据报协议): 无连接, 尽最大努力交付, 面向报文. 适用于对实时性要求高、对丢包不敏感的应用(如视频会议).

  • TCP (传输控制协议): 面向连接, 提供可靠交付, 面向字节流. 适用于要求数据准确无误的应用(如文件传输、邮件).

不同操作系统上,进程标识符的格式是不一样的.为了解决这个问题,运输层使用了端口号,来统一标识进程.

端口号

端口号是一个16位的整数, 范围是 0 ~ 65535. 它用于在主机中唯一标识一个应用进程.

端口号只具有本地意义, 即端口号只是为了标志本计算机应用层中的各进程. 在因特网中不同计算机的相同端口号是没有联系的.

端口号的分类

端口号分为以下三类:

  1. 熟知端口号 (Well-known Ports):

    • 范围: 0 ~ 1023
    • 由 IANA (互联网号码分配机构) 指派给 TCP/IP 最重要的一些应用程序.
    • 例如: FTP (21), SSH (22), Telnet (23), SMTP (25), DNS (53), HTTP (80), HTTPS (443).
  2. 登记端口号 (Registered Ports):

    • 范围: 1024 ~ 49151
    • 为没有熟知端口号的应用程序使用. 使用这类端口号必须在 IANA 登记, 以防止重复.
    • 例如: MySQL (3306), Redis (6379), Tomcat (8080).
  3. 客户端口号 (Ephemeral Ports / Dynamic Ports):

    • 范围: 49152 ~ 65535
    • 仅在客户进程运行时才动态选择, 因此又称为短暂端口号.
    • 当服务器进程收到客户进程的报文时, 就知道了客户进程所使用的端口号, 因而可以把数据发送给客户进程. 通信结束后, 该端口号可供其他客户进程以后使用.
访问网站的全过程
sequenceDiagram
    participant User as 用户主机
    participant DNS as 本地DNS服务器
    participant Web as 网站服务器

    Note over User: 1. 用户在浏览器输入 www.example.com

    %% DNS 解析过程 (UDP)
    rect rgb(240, 248, 255)
    Note right of User: DNS解析阶段 (通常使用UDP 53端口)
    User->>User: 检查本地hosts和缓存
    User->>DNS: 发送DNS查询请求 (UDP)
    Note right of User: 源端口:随机, 目的端口:53
    DNS->>DNS: 递归查询 (Root -> TLD -> Authoritative)
    DNS-->>User: 返回IP地址 (例如 1.2.3.4)
    end

    %% HTTP 请求过程 (TCP)
    rect rgb(255, 250, 240)
    Note right of User: HTTP请求阶段 (使用TCP 80/443端口)

    %% TCP 三次握手
    User->>Web: SYN (建立连接请求)
    Note right of User: 源端口:随机, 目的端口:80
    Web-->>User: SYN + ACK (确认并同意)
    User->>Web: ACK (确认)
    Note over User, Web: TCP连接建立完成 (三次握手)

    %% 数据传输
    User->>Web: HTTP GET 请求
    Web-->>User: HTTP 响应 (网页内容)

    %% TCP 四次挥手 (简化)
    User->>Web: FIN (断开连接请求)
    Web-->>User: ACK
    Web-->>User: FIN
    User->>Web: ACK
    Note over User, Web: TCP连接释放 (四次挥手)
    end

TCP

TCP的首部格式


TCP首部格式
  1. 源端口 (Source Port): 16位。发送方的端口号。

  2. 目的端口 (Destination Port): 16位。接收方的端口号。

  3. 序号 (Sequence Number): 32位。本报文段所发送数据的第一个字节的序号。在建立连接时,SYN报文段会携带一个初始序号。

  4. 确认号 (Acknowledgment Number): 32位。期望收到对方下一个报文段的第一个字节的序号。若确认号为N,则表示N-1及以前的数据都已正确收到。

  5. 数据偏移 (Data Offset): 4位。表示TCP报文段的数据部分的起始位置距离TCP报文段的起始位置有多远,即TCP首部长度。单位是4字节(32位字)。因此,TCP首部最大长度为 (2^4 - 1) * 4 = 15 * 4 = 60字节。

  6. 保留 (Reserved): 6位。保留为今后使用,目前必须置为0。

  7. 标志位 (Flags): 6位。

    • URG (Urgent): 紧急指针有效。
    • ACK (Acknowledgment): 确认号有效。
    • PSH (Push): 立即发送数据。
    • RST (Reset): 重置连接。
    • SYN (Synchronize): 同步序号,用于建立连接。
    • FIN (Finish): 终止发送方数据,用于释放连接。
  8. 窗口大小 (Window Size): 16位。接收方告知发送方自己还能接收多少字节的数据,用于流量控制。

  9. 校验和 (Checksum): 16位。对整个TCP报文段(包括首部和数据)进行校验,由发送方计算,接收方验证。

    计算方法:

    1. 将 TCP 首部中检验和字段的值置为 0.

    2. 伪首部TCP 首部 以及 数据载荷 这三部分划分成若干个 2 字节的字.

    3. 若这三部分的总长度不是偶数个字节, 则在最后添加 1 个 "全 0" 字节.

    4. 对划分出的全部 2 字节的字进行 反码算数运算求和, 并将求和结果 取反码.

    5. 将结果写入 TCP 首部中的检验和字段.

    校验和计算举例 (简化)

    假设我们有 3 个 4 位二进制数需要通过反码算术运算求和 (实际 TCP 使用 16 位, 原理相同):

    数据: 0111 (7), 1011 (11), 1100 (12)

    步骤 1: 前两个数相加

       0111
     + 1011
     ------
      10010  (产生进位 1)
    
    将进位加到末尾 (回卷, End-around carry):
       0010
     +    1
     ------
       0011
    

    步骤 2: 加上第三个数

       0011 (上一步的结果)
     + 1100
     ------
       1111 (没有进位)
    

    步骤 3: 取反码 (Checksum)

    ~  1111
    =  0000 (这就是校验和)
    

    什么是伪首部 (Pseudo Header)

    伪首部 是在计算校验和时,临时加在 TCP 首部前面的 12 个字节。它 会被发送到网络上,只是为了计算用的。

    目的: 验证数据包是否真的到达了正确的 IP 地址 (防止 IP 层路由错误)。

    结构 (12 字节):

    字段 长度 说明
    源 IP 地址 4 字节 发送方的 IP
    目的 IP 地址 4 字节 接收方的 IP
    保留位 1 字节 置 0
    协议号 1 字节 TCP 的协议号是 6
    TCP 长度 2 字节 TCP 首部 + 数据的长度

    校验和包含了伪首部,意味着 TCP 不仅校验了数据本身,还校验了 IP 地址,增加了可靠性。

  10. 紧急指针 (Urgent Pointer): 16位。当URG标志位为1时有效,指出紧急数据在当前报文段中的偏移量。

  11. 选项 (Options): 长度可变,最长40字节。例如,最大报文段长度(MSS)、窗口扩大因子、时间戳等。

  12. 填充 (Padding): 长度可变。用于确保TCP首部长度是4字节的整数倍。

TCP的运输管理

TCP的连接与传输分为三个阶段:

  1. 三次握手建立连接

  2. 传输数据

  3. 四次挥手释放连接

三次握手建立连接 (Three-Way Handshake)

TCP 建立连接的过程叫做握手,握手需要在客户和服务器之间交换三个 TCP 报文段。

图解
sequenceDiagram
    participant C as 客户端 (Client)
    participant S as 服务器 (Server)

    Note left of C: CLOSED (关闭)
    Note right of S: LISTEN (监听)

    Note over C,S: 第一次握手: 建立连接请求
    C->>S: SYN=1, seq=x
    Note left of C: SYN-SENT (同步已发送)
    Note right of S: 收到 SYN

    Note over C,S: 第二次握手: 确认并同意连接
    S->>C: SYN=1, ACK=1, seq=y, ack=x+1
    Note right of S: SYN-RCVD (同步收到)
    Note left of C: 收到 SYN+ACK

    Note over C,S: 第三次握手: 确认确认
    C->>S: ACK=1, seq=x+1, ack=y+1
    Note left of C: ESTABLISHED (已建立连接)
    Note right of S: ESTABLISHED (已建立连接)
  1. 第一次握手 (Client -> Server):

    • 动作: 客户端发送 连接请求报文段.

    • 标志位: SYN=1 (表示这是一个连接请求), ACK=0.

    • 序号: seq=x (随机生成一个初始序号 ISN).

      这个初始序号会作为服务器端记录收到字节流的初始序号

    • 状态变迁: 客户端进入 SYN-SENT 状态.

    • 注意: SYN 报文段不能携带数据,但要消耗掉一个序号.

  2. 第二次握手 (Server -> Client):

    • 动作: 服务器收到请求后,如果同意连接,则发送 确认报文段.
    • 标志位: SYN=1 (同步), ACK=1 (确认).

    • 序号: seq=y (服务器也随机生成自己的初始序号).

    • 确认号: ack=x+1 (期望收到客户端的下一个字节序号).

    • 状态变迁: 服务器进入 SYN-RCVD 状态.

    • 注意: 这个报文段也不能携带数据,也要消耗掉一个序号.

  3. 第三次握手 (Client -> Server):

    • 动作: 客户端收到确认后,还要向服务器给出确认.
    • 标志位: ACK=1 (表示确认有效), SYN=0.

    • 序号: seq=x+1.

    • 确认号: ack=y+1.

    • 状态变迁: 客户端进入 ESTABLISHED 状态. 服务器收到后也进入 ESTABLISHED 状态.

    • 注意: ACK 报文段可以携带数据,如果不携带数据则不消耗序号.

为什么要三次握手?(Why 3-way?)

主要为了 防止已失效的连接请求报文段突然又传送到了服务器 (Preventing Stale Duplicates),从而产生错误。

  • 场景: 客户端发的第一个连接请求 SYN 在网络中滞留了,客户端超时重传了新的 SYN 并建立了连接。

  • 问题: 等连接释放后,那个滞留的旧 SYN 终于到达服务器。

  • 后果 (如果是两次握手): 服务器收到旧 SYN 就误以为客户端又要建连接,于是发出确认并建立连接,一直等待客户端发数据,导致服务器资源白白浪费。
  • 三次握手的作用: 服务器收到旧 SYN 并回复 SYN+ACK 后,客户端发现这个确认号不对 (不是自己刚才发的),就不会理睬 (不发第三次 ACK)。服务器收不到确认,就知道连接没建立。

四次挥手释放连接 (Four-Way Handshake for Termination)

数据传输结束后,通信的双方都可以释放连接。现在客户端和服务器都处于 ESTABLISHED 状态。

图解
sequenceDiagram
    participant C as 客户端 (Client)
    participant S as 服务器 (Server)

    Note over C,S: 第一次挥手: 客户端请求断开
    C->>S: FIN=1, seq=u
    Note left of C: FIN-WAIT-1 (终止等待1)
    Note right of S: 收到 FIN

    Note over C,S: 第二次挥手: 服务器确认断开
    S->>C: ACK=1, seq=v, ack=u+1
    Note right of S: CLOSE-WAIT (关闭等待)
    Note left of C: FIN-WAIT-2 (终止等待2)
    Note right of S: 此时服务器仍可发送剩余数据

    Note over C,S: 第三次挥手: 服务器请求断开
    S->>C: FIN=1, ACK=1, seq=w, ack=u+1
    Note right of S: LAST-ACK (最后确认)
    Note left of C: 收到 FIN

    Note over C,S: 第四次挥手: 客户端确认断开
    C->>S: ACK=1, seq=u+1, ack=w+1
    Note left of C: TIME-WAIT (时间等待)
    Note right of S: CLOSED (关闭)

    Note left of C: 等待 2MSL 后
    Note left of C: CLOSED (关闭)
  1. 第一次挥手 (Client -> Server):

    • 动作: 客户端发送 连接释放报文段,停止发送数据。
    • 标志位: FIN=1.
    • 序号: seq=u (等于前面已传送数据的最后一个字节序号 + 1).
    • 状态: 客户端进入 FIN-WAIT-1 状态.
  2. 第二次挥手 (Server -> Client):

    • 动作: 服务器收到 FIN,发出确认。
    • 标志位: ACK=1.
    • 序号: seq=v.
    • 确认号: ack=u+1.
    • 状态: 服务器进入 CLOSE-WAIT 状态. 客户端收到后进入 FIN-WAIT-2 状态.
    • 注意: 此时连接处于 半关闭 (Half-Close) 状态。客户端不再发数据,但服务器如果还有数据要发,客户端仍需接收。
  3. 第三次挥手 (Server -> Client):

    • 动作: 服务器数据发完后,发送连接释放报文段。
    • 标志位: FIN=1, ACK=1.
    • 序号: seq=w.
    • 确认号: ack=u+1 (重复上次的确认).
    • 状态: 服务器进入 LAST-ACK 状态.
  4. 第四次挥手 (Client -> Server):

    • 动作: 客户端收到 FIN,发出确认。
    • 标志位: ACK=1.
    • 序号: seq=u+1.
    • 确认号: ack=w+1.
    • 状态: 客户端进入 TIME-WAIT 状态,服务器收到后进入 CLOSED 状态.
为什么 Client 主要等待 2MSL? (TIME-WAIT)
  1. 保证最后一个 ACK 报文段能够到达服务器: 如果第四次挥手丢失, 服务器会超时重传 FIN. 客户端必须保留这段时间来处理可能的重传.

  2. 防止"已失效的连接请求报文段"出现在本连接中: 等待 2MSL (Maximum Segment Lifetime), 可以使本连接持续时间内所产生的所有报文段都从网络中消失.


TCP 流量控制

TCP 采用 滑动窗口 (Sliding Window) 机制来实现流量控制,目的是防止发送方发送数据过快,接收方来不及处理。在TCP首部里就有窗口字段,

接收窗口 (rwnd)

  • 机制: 在 TCP 报文段首部的 窗口大小 字段中,接收方告诉发送方自己当前的接收缓存还可以接收多少字节数据。

  • 作用: 发送方维护一个 发送窗口,其大小不能超过接收方告知的 接收窗口 (rwnd)

  • 动态调整: 随着接收方缓存被应用程序读取或填满,rwnd 会动态变化。

接受方在发送数据时,会带上rwnd,这个值的含义要结合ack的序号一起来看.它代表了接受方在收到ack-1的那一刻,还能接收多少字节的数据.

发送窗口 (Send Window)

发送窗口实际上就是\(min\{rwnd, cwnd\}\),其中\(cwnd\)是拥塞窗口.

发送方的发送窗口由 前沿 (Front Edge)后沿 (Rear Edge) 定义。发送窗口内的数据是 允许发送 的 (包括已发送但未收到确认的,和允许发送但尚未发送的)。

  • 后沿 (Rear Edge / Left Edge):

    • 含义: 表示已发送但尚未收到确认的最小序号。
    • 移动: 只能向 前 (右) 移动。
    • 触发: 当收到接收方发来的 新的确认 (New ACK) 时,后沿向前移动。
    • 不动: 如果没有收到新的确认,后沿不动。
  • 前沿 (Front Edge / Right Edge):

    • 含义: 表示允许发送的最大序号 + 1。
    • 计算: 前沿 = 后沿 + 接收窗口(rwnd)
    • 移动: 通常向 前 (右) 移动。
    • 触发:
      1. 收到新的 ACK (后沿前移),且 rwnd 不变或变大。
      2. 后沿不动,但接收方通知的 rwnd 变大。
    • 不动:
      1. 没收到新 ACK,且 rwnd 不变。
      2. 收到新 ACK (后沿前移),但 rwnd 变小了相同幅度 (这种情况不常见)。
    • 向后 (左) 移动 (不建议):
      • 这被称为 窗口收缩 (Window Retraction)。即接收方突然缩小了 rwnd,导致前沿不得不左移 (甚至跑到后沿左边)。TCP 标准强烈不建议这样做。

死锁场景 (Deadlock)

  1. 接收方缓存满了,发送 rwnd = 0 (零窗口通知) 给发送方。

  2. 发送方收到后,停止发送数据,等待接收方通知新的窗口大小。

  3. 接收方应用程序读取了数据,缓存有了空间。接收方发送一个新的报文段通知 rwnd = 400

  4. 死锁: 如果这个通知报文段 在网络中丢失了

    • 发送方还在苦苦等待接收方的非零窗口通知。

    • 接收方以为发送方收到了通知,正在等待发送方发数据。

    • 双方互相等待,产生死锁。

为了解决上述死锁,TCP 为每个连接设有一个 坚持定时器

  • 触发: 当发送方收到一个 rwnd = 0 的报文段时,就启动该定时器。

  • 动作: 定时器到期后,发送方会发送一个 1字节 的探测报文段 (零窗口探测报文, Zero Window Probe, ZWP),询问接收方现在的窗口大小。

    • 如果接收方回复仍是 rwnd = 0,则重置定时器,继续等待。

    • 如果接收方回复 rwnd > 0,则死锁解除,恢复数据发送。

TCP 拥塞控制

拥塞控制流量控制 的区别:

  • 流量控制: 是点对点的通信量控制, 主要解决接收方来不及接收的问题

  • 拥塞控制: 是全局性的过程, 涉及到所有的主机、路由器, 主要解决网络中链路过载的问题.

TCP 拥塞控制基于 窗口 机制, 发送方维护一个 拥塞窗口 (cwnd, congestion window).

发送方实际能发送的数据量 = min(rwnd, cwnd). (取接收窗口和拥塞窗口的最小值).

TCP 拥塞控制主要有四种算法:

什么是 RTT (往返时间) 与 传输轮次?
  • RTT (Round-Trip Time): 从发送方发送数据开始,到发送方收到接收方确认 (ACK) 为止所经历的时间。
  • 传输轮次 (Transmission Round): 拥塞控制中常把 "往返时间 RTT" 称为一个 "传输轮次"。
    • 也就是把 当前拥塞窗口 cwnd 内所允许发送的全部报文段,都发送出去,并且 收到了对已发送的最后一个字节的确认,就算经过了一个传输轮次。

慢开始 (Slow Start)

这里,我们说cwnd的1,指的是1个MSS大小.比如,TCP报文段的最大长度为1KB,那么cwnd=1时,表示每次最多发送1KB的数据.

  • 原理: 一开始不发送大量数据, 而是先探测一下网络的拥塞程度, 由小到大逐渐增大拥塞窗口.

  • 增长规则: 每收到一个 ACK, cwnd 加 1 (意味着每个RTT轮次, cwnd 加倍, 呈指数增长).

  • 门限 (ssthresh): 当 cwnd 达到 慢开始门限 (ssthresh) 时, 改用拥塞避免算法.

拥塞避免 (Congestion Avoidance)

  • 原理: 让拥塞窗口 cwnd 缓慢增大.

  • 增长规则: 每经过一个往返时间 RTT, cwnd 就加 1 (呈 线性增长), 而不是加倍.

  • 拥塞发生时: 只要出现超时重传 (认为网络拥塞了):

    1. ssthresh 设为当前 cwnd 的一半.
    2. cwnd 重置为 1.
    3. 重新执行 慢开始.

慢开始与拥塞避免

快重传 (Fast Retransmit)

  • 机制: 接收方每收到一个失序的报文段, 就立即发出重复确认 (Duplicate ACK).
  • 触发: 发送方只要连续收到 3个重复确认 (3-Duplicate ACKs), 就应当立即重传对方尚未收到的报文段, 而不必等待超时重传定时器.
  • 作用: 大大提高了信道利用率.

快恢复 (Fast Recovery)

  • 触发: 当发送方连续收到 3 个重复确认时 (说明是个别报文丢失, 网络可能没拥塞).
  • 动作:
    1. ssthresh 设为当前 cwnd 的一半.
    2. 关键点: cwnd 设为新的 ssthresh 值 (而不是 1).
    3. 直接执行 拥塞避免 算法 (线性增长).

快重传与快恢复

TCP 超时重传时间的选择

TCP 采用一种自适应算法,根据测量的往返时间 RTT 动态地计算重传时间 RTO (Retransmission Time-Out)。


RTT与RTO

加权平均往返时间 (SRTT, Smoothed RTT)

第一个SRTT的值来自于第一个RTT,即SRTT = RTT

RTT 的值会随网络状况波动。TCP 不直接使用某一次的 SampleRTT,而是计算 加权平均往返时间 SRTT

  • 公式: \(SRTT = (1 - \alpha) \times Old\_SRTT + \alpha \times New\_RTT\)
  • 参数: 标准推荐值 \(\alpha = 0.125\) (即 1/8)。
  • 意义: 这样得出的 SRTT 更加平滑,不会因为一次偶然的 RTT 波动而剧烈变化。

RTT 的偏差的加权平均值 (RTTD, Deviation RTT)

第一个RTTD的值来自于第一个RTT的一半,即RTTD = RTT/2

为了如实反映 RTT 的波动程度,TCP 还需要计算 RTT 的偏差 (即 RTT 波动了多少)。

  • 公式: \(RTT_D = (1 - \beta) \times Old\_RTT_D + \beta \times |SRTT - New\_RTT|\)

  • 参数: 标准推荐值 \(\beta = 0.25\) (即 1/4)。

超时重传时间 (RTO) 的计算

RTO 应略大于 SRTT。根据 RFC 6298 建议,RTO 的计算公式为:

\[ RTO = SRTT + 4 \times RTT_D \]

Karn 算法 (Karn's Algorithm)

在重传报文段时,如果发送方收到了确认,就无法知道这个确认是对 刚才重传的报文 的确认,还是对 原先发送的报文 的确认。这会导致 RTT 测量不准。

Karn 算法规则:

  1. 只要报文段重传了,就不采用其计算 SRTT (即不采样这次的 RTT)。

  2. 报文段每重传一次,就把 RTO 增大一些 (典型的做法是取新的 RTO = 旧的 RTO \(\times\) 2, 这称为 指数退避)。

评论