第 1 章
一次请求到底经过了什么——从 DNS 到响应的完整旅程
本章目标:
- 建立一次 HTTP 请求的完整地图:DNS、TCP、TLS、nginx 分发、应用处理、响应渲染,每一段都能叫出名字
- 会用
dig和curl -v把每一段亲手跑出来看,而不是背概念- 理解 SNI 机制:为什么一台服务器、一个 IP 能挂多个域名和各自独立的证书
- 分清静态站和动态站在 nginx 里的两条命运:
try_files与proxy_pass- 拿走一组排错入口:hosts、证书、502/504、301 缓存
本章基线:nginx + Halo(动态站 blog.qianyecube.cn)+ Astro 纯静态(notes.qianyecube.cn)+ Let’s Encrypt 证书 | 两台站跑在同一台云服务器上,正好当「一 IP 多域名」的活标本 | 命令输出为 2026-11-01 实拍
脱敏声明:服务器 IP 一律写作 203.0.113.10(RFC 5737 专门划给文档的测试网段——写教程不该把真实 IP 送人,这也是你的服务器的第一道习惯),软件版本号已抹去;域名是公开信息,原样保留,你拿自己的域名跑同款命令,输出长得一模一样
6.1 全景:一次请求的九段旅程
在浏览器地址栏敲下 https://notes.qianyecube.cn/knowledge 回车,到页面出现,中间发生了九件事:
sequenceDiagram
participant B as 浏览器
participant D as DNS 解析
participant N as nginx(服务器)
participant A as 应用(Halo 容器)
B->>D: ① 查 notes.qianyecube.cn 的 IP
D-->>B: ② 203.0.113.10(带 TTL 缓存)
B->>N: ③ TCP 三次握手 :443
B->>N: ④ TLS 握手(验证书、协商密钥、SNI 报域名)
B->>N: ⑤ HTTP 请求 GET /knowledge
N->>N: ⑥ 匹配 server_name,选 server 块
alt 静态站(notes)
N->>N: ⑦a try_files 找文件,直接回
else 动态站(blog)
N->>A: ⑦b proxy_pass 到 127.0.0.1:8091
A-->>N: 渲染好的 HTML
end
N-->>B: ⑧ 响应 + 状态码 + 缓存头
B->>B: ⑨ 解析 HTML,再发起新请求(CSS/JS/图)
第 ⑨ 步值得划重点:页面出现不代表只有一次请求——HTML 只是第一张多米诺,CSS、JS、图片、字体各是独立请求。性能优化的一半工作发生在「 subsequent requests」上(合并、缓存、预加载),这也是为什么 6.6 的缓存头值得单独一节。
本章所有环节都可以亲手复现。装备两条命令就够:dig 看 DNS,curl -v 看其余全部。
6.2 第一段:DNS——域名怎么变成 IP
机器之间用 IP 通信,域名是给人用的。DNS 就是把域名翻译成 IP 的全球分布式数据库:
$ dig +short notes.qianyecube.cn
203.0.113.10
这一行答案背后是一条查询链:浏览器先查自己的缓存,没有就问操作系统( hosts 文件和系统缓存在这层),还没有就问递归解析服务器(运营商或公共 DNS,如 223.5.5.5),递归服务器再从根域名服务器一路问到 .cn 权威服务器,最终拿到 notes.qianyecube.cn 在 DNS 记录里的 A 记录值。每一层都带 TTL 缓存——所以改 DNS 记录后「要等一会儿才生效」,等的就是各层缓存的 TTL 到期。
两个实用推论:换服务器 IP 时把 TTL 先调小(比如 300 秒),迁移窗口就不至于被旧缓存钉死;本地开发想「强制域名指到某台机器」,改的是 hosts 文件(操作系统层),浏览器缓存要另清。
6.3 TCP 与 TLS:两条命令看懂握手
DNS 拿到 IP 之后,浏览器和 203.0.113.10 的 443 端口建立 TCP 连接(三次握手),然后在这个加密通道里做 TLS 握手。curl -v 把两段全打出来:
$ curl -v -o /dev/null https://notes.qianyecube.cn/knowledge
* Trying 203.0.113.10:443... ← DNS 已解析,开始 TCP
* Connected to notes.qianyecube.cn (203.0.113.10) port 443 ← 三次握手完成
* SSL connection using TLSv1.3 / AEAD-CHACHA20-POLY1305-SHA256 ← TLS 1.3,一套加密套件
* subject: CN=notes.qianyecube.cn ← 证书发给谁
* issuer: C=US; O=Let's Encrypt; CN=YE2 ← 谁签发的(Let's Encrypt 中间证书)
> GET /knowledge HTTP/2 ← TLS 通道里发的第一条 HTTP 请求
< HTTP/2 200 ← 注意协议是 HTTP/2
< server: nginx
< cache-control: no-cache
读这十行的正确方式:TLS 之前的一切都是明文可见的(DNS 查询、TCP 握手、SNI 里的域名),TLS 之后的一切(包括 URL 路径、请求头、响应体)都是密文。TLS 1.3 相对 1.2 把握手从两次往返压到一次,握手阶段还把证书加密了——这是公网接口都该开的最低配置,nginx 里对应 ssl_protocols TLSv1.2 TLSv1.3。
证书字段顺带解释了浏览器的小锁图标:浏览器验证 subject 里的域名和访问的域名一致、issuer 的签发链能追到受信根证书、有效期没过——三条有一条不过,就是那个「您的连接不是私密连接」。
一个 IP 怎么挂两个域名:SNI
作者的两台站点在同一台服务器、同一个 IP 上,证书却是两张(blog 一张、notes 一张)。TLS 握手时,客户端在明文段里通过 SNI(Server Name Indication)扩展告诉服务器「我要访问的是哪个域名」,nginx 据此选出对应的 server 块、递上对应的证书:
notes.qianyecube.cn → A 记录 203.0.113.10 ┐
blog.qianyecube.cn → A 记录 203.0.113.10 ┘ 同一台机器
浏览器 SNI: "notes.qianyecube.cn" → nginx 递 notes 的证书 → 进 notes 的 server 块
没有 SNI,一台 IP 只能服务一张证书,HTTPS 时代的虚拟主机就不成立。老系统对接时如果碰到「证书不对但域名对」,先查对方是不是不支持 SNI 的古董客户端。
6.4 nginx:两条命运的分岔口
TLS 隧道打通,请求进入 nginx。nginx 用 server_name 匹配出该处理它的 server 块——这台服务器上有两份真实配置,正好是静态站和动态站的两条命运。
共同的第一站:HTTP 请求先被 80 端口的 server 块接住,整段 301 到 HTTPS——这是全站唯一入口原则,避免搜索引擎把 http:// 和 https:// 当两个站分权重:
server {
listen 80;
server_name notes.qianyecube.cn;
location /.well-known/acme-challenge/ { root /var/www/html; } # 证书续期专用
location / { return 301 https://$host$request_uri; }
}
命运 A——静态站(notes):请求的文件就在磁盘上,try_files 按顺序找:
root /opt/qianyecube/notes/current; # current 是软链,指向最新一版发布目录
location / {
try_files $uri $uri/index.html $uri/ =404; # 文件 → 目录首页 → 目录跳转 → 404
add_header Cache-Control "no-cache";
}
/knowledge 这个 URL 磁盘上没有同名文件,落进 $uri/index.html 找到 knowledge/index.html 直接回——nginx 自己就是应用,没有后端参与,这就是静态站的快。
命运 B——动态站(blog):文件不在磁盘上,nginx 转发给真正干活的应用:
location / {
proxy_pass http://127.0.0.1:8091; # Halo 应用(容器只绑回环地址)
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
那几行 proxy_set_header 不是仪式:应用在容器里看到的连接全部来自 127.0.0.1,不传 X-Real-IP 它就以为所有访客都是本机;不传 X-Forwarded-Proto,应用生成的跳转链接可能是 http://,又是一轮 301。反代的本质是替后端转述「真实的世界长什么样」。
6.5 响应:状态码与缓存头
请求处理完,响应带着三样东西回来:状态码、响应头、响应体。状态码只记三类高频:200 找到了(curl 实拍里 /knowledge 的响应)、301 永久搬家(本站 80→443、无尾斜杠补齐都用它)、404 没有这个资源(try_files 的 =404 兜底)。5 开头的两兄弟要分清:502 是 nginx 敲应用的门没人应(应用挂了、端口不对),504 是应了但超时没答复(应用活着但太慢)——502 查应用死活,504 查应用为什么慢,排错方向完全不同。
缓存头决定浏览器和中间节点下次还问不问。notes 站的真实分层策略:HTML 用 no-cache(每次发布立即生效,访问者永不看到旧页面),带内容哈希的构建产物 /_astro/ 用 30 天强缓存(文件名里带哈希,内容变则文件名必变,缓存永远不会错):
< cache-control: no-cache ← HTML:可缓存但必须回源验证
< expires: Tue, 01 Dec 2026 12:29:55 GMT ← /_astro/xxx.HASH.webp:30 天强缓存
< cache-control: max-age=2592000
这套「HTML 不缓存、哈希资源长缓存」是静态站发布系统的标准答案,任何一次发布都即时生效,同时老访客的二次加载几乎全走本地缓存。
6.6 排错清单:现象 → 先查什么
- 改了 DNS 不生效:先
dig +short 域名看解析到哪;没变是缓存 TTL 没到,变了还不对是浏览器缓存(换无痕窗口排除)。 - 证书报错但域名对:
curl -v看subject:是不是你要的域名——多站点服务器上 SNI 选错 server 块会递错证书;再看certbot certificates的有效期。 - 502 Bad Gateway:应用没起来或端口不对。动态站先
curl 127.0.0.1:8091从服务器本地敲应用的门,通了再查 nginx 配置。 - 504 Gateway Timeout:应用活着但太慢,别盯 nginx,去查应用的慢 SQL 和外部依赖。
- 改了配置没生效还一切正常:你看过的一直是 301 跳转后的缓存。
curl -v直接看响应头里的location:,别信浏览器地址栏。 - 本地开发连不上线上接口:先
ping(看 IP 通不通)再curl -v(看卡在哪一段)—— Trying 后无响应是安全组/防火墙,Connected 后卡住是服务端慢。
6.7 本章小结
| 段 | 干什么 | 一条命令亲眼看 |
|---|---|---|
| DNS | 域名 → IP,层层缓存 | dig +short 域名 |
| TCP | 三次握手建立连接 | curl -v 的 Trying/Connected |
| TLS | 验证书、协商密钥(SNI 选站点) | curl -v 的 SSL connection/subject |
| nginx 分发 | server_name 匹配;静态 try_files / 动态 proxy_pass | cat 服务器上的两份 conf |
| 应用 | 容器里真干活的服务 | docker ps 看端口映射 |
| 响应 | 状态码 + 缓存头 | curl -sI 网址 |
这张地图的价值不在面试,在排错:知道请求卡在哪一段,就知道该去哪台机器看哪份日志。下面三个练习把地图变成手感。
6.8 练习题
- 对你自己的任意一个常用网站跑
curl -v -o /dev/null,把输出按 6.3 的十行拆解一遍:TLS 版本、证书发给谁、谁签发的、HTTP 几、缓存头是什么。 - 用
dig +short分别查两个域名解析到同一个 IP 的站点(大厂很常见),再用浏览器开发者工具的 Security 面板确认它们证书不同——亲手验证一次 SNI。 - 在本地起一个
python3 -m http.server 8000,再curl -v http://127.0.0.1:8000:对照本章地图认出每一段——这是没有 DNS 和 TLS 的最短旅程,缺的那两段为什么本地可以省? - 故意把 hosts 里某域名指到一个不存在的 IP,观察浏览器报错停在旅程的哪一段;改回来再清缓存,体会「排错先定段」。
6.9 延伸读
- 请求落地之前,环境是怎么起的:Docker 起一套本地环境
- 6.4 静态站与反代的完整配置解析:Nginx 反向代理要点
- 动态应用从代码到可部署的流水线:CI/CD 最小可用方案
- 应用层怎么记录这一次请求:日志排查三板斧