写这一章的时候,我的 Wireshark 和终端一直开着,因为本章的每个数字都来自一次真实的抓包:在我自己的机器上访问 https://example.com/,从敲下回车到页面到手。前面九章是分镜头,这一章把镜头接回去:DNS、TCP、TLS、HTTP、路由、ARP,所有的东西在一次请求里各就各位。读法建议:对照第 1 章那棵协议树,每一层都能在这里找到自己的戏份。
用 curl 把各阶段耗时拆出来(-w 支持直接输出这些计时器):
$ curl -s -o /dev/null -w 'DNS: %{time_namelookup}s\nTCP: %{time_connect}s\nTLS: %{time_appconnect}s\n首字节: %{time_starttransfer}s\n总计: %{time_total}s\n' https://example.com/
DNS: 0.005780s
TCP: 0.219637s
TLS: 0.456015s
首字节: 0.696126s
总计: 0.696170s
这是计时器是累计值,相减得到各阶段真实开销:DNS 查询 6 ms(本机缓存命中,几乎白送),TCP 握手 214 ms,TLS 握手 236 ms,服务器吐出首字节 240 ms。
重点看中间两个数:第 5 章我 ping 同一批地址,RTT 是 186 ms 上下,而 TCP 握手恰好需要一个 RTT(发出 SYN,等回 SYN+ACK),214 减去一点开销,对上了。TLS 1.3 只需要 1-RTT,236 ms 也在一个 RTT 的量级。一个 700 ms 的「秒开」请求里,物理距离吃掉了六成,这就是为什么 CDN 要把机房搬到你旁边,也是第 7 章那个「光速的三分之二」的现实版本。
从回车到页面,全程九步(括号是详解所在章节):
1. DNS 解析 域名 → IP,缓存/递归/迭代 (第 2 章)
2. TCP 三次握手 随机端口(如 33906)→ 443 (第 3、4、9 章)
3. TLS 握手 验证书,协商会话密钥 (第 8 章)
4. 发送请求 GET / HTTP/2 (第 2 章)
5. 服务器处理 路由、查库、渲染模板…… (框架的事)
6. 返回响应 200 + HTML,分段编号滑出 (第 4 章)
7. 沿途转发 每一跳:路由表 → ARP → 帧出网卡 (第 5、6 章)
8. 浏览器渲染 解析 HTML/CSS/JS,可能再触发 N 个请求 (浏览器的事)
9. 连接收尾 HTTP/2 复用连接;空闲后四次挥手 (第 4 章)
第 4 到 7 步值得强调:每往下一层,数据就被再包一层(TCP 头、IP 头、以太网帧头尾,第 1 章的套娃),每过一个路由器,MAC 头撕掉重写而 IP 头不动(第 6 章),每过一段光纤就变成光脉冲(第 7 章)。服务器回程时同理反着来一遍。
curl -v 把整场戏的台词都打印出来(节选,本机实跑):
* Host example.com:443 was resolved.
* IPv6: 2606:4700:10::6814:179a, 2606:4700:10::ac42:93f3
* IPv4: 172.66.147.243, 104.20.23.154
* Trying [2606:4700:10::6814:179a]:443...
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
* TLSv1.3 (IN), TLS handshake, Server hello (2):
* TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8):
* TLSv1.3 (IN), TLS handshake, Certificate (11):
* SSL connection using TLSv1.3 / TLS_AES_256_GCM_SHA384 / X25519MLKEM768
* Server certificate:
* subject: CN=example.com
* issuer: C=US; O=SSL Corporation; CN=Cloudflare TLS Issuing ECC CA 3
* Established connection to example.com (2606:4700:10::6814:179a port 443)
* using HTTP/2
> GET / HTTP/2
> Host: example.com
> User-Agent: curl/8.23.0
> Accept: */*
逐段对应时间线:第一块是 DNS 结果(注意域名解析出一批 IPv6 和 IPv4,curl 挨个试,Happy Eyeballs);握手三个 TCP 包 curl 不屑于打印(太 routine),直接跳到 TLS 的 ClientHello;证书在 Certificate (11) 那一步到达并被本机 OpenSSL 验证;using HTTP/2 是 TLS 握手里 ALPN 扩展顺带谈成的——连用什么 HTTP 版本都塞进了 TLS 握手;最后 > 开头的就是发出去的请求。
想看更底层的,抓包就是了:
sudo tcpdump -i any port 443 -c 10 # 只看 443,收十个包收手
Wireshark 里同样的事用过滤器:dns 看 10.2 的第 1 步,tcp.port == 443 看握手,icmp 看第 5 章的体检报文。第 4 章画的时序图和真实报文一一对应,看过一次就再也忘不掉。
DevTools 的 Network 面板里点开任一请求,Timing 标签把同样的账单换个名字再给你一遍:DNS Lookup、Initial connection(TCP)、SSL(TLS)、Waiting for server response(即 TTFB)、Content Download(正文下载)。两边对照过一次,你就拥有了两种武器:curl 快、适合脚本和命令行;DevTools 直观、能看每个资源的瀑布图。页面慢的时候,先看时间大头落在哪一段,再决定去查 DNS、网络还是服务器。
最后给我的排查习惯,按层从下往上,每步都有第 N 章背书:
| 症状线索 | 先查什么 | 章节武器 |
|---|---|---|
| ping 域名不通,ping IP 通 | DNS | getent hosts,第 2 章 |
| ping 同网段通,外网全断 | 网关/默认路由 | ip route,第 5 章 |
curl 卡在 Trying <IP>:443... | 路由或防火墙丢包 | 第 5 章,tracepath 看断在哪一跳 |
curl 立刻 Connection refused | 端口上没服务 | 第 3 章 RST,ss -tlnp 看监听 |
| TLS 告警 | 证书链/系统时间 | 第 8 章,读错误码 |
| 小包通、大包死 | MTU | ping -s 1472,第 5 章 |
| 请求发出去后周期性卡顿 | Nagle × 延迟 ACK | 第 4 章,40 ms 空档 |
这张表当然覆盖不了全部故障,但「先分层定位、再深入单层」是永远不会错的框架,毕竟网络本来就是这么造出来的。