Skip to content
Gains Summary
Main Navigation 首页 / Home
C++ 编程 / C++ Programming
系统与高性能 / Systems & Performance
Web 开发 / Web Development
人工智能 / Artificial Intelligence
工业软件 / Industrial Software
其他内容 / Other Topics
C++ 编程 / C++系统与性能 / SystemsWeb 开发 / Web人工智能 / AI工业软件 / Industrial

外观

Sidebar Navigation

← Web 开发 / Web Development

Web 平台与 API / Web Platform & APIs

1. 浏览器存储完全指南 / A Complete Guide to Browser Storage

2. 网络基础——10 万长连接是如何拖垮服务器的 / Network Fundamentals and the Cost of One Hundred Thousand Connections

3. API 设计——为什么你的接口总是被吐槽 / API Design for Clear and Evolvable Interfaces

本页目录

网络基础——10 万长连接是如何拖垮服务器的 / Network Fundamentals and the Cost of One Hundred Thousand Connections ​

📅 创建时间:2026-05-08 🏷️ 标签:#TCP #HTTP #三次握手 #四次挥手 #time_wait #滑动窗口 📚 前置知识:[[00-backend-overview]] [[05-middleware]](网关的连接问题) 📚 相关知识:[[08-concurrency]](线程池与连接数)


场景:你的服务器有 65535 个连接,但它已经撑不住了 ​

┌─────────────────────────────────────────────────────────────┐
│                                                             │
│  运维监控:                                              │
│  CPU: 5%(几乎空闲)                                   │
│  内存: 30%(正常)                                     │
│  网络: 100Mbps(满了)                                 │
│  连接数: 40000(很多)                                 │
│                                                             │
│  但用户投诉:页面加载慢,请求超时。                      │
│                                                             │
│  为什么 CPU 空闲,但系统却很慢?                        │
│                                                             │
│  问题出在 TCP 连接的底层机制上。                       │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

第1节:TCP 三次握手——为什么连接这么慢 ​

场景:两个人打电话 ​

小李(客户端):喂,我想和你建立通话,可以吗?
小明(服务端):可以!请问你听得到吗?
小李(客户端):听得到!

→ 通话建立!
1
2
3
4
5

TCP 三次握手图解 ​

┌─────────────────────────────────────────────────────────────┐
│                 TCP 三次握手                               │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  客户端                                                服务端 │
│    │                                                    │   │
│    │ ─────── SYN=1, seq=x ──────────────────────▶ │   │
│    │      第一次握手:客户端想建立连接,发送 SYN        │   │
│    │                                                    │   │
│    │ ◀───────────────── SYN=1, seq=y, ACK=x+1 ─── │   │
│    │      第二次握手:服务端同意,返回 SYN + ACK       │   │
│    │                                                    │   │
│    │ ─────── ACK=y+1 ─────────────────────────────▶ │   │
│    │      第三次握手:客户端确认收到                    │   │
│    │                                                    │   │
│    │                    建立连接!                      │   │
│    │                                                    │   │
│                                                             │
│  为什么不两次握手?                                       │
│  客户端发送 SYN 后崩溃了 → 服务端收到 SYN → 等待      │
│  → 服务端资源被浪费(半连接队列)                      │
│                                                             │
│  为什么不四次握手?                                       │
│  服务端的 SYN + ACK 可以一起发(三次够了)              │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

HTTP 请求的完整过程 ​

┌─────────────────────────────────────────────────────────────┐
│                 HTTP 请求完整过程                          │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  DNS 解析(第一次通信)                                  │
│  浏览器 ──▶ DNS 服务器 ──▶ 获得 IP                     │
│                                                             │
│  TCP 连接(三次握手)                                    │
│  浏览器 ──▶ 服务器(SYN)─── SYN+ACK ──▶ 浏览器     │
│  浏览器 ──▶ 服务器(ACK)                                 │
│                                                             │
│  TLS 握手(HTTPS)                                       │
│  浏览器 ──▶ 服务器(ClientHello)                        │
│  浏览器 ◀── 服务器(ServerHello + 证书)               │
│  浏览器 ──▶ 服务器(密钥交换)                          │
│  浏览器 ◀── 服务器(握手完成)                          │
│                                                             │
│  HTTP 请求/响应                                          │
│  浏览器 ──▶ 服务器(GET /api/order HTTP/1.1)         │
│  浏览器 ◀── 服务器(200 OK + JSON)                     │
│                                                             │
│  TCP 断开(四次挥手)                                    │
│  浏览器 ──▶ 服务器(FIN)─── ACK ──▶ 浏览器         │
│  浏览器 ◀── 服务器(FIN)                               │
│  浏览器 ──▶ 服务器(ACK)                                │
│                                                             │
│  总耗时:                                                │
│  HTTP/1.1 + TLS 1.3:约 3 个 RTT(往返时延)          │
│  假设 RTT = 50ms:建立连接需要 150ms                   │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

第2节:四次挥手——为什么连接总是关不掉 ​

场景:两个人通话结束 ​

小李:我说完了,再见!
小明:好的收到!
...(等待小明确认说完)...
小明:我也说完了,再见!
小李:好的!
1
2
3
4
5

