第03章 一个请求穿过代理的一生:连接生命周期全景
学习目标
- 把前两章的静态模型变成动态影片:跟随一个请求走完代理的每一个阶段
- 搞清一个反复要命的问题:DNS 到底由谁解析——客户端还是代理?这决定了"DNS 泄漏"
- 用
curl -w把请求拆成可测量的时间分段,知道每一毫秒花在哪 - 建立"阶段 → 故障 → 状态码"的对应表,为 第32章排错 打底
前置知识
- 第01章 统一模型、第02章 网络层级
- 第04章 HTTP 代理协议(CONNECT、连接池)
原理
全景时间线
一个客户端经正向 HTTP 代理访问 https://example.com/,完整生命周期是这样的(注意它其实是两条连接的接力):
客户端侧 代理 源站侧
──────── ────── ──────
│
│ ① 解析代理地址(读 HTTP_PROXY / PAC)
│ ② DNS 解析"代理"的主机名(不是源站!)
│ ③ TCP 三次握手 → 连上代理 :3128
├──────────────────────────────▶│
│ ④ 发送 CONNECT example.com:443 │
├──────────────────────────────▶│
│ │ ⑤ 代理认证(401? 407?)
│ │ ⑥ DNS 解析"源站"example.com ← 代理来解析!
│ │ ⑦ 连接池查找/新建 → TCP 连源站:443
│ ├──────────────────────────────▶│
│ ⑧ 200 Connection Established │◀──── 握手完成 ─────────────────┤
│◀──────────────────────────────┤
│ ⑨ 与源站做 TLS 握手(端到端,代理盲转密文)
├═══════════════════════════════│═══════════════════════════════▶│
│ ⑩ 发 HTTP 请求(加密) │ 盲转 │
├═══════════════════════════════│═══════════════════════════════▶│
│ │ ⑪ 源站处理、生成响应 │
│ ⑫ 流式收到响应(代理边收边转) │◀══════════════════════════════┤
│◀═══════════════════════════════│ │
│ ⑬ 连接保活(keep-alive)放回两端的连接池,或关闭、拆除
下面把关键阶段逐个讲透。
阶段②⑥:DNS 由谁解析——"DNS 泄漏"的根源
这是初学者最大的盲点。谁来把域名变成 IP,取决于代理类型和配置:
| 场景 | 客户端解析谁 | 代理解析谁 | 源站域名谁解析 |
|---|---|---|---|
| 正向 HTTP 代理(absolute-form / CONNECT) | 只解析代理的地址 | 解析源站域名 | 代理(远端 DNS) |
SOCKS5 + --socks5(本地 DNS) | 解析代理 + 源站 | 不解析 | 客户端(本地 DNS) |
SOCKS5 + --socks5-hostname(远端 DNS) | 只解析代理 | 解析源站 | 代理(远端 DNS) |
| 直连(无代理) | 解析源站 | — | 客户端 |
为什么这事关重大?
- DNS 泄漏:用代理本是为了隐藏访问目标,但若客户端在本地解析了源站域名,你的本地 DNS(运营商)就看到了你要访问谁——代理白用了。这正是科学上网里
--socks5vs--socks5-hostname的关键差异(第20章)。 - 内网解析:企业里只有代理能解析内网域名,客户端本地解析会失败——必须用远端 DNS。
记忆点:正向 HTTP 代理和
--socks5-hostname都是"远端 DNS"(代理解析),SOCKS5 默认--socks5是"本地 DNS"。 想不泄漏,用远端 DNS。
阶段⑤:认证发生在连源站之前
代理认证(第04章 的 407)发生在代理真正去连源站之前——代理得先确认"你有没有资格用我",才会替你出门。认证失败,源站这一跳根本不会发生。
阶段⑦:连接池——为什么第二个请求快得多
代理到源站这一跳,不会每次都新建 TCP+TLS 连接——那太贵了(一次 TLS 握手就是 1~2 个 RTT)。代理维护连接池:
第 1 次请求 example.com:池里没有 → 新建 TCP+TLS(慢,~100ms+)→ 用完放回池
第 2 次请求 example.com:池里有空闲连接 → 直接复用(快,省掉握手)
这就是为什么压测时第一个请求慢、后续快。连接池的大小、空闲超时是代理性能的核心调参(第31章)。客户端↔代理、代理↔源站是两个独立的池。
阶段⑫:流式转发 vs 缓冲整包
代理拿到源站响应后,何时开始转给客户端?两种策略:
- 流式(streaming):边收边转,第一个字节就转发——延迟低,内存省。适合大文件、SSE、流式响应。
- 缓冲(buffering):等收完整个响应体再转——便于改写、压缩、做内容检查,但增加延迟和内存。Nginx 默认对响应做缓冲(
proxy_buffering on)。
这个选择直接影响 TTFB(首字节时间)和大响应的内存占用,第31章 详谈。
阶段⑬:连接拆除与半关闭
转发完成后,两条连接各自决定:keep-alive 就放回池等复用,否则四次挥手关闭。隧道场景要处理半关闭(half-close):一个方向 EOF 了,另一个方向可能还在传——这正是 第21章 里"任一方向结束就关闭双端"那段代码要小心的地方,处理不好会留下一堆 CLOSE_WAIT。
命令 / 实验
实验一:把请求拆成可测量的时间分段
curl -w 能打印生命周期各阶段的累计耗时。建一个模板:
cat > curl-format.txt <<'EOF'
DNS 解析 : %{time_namelookup}s
TCP 连接 : %{time_connect}s
TLS 握手 : %{time_appconnect}s
传输前就绪 : %{time_pretransfer}s
首字节(TTFB) : %{time_starttransfer}s
总耗时 : %{time_total}s
EOF
直连 vs 走代理对比:
# 直连
curl -w "@curl-format.txt" -s -o /dev/null https://example.com/
# 走代理(注意 time_connect 现在是"连到代理"的耗时)
curl -w "@curl-format.txt" -s -o /dev/null -x http://127.0.0.1:3128 https://example.com/
典型输出(走代理):
DNS 解析 : 0.004s ← 解析"代理"地址(②)
TCP 连接 : 0.005s ← 连上代理(③)
TLS 握手 : 0.210s ← 含 CONNECT 隧道(④⑧) + 到源站的 TLS(⑨)
传输前就绪 : 0.211s
首字节(TTFB) : 0.355s ← 源站处理 + 回传(⑪⑫)
总耗时 : 0.360s
把这些数字对回时间线:
time_connect是"到代理",time_appconnect把 CONNECT 隧道和端到端 TLS 都算进去了,time_starttransfer减去time_appconnect大致是源站处理时间。学会读这几个数,排查"慢"就有了坐标。
实验二:验证"DNS 由谁解析"(抓本地 DNS 看泄漏)
# 监听本机 DNS 查询
sudo tcpdump -i any -n 'udp port 53' &
# A. SOCKS5 本地 DNS:本地会查 example.com → 泄漏!
curl --socks5 127.0.0.1:1080 https://example.com/ -s -o /dev/null
# tcpdump 里能看到对 example.com 的 A 查询
# B. SOCKS5 远端 DNS:本地不查 example.com → 不泄漏
curl --socks5-hostname 127.0.0.1:1080 https://example.com/ -s -o /dev/null
# tcpdump 里没有对 example.com 的查询(代理替你查了)
亲眼看到 A 泄漏、B 不泄漏,你就彻底懂了"远端 DNS"的价值。
实验三:人为制造 502 与 504,看故障落在哪个阶段
# 502:源站直接不可达(阶段⑦失败)——代理连不上后端
# 配一个 proxy_pass 指向一个根本没监听的端口,curl 走它 → 502 Bad Gateway
# 504:源站存在但响应极慢(阶段⑪⑫超时)
# 后端 sleep 超过代理 read timeout → 504 Gateway Timeout
记住这条因果链:502 = 代理连/读源站这一跳"坏了";504 = 这一跳"超时了"。两者都发生在"代理→源站"段,与"客户端→代理"段无关。
排错:阶段 → 故障 → 状态码
把生命周期翻过来用,就是一张排障地图:
| 卡在哪个阶段 | 现象 | 多半是 |
|---|---|---|
| ② 解析代理地址 | Could not resolve proxy | 代理主机名错、本地 DNS 挂了 |
| ③ 连代理 | Connection refused 连 :3128 | 代理没起、端口错、防火墙 |
| ⑤ 代理认证 | 407 Proxy Authentication Required | 没给/给错 Proxy-Authorization |
| ⑥ 代理解析源站 | 502,日志含 DNS 失败 | 代理侧 DNS 配置、内网域名 |
| ⑦ 连源站 | 502 Bad Gateway | 源站宕、端口错、网络不通 |
| ⑪⑫ 源站慢 | 504 Gateway Timeout | 源站负载高、代理 read timeout 太短 |
| ⑬ 拆连接 | 大量 CLOSE_WAIT、连接泄漏 | 半关闭处理不当、连接池泄漏 |
| CONNECT 阶段 | 403/405 | 代理端口白名单不含目标端口 |
完整的排错决策树(含抓包定位、分段二分法)见 第32章。
本章小结
- 一个走代理的请求是两条连接的接力:客户端↔代理、代理↔源站,各有独立的握手、连接池和超时。
- DNS 由谁解析决定是否泄漏:正向 HTTP 代理与
--socks5-hostname是远端 DNS(不泄漏),SOCKS5 默认本地 DNS(泄漏)。 curl -w的time_namelookup/connect/appconnect/starttransfer能把生命周期拆成可测量分段。- 502 = 代理→源站这一跳坏了;504 = 这一跳超时,都与客户端侧无关。
至此原理篇收官——你已经有了看穿任何代理的统一坐标系(类型 × 层级 × 生命周期)。下一篇进入协议篇,从 第05章 HTTPS 与 TLS 代理 开始,把每种代理协议的报文细节逐一拆开。