TCP/IP 协议栈深度

用生活化的比喻,让你深入理解 TCP 的连接状态机、滑动窗口、拥塞控制、队头阻塞——这些是排查网络问题的底层知识

前置知识:第05章 I/O与文件系统(理解 I/O 多路复用和 Socket)


阅读指南(初学者必看)

为什么你需要深入学习 TCP/IP?

你每天都在用 TCP——HTTP 请求、WebSocket 连接、数据库连接都是 TCP。但你可能不理解底层机制:

  • TCP 的三次握手、四次挥手到底在做什么?TIME_WAIT 为什么等 2MSL?
  • 为什么 TCP 丢包后会"降速"?这对游戏有什么影响?
  • 什么是队头阻塞?为什么 WebSocket 也有这个问题?

学完本章,你能回答:

  • TCP 连接的状态机是怎样的?为什么需要这些状态?
  • 滑动窗口和流量控制是怎么工作的?
  • 拥塞控制的四个阶段是什么?为什么对游戏很重要?
  • 队头阻塞是什么?怎么解决?

本文结构

第一部分:TCP/IP 概述
第二部分:TCP 连接状态机(理解连接的生命周期)
第三部分:滑动窗口与流量控制(理解 TCP 的收发机制)
第四部分:拥塞控制(理解 TCP 的"保守"策略)
第五部分:TCP 队头阻塞(理解 TCP 的致命缺陷)

6.1 TCP/IP 概述

生活比喻:TCP/IP 就像邮政系统。

应用层:写信的人(游戏程序)
传输层:邮局分拣(TCP/UDP)
网络层:运输路线规划(IP)
链路层:卡车运输(以太网/WiFi)

写信 → 装信封 → 分拣 → 运输 → 分拣 → 投递 → 收信

协议栈结构

┌─────────────────────────────────────┐
│          应用层 (Application)        │  HTTP, WebSocket, 自定义协议
├─────────────────────────────────────┤
│          传输层 (Transport)          │  TCP, UDP
├─────────────────────────────────────┤
│          网络层 (Network)            │  IP, ICMP, ARP
├─────────────────────────────────────┤
│        数据链路层 (Data Link)        │  Ethernet, WiFi
├─────────────────────────────────────┤
│          物理层 (Physical)           │  网线, 光纤, 无线电
└─────────────────────────────────────┘

6.2 TCP 连接状态机

TCP 状态转换图(完整版):

                    ┌─────────┐
         被动打开   │ CLOSED  │  主动打开
        ┌──────────│         │──────────┐
        │          └─────────┘          │
        ▼                               ▼
  ┌──────────┐    SYN     ┌───────────┐
  │  LISTEN  │◀──────────│ SYN_SENT  │
  └────┬─────┘            └─────┬─────┘
       │ SYN/ACK                 │ SYN/ACK
       ▼                         ▼
  ┌─────────────┐    ACK    ┌───────────┐
  │ SYN_RCVD    │──────────▶│ESTABLISHED│◀── 连接建立!
  └─────────────┘          └─────┬─────┘
                                │ FIN
                                ▼
                          ┌───────────┐
                          │ FIN_WAIT_1│
                          └─────┬─────┘
                                │ ACK
                                ▼
                          ┌───────────┐
                          │ FIN_WAIT_2│
                          └─────┬─────┘
                                │ FIN
                                ▼
                          ┌───────────┐
                          │ TIME_WAIT │ ← 等 2MSL 后关闭
                          └───────────┘

三次握手

class ThreeWayHandshake {
  constructor() {
    this.client = new TCPConnection();
    this.server = new TCPConnection();
  }

  simulate() {
    console.log('=== TCP三次握手模拟 ===\n');
    this.server.passiveOpen(8080);
    console.log('1. 服务器进入LISTEN状态');

    let syn = this.client.activeOpen(12345, 8080);
    console.log('2. 客户端发送SYN');

    let synAck = this.server.processSegment(syn);
    console.log('3. 服务器收到SYN,发送SYN+ACK');

    let ack = this.client.processSegment(synAck);
    console.log('4. 客户端收到SYN+ACK,发送ACK');

    this.server.processSegment(ack);
    console.log('5. 服务器收到ACK,连接建立');

    console.log('\n客户端状态:', this.client.state);
    console.log('服务器状态:', this.server.state);
  }
}

TIME_WAIT 为什么要等 2MSL?

  • MSL = Maximum Segment Lifetime(报文最大生存时间,通常 60 秒)
  • 等待 2MSL 确保最后一个 ACK 能到达对方
  • 如果对方没收到 ACK,会重发 FIN,这边还能响应
  • 游戏服务器大量短连接时,TIME_WAIT 堆积是常见问题

游戏中的 TCP 状态问题

# 查看服务器上的 TCP 状态分布
netstat -ant | awk '{print $6}' | sort | uniq -c | sort -rn

# 常见问题:
# TIME_WAIT 过多 → 短连接太多,改用长连接或设置 SO_REUSEADDR
# CLOSE_WAIT 过多 → 应用没调 close(),代码 bug
# SYN_RECV 过多 → SYN Flood 攻击

6.3 滑动窗口与流量控制

生活类比:滑动窗口就像"流水线上的产品"。发送方和接收方各有一个窗口,窗口大小决定可以连续发送多少数据而不需要等待确认。

发送窗口:
┌──────────────────────────────────────────────┐
│  已确认  │  已发送未确认  │  可发送  │  不可发送  │
│         │  (在窗口内)   │(窗口内)│           │
└──────────────────────────────────────────────┘
           ◀──── 发送窗口大小 ────▶

接收窗口:
- 接收方通过 ACK 告诉发送方自己的窗口大小
- 窗口为 0 → 发送方暂停(零窗口)
- 窗口打开 → 发送方继续