TCP 四次挥手图解 ​

┌─────────────────────────────────────────────────────────────┐
│                 TCP 四次挥手                               │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  客户端                                                服务端 │
│    │                                                    │   │
│    │ ─────── FIN=1, seq=u ────────────────────────▶ │   │
│    │      第一次挥手:客户端发送 FIN,请求断开           │   │
│    │                                                    │   │
│    │ ◀────────────────── ACK=u+1 ─────────────────── │   │
│    │      第二次挥手:服务端确认(但可能还有数据没发完)│   │
│    │                                                    │   │
│    │                        ... 处理剩余数据 ...        │   │
│    │                                                    │   │
│    │ ◀────────────────── FIN=1, seq=w ─────────────── │   │
│    │      第三次挥手:服务端发完数据,发送 FIN         │   │
│    │                                                    │   │
│    │ ─────── ACK=w+1 ──────────────────────────────▶ │   │
│    │      第四次挥手:客户端确认                       │   │
│    │                                                    │   │
│    │                   ┌─────────────────────┐         │   │
│    │                   │ TIME_WAIT (客户端)  │         │   │
│    │                   │ 等待 2MSL(分)    │         │   │
│    │                   │ 确保服务端收到 ACK   │         │   │
│    │                   └─────────────────────┘         │   │
│    │                                                    │   │
│                                                             │
│  为什么不三次挥手?                                       │
│  服务端发完数据才能发 FIN,所以 ACK 和 FIN 分开发送     │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

TIME_WAIT:为什么端口用着用着就没了 ​

┌─────────────────────────────────────────────────────────────┐
│                 TIME_WAIT 问题                             │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  TIME_WAIT = 2MSL(Maximum Segment Lifetime)             │
│  MSL = 60 秒(Linux 默认)                               │
│  TIME_WAIT = 2 分钟                                       │
│                                                             │
│  作用:                                                   │
│  1. 确保最后的 ACK 能到达服务端                          │
│  2. 让旧连接的重复数据包在网络中消散                     │
│                                                             │
│  问题:                                                   │
│  连接关闭后,客户端端口处于 TIME_WAIT 状态 2 分钟       │
│  每个连接占用一个端口                                     │
│  系统端口数:65535                                        │
│  高并发短连接:2分钟内用完所有端口                       │
│                                                             │
│  ┌─────────────────────────────────────────────────────┐ │
│  │  查看:netstat -an | grep TIME_WAIT               │ │
│  │                                                     │ │
│  │  解决:                                            │ │
│  │  1. 客户端使用连接池(复用连接)                    │ │
│  │  2. 服务端设置 SO_LINGER = 0(强制关闭连接)      │ │
│  │  3. 调整 MSL:sysctl -w net.ipv4.tcp_fin_timeout=30 │ │
│  │  4. 开启 tcp_tw_reuse(复用 TIME_WAIT 端口)      │ │
│  └─────────────────────────────────────────────────────┘ │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

CLOSE_WAIT:为什么连接关不掉 ​

┌─────────────────────────────────────────────────────────────┐
│                 CLOSE_WAIT 问题                            │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  CLOSE_WAIT = 服务端收到了 FIN,但没调用 close()        │
│                                                             │
│  常见原因:                                               │
│  代码 bug:读取数据后没调用 socket.close()               │
│                                                             │
│  ┌─────────────────────────────────────────────────────┐ │
│  │  服务端代码 bug:                                  │ │
│  │                                                     │ │
│  │  while True:                                      │ │
│  │      data = socket.recv(1024)                     │ │
│  │      if not data:                                 │ │
│  │          break  # ← 这里没有调用 close()!       │ │
│  │      process(data)                                │ │
│  └─────────────────────────────────────────────────────┘ │
│                                                             │
│  后果:CLOSE_WAIT 越来越多 → 端口耗尽                    │
│                                                             │
│  排查命令:                                              │
│  netstat -an | grep CLOSE_WAIT | wc -l                   │
│  netstat -an | grep CLOSE_WAIT                           │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

第3节:HTTP 各版本对比 ​

┌─────────────────────────────────────────────────────────────┐
│              HTTP/1.0 vs 1.1 vs 2.0 vs 3.0               │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  HTTP/1.0(1996):                                     │
│  • 每次请求建立新连接,用完关闭                        │
│  • 无 Keep-Alive,每个资源都要一次握手                   │
│  • 连接数限制:浏览器最多 4-6 个并发连接               │
│                                                             │
│  HTTP/1.1(1999,推荐):                                │
│  • 默认开启 Keep-Alive(连接复用)                      │
│  • 管道化(Pipelining):不等待响应可发下一个请求       │
│  • 问题:队头阻塞(HOL Blocking)                       │
│    → 请求 1 响应慢,请求 2/3 也被阻塞                   │
│                                                             │
│  HTTP/2.0(2015):                                     │
│  • 多路复用:一个 TCP 连接并行传输多个请求/响应          │
│    → 解决 HTTP/1.1 的队头阻塞                          │
│  • Header 压缩(HPACK)                                 │
│  • Server Push:服务端主动推送资源                      │
│  • 问题:TCP 层面的队头阻塞(一个包丢了,全部等)       │
│                                                             │
│  HTTP/3.0(QUIC,2022):                               │
│  • 基于 UDP,不存在 TCP 队头阻塞                        │
│  • 0-RTT(快速恢复,上下文复用)                       │
│  • 连接迁移(切换网络 IP 不掉线)                       │
│  • 目前在逐步采用中                                      │
│                                                             │
│  实际影响:                                               │
│  • 静态资源多 → HTTP/2 多路复用效果明显                │
│  • 高丢包网络 → HTTP/3 优势明显                        │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

第4节:Keep-Alive vs 长连接 ​

场景:10 万 QPS 要建立 10 万次 TCP 连接吗? ​

HTTP/1.0,无 Keep-Alive:
10 万请求 = 10 万次 TCP 连接 = 10 万次握手
→ 握手耗时 150ms × 10 万 = 15000 秒 = 4 小时!(不可能)

HTTP/1.1,Keep-Alive:
→ 建立一个 TCP 连接,复用处理 10 万请求
→ 10 万次 HTTP 请求,复用少量 TCP 连接
→ 性能提升 10-100 倍
1
2
3
4
5
6
7
8

连接池:最优实践 ​

python
# Python requests 的连接池(默认开启)
import requests

# 单个 Session 对象维护一个连接池
session = requests.Session()
for i in range(10000):
    session.get(f"http://api.example.com/user/{i}")
# 实际只建立了几个 TCP 连接,复用处理 1 万请求

# HTTP/1.1 Keep-Alive 超时(服务端默认 15 秒)
# 超过这个时间没活动,连接关闭
1
2
3
4
5
6
7
8
9
10
11

第5节:滑动窗口——TCP 怎么控制流量 ​

┌─────────────────────────────────────────────────────────────┐
│                 TCP 滑动窗口                               │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  问题:发送方发得太快,接收方处理不过来怎么办?            │
│                                                             │
│  滑动窗口 = 接收方告诉发送方:"你最多发这么多"           │
│                                                             │
│  ┌─────────────────────────────────────────────────────┐ │
│  │  接收方窗口大小:4 个包                           │ │
│  │                                                     │ │
│  │  已确认   已发未确认    可发送      不能发        │ │
│  │  [1,2]    [3,4]       [5,6,7]     [8,9,10]    │ │
│  │                    ↑                              │ │
│  │                发送指针                           │ │
│  │                                                     │ │
│  │  ACK=3 到达 → 滑动 → [1,2]已确认,可发 [8,9]  │ │
│  │                                                     │ │
│  │  如果接收方处理慢(窗口=0):                      │ │
│  │  发送方停止发送 → 零窗口探测(探测窗口何时打开)  │ │
│  └─────────────────────────────────────────────────────┘ │
│                                                             │
│  拥塞控制(防止网络本身过载):                          │
│  • 慢启动:连接建立时,从小到大试探窗口大小              │
│  • 拥塞避免:达到阈值后,缓慢增长                      │
│  • 快重传:收到 3 个重复 ACK,立即重传丢失的包         │
│  • 快恢复:重传后,窗口减半而不是回到起点               │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

升华:网络问题排查思路 ​

┌─────────────────────────────────────────────────────────────┐
│              网络问题排查三板斧                            │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  第一板斧:netstat / ss 查看连接状态                       │
│  ss -s  # 统计各类连接数量                               │
│  ss -tan state established | wc -l  # 并发连接数         │
│                                                             │
│  第二板斧:TIME_WAIT 太多?                               │
│  → 开启 tcp_tw_reuse                                      │
│  → 减少 MSL 时间                                          │
│  → 服务端主动关闭连接                                     │
│                                                             │
│  第三板斧:CLOSE_WAIT 太多?                              │
│  → 一定是代码 bug:调用了 close() 的地方没调用           │
│  → 搜索代码中的 close() 调用                              │
│                                                             │
│  一句话总结:                                              │
│  TIME_WAIT = 协议行为(调参数),CLOSE_WAIT = 代码 bug。  │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

"AI 可查 vs 必须理解"清单 ​

AI 可查:
✅ netstat / ss / tcpdump 的具体命令参数
✅ /proc/sys/net/ 下各种 TCP 参数
✅ HTTP/2 HPACK 压缩算法细节

必须理解:
🔴 三次握手 vs 四次挥手(为什么一个是三,一个是四)
🔴 TIME_WAIT 和 CLOSE_WAIT 的本质区别
🔴 TIME_WAIT 太多的后果和解决方案
🔴 HTTP/1.1 vs 2.0 多路复用的区别
🔴 队头阻塞在 HTTP/1.1 和 HTTP/2 的不同表现
1
2
3
4
5
6
7
8
9
10
11

学习状态:🟡 开始学习

最后更新于:

Pager
上一篇1. 浏览器存储完全指南 / A Complete Guide to Browser Storage
下一篇3. API 设计——为什么你的接口总是被吐槽 / API Design for Clear and Evolvable Interfaces

持续记录,持续成长

Copyright © Tidenflow