流量控制:接收方通过调整窗口大小控制发送速度
- 接收方处理慢 → 缩小窗口 → 发送方减速
- 接收方处理快 → 扩大窗口 → 发送方加速

→ 这和 Node.js Stream 的背压机制是同一个思想!

6.4 拥塞控制

生活类比:拥塞控制就像"城市交通管制"。路上车太多就限流,路上空就放行。

TCP 拥塞控制四个阶段:

1. 慢启动(Slow Start)
   初始窗口 = 1 MSS(约 1460 字节)
   每收到一个 ACK,窗口翻倍
   1 → 2 → 4 → 8 → 16 → ... 指数增长

2. 拥塞避免(Congestion Avoidance)
   窗口达到慢启动阈值(ssthresh)后
   每个RTT窗口+1,线性增长

3. 快速重传(Fast Retransmit)
   收到 3 个重复 ACK → 立即重传丢失的包
   不等超时,减少延迟

4. 快速恢复(Fast Recovery)
   不回到慢启动,而是减半窗口后继续拥塞避免
class CongestionControl {
  constructor() {
    this.cwnd = 1;
    this.ssthresh = 64;
    this.rtt = 100;
    this.rttVar = 50;
    this.rto = 200;
    this.state = 'slow_start';
  }

  onAck(acked) {
    switch (this.state) {
      case 'slow_start':
        this.cwnd += acked;
        if (this.cwnd >= this.ssthresh) {
          this.state = 'congestion_avoidance';
        }
        break;
      case 'congestion_avoidance':
        this.cwnd += acked / this.cwnd;
        break;
    }
  }

  onLoss() {
    this.ssthresh = Math.max(this.cwnd / 2, 1);
    this.cwnd = 1;
    this.state = 'slow_start';
  }
}

为什么这对游戏很重要?

  • TCP 拥塞控制是"保守的"——丢包就降速
  • 游戏网络经常丢包(WiFi、移动网络)
  • TCP 降速后恢复很慢 → 游戏卡顿
  • 这是游戏用 UDP 的核心原因之一

6.5 TCP 队头阻塞(Head-of-Line Blocking)

问题:TCP 保证有序交付
  包1 ✓ → 包2 ✓ → 包3 ✗ 丢失 → 包4 ✓ → 包5 ✓
                                    ↑
                          包4和5已经到了,但要等包3重传
                          整个 TCP 流被阻塞!

游戏中的影响:
- 多个游戏消息通过同一个 TCP 连接发送
- 一个消息的包丢了,后面所有消息都要等
- 即便后面的消息彼此独立,也得等

WebSocket 也存在这个问题!
- WebSocket 基于 TCP
- 游戏消息通过 WebSocket 发送
- 一个消息丢包 → 所有消息延迟

解决方案:
1. 多个 TCP 连接(不推荐,连接开销大)
2. 使用 UDP + 自定义可靠传输(QUIC 方案)
3. HTTP/3 基于 QUIC,解决了 TCP 队头阻塞

自问自答

Q:为什么 TCP 需要三次握手而不是两次? A:三次握手的核心目的是"双方都确认对方的发送和接收能力正常"。两次握手只能确认一方的收发能力。如果只有两次握手,一个延迟的 SYN 可能导致服务器建立无效连接(历史连接问题)。第三次 ACK 让服务器确认客户端确实在响应当前连接请求。

Q:TIME_WAIT 过多怎么办? A:1)使用长连接代替短连接,减少连接创建/关闭次数;2)设置 SO_REUSEADDR,允许重用 TIME_WAIT 状态的端口;3)调整 tcp_tw_reuse(Linux)允许快速回收 TIME_WAIT 连接;4)使用 tcp_max_tw_buckets 限制 TIME_WAIT 数量。游戏服务器应优先使用长连接。

Q:为什么 TCP 拥塞控制对游戏不利? A:TCP 拥塞控制的策略是"丢包就降速"——这在传统网络(拥塞导致丢包)是合理的,但在游戏网络(无线信号导致丢包)就出问题了。WiFi/4G 的丢包不是因为拥塞,而是信号波动。TCP 误判为拥塞→降速→恢复慢→游戏卡顿。这就是游戏用 UDP 的核心原因。

Q:滑动窗口和拥塞窗口有什么区别? A:滑动窗口是接收方的限制——"我能处理多少数据"(流量控制)。拥塞窗口是发送方的限制——"网络能承受多少数据"(拥塞控制)。实际发送窗口 = min(滑动窗口, 拥塞窗口)。两者是独立的控制机制,共同决定发送速度。


实践任务

  • 任务1:用 Wireshark 抓包分析 TCP 三次握手和四次挥手的全过程,标注每个状态转换
  • 任务2:用 ss -i 查看 TCP 连接的详细参数(窗口大小、RTT、重传次数)
  • 任务3:用 tc netem 模拟网络丢包,观察 TCP 拥塞窗口的降速和恢复过程
  • 任务4:在游戏服务器上执行 netstat -ant | awk '{print $6}' | sort | uniq -c,分析 TCP 状态分布
  • 任务5:用 Node.js 实现一个简单的 TCP 服务器和客户端,观察连接状态变化

与其他章节的关联

本章内容 关联章节 关联点
TCP 状态机 第07章 UDP与可靠传输 QUIC 基于 UDP,不需要 TCP 的状态机
队头阻塞 第08章 HTTP/2与HTTP/3 HTTP/3 基于 QUIC 解决了 TCP 队头阻塞
滑动窗口 第09章 网络安全 TLS 加密不影响 TCP 的流量控制
拥塞控制 第10章 实战篇 网络延迟排查需要理解拥塞控制

上一章:05-操作系统IO与文件系统 | 下一章:07-UDP与可靠传输