当你在浏览器地址栏输入一个 URL 并按下回车,背后会经历 URL 解析、DNS 查询、TCP 连接、TLS 握手、HTTP 请求与响应、服务器处理以及浏览器渲染等一系列步骤,最终将页面呈现在你眼前。
从输入 URL 到页面渲染完成,整个过程可以用下面的流程图概括:
flowchart LR
A[URL 解析] --> B[DNS 查询]
B --> C[TCP 连接]
C --> D[TLS 握手]
D --> E[HTTP 请求]
E --> F[服务器处理]
F --> G[HTTP 响应]
G --> H[浏览器渲染]
| 阶段 | 核心任务 | 关键协议/技术 |
|---|---|---|
| URL 解析 | 拆解协议、主机名、路径等信息 | URL 规范 |
| DNS 查询 | 将域名解析为 IP 地址 | DNS |
| TCP 连接 | 建立可靠的传输通道 | TCP 三次握手 |
| TLS 握手 | 建立加密安全通道 | TLS 1.2 / 1.3 |
| HTTP 请求 | 发送请求报文 | HTTP/1.1、HTTP/2 |
| 服务器处理 | 路由、业务逻辑、数据库查询等 | 后端框架 |
| HTTP 响应 | 返回状态码、响应头和响应体 | HTTP |
| 浏览器渲染 | 解析 HTML/CSS/JS,构建渲染树绘制 | 浏览器引擎 |
curl -v 是观测网络请求全过程的利器。一条命令就能看到 DNS 解析、TCP 连接、TLS 握手、HTTP 请求与响应的完整细节:
curl -v https://httpbin.org/get
提示:
-v(verbose)标志会输出连接过程中每个阶段的详细信息,是理解网络请求流程最直观的方式。
完整输出如下,后续章节会逐段拆解分析:
* Host httpbin.org:443 was resolved.
* IPv6: (none)
* IPv4: 198.18.0.40
* Trying 198.18.0.40:443...
* Connected to httpbin.org (198.18.0.40) port 443
* ALPN: curl offers h2,http/1.1
* (304) (OUT), TLS handshake, Client hello (1):
* CAfile: /etc/ssl/cert.pem
* CApath: none
* (304) (IN), TLS handshake, Server hello (2):
* TLSv1.2 (IN), TLS handshake, Certificate (11):
* TLSv1.2 (IN), TLS handshake, Server key exchange (12):
* TLSv1.2 (IN), TLS handshake, Server finished (14):
* TLSv1.2 (OUT), TLS handshake, Client key exchange (16):
* TLSv1.2 (OUT), TLS change cipher, Change cipher spec (1):
* TLSv1.2 (OUT), TLS handshake, Finished (20):
* TLSv1.2 (IN), TLS change cipher, Change cipher spec (1):
* TLSv1.2 (IN), TLS handshake, Finished (20):
* SSL connection using TLSv1.2 / ECDHE-RSA-AES128-GCM-SHA256 / [blank] / UNDEF
* ALPN: server accepted h2
* Server certificate:
* subject: CN=httpbin.org
* start date: Jul 20 00:00:00 2025 GMT
* expire date: Aug 17 23:59:59 2026 GMT
* subjectAltName: host "httpbin.org" matched cert's "httpbin.org"
* issuer: C=US; O=Amazon; CN=Amazon RSA 2048 M03
* SSL certificate verify ok.
* using HTTP/2
* [HTTP/2] [1] OPENED stream for https://httpbin.org/get
* [HTTP/2] [1] [:method: GET]
* [HTTP/2] [1] [:scheme: https]
* [HTTP/2] [1] [:authority: httpbin.org]
* [HTTP/2] [1] [:path: /get]
* [HTTP/2] [1] [user-agent: curl/8.7.1]
* [HTTP/2] [1] [accept: */*]
> GET /get HTTP/2
> Host: httpbin.org
> User-Agent: curl/8.7.1
> Accept: */*
>
* Request completely sent off
< HTTP/2 200
< date: Sun, 01 Mar 2026 14:54:46 GMT
< content-type: application/json
< content-length: 256
< server: gunicorn/19.9.0
< access-control-allow-origin: *
< access-control-allow-credentials: true
<
{
"args": {},
"headers": {
"Accept": "*/*",
"Host": "httpbin.org",
"User-Agent": "curl/8.7.1",
"X-Amzn-Trace-Id": "Root=1-69a45336-01c9a5360d78dba746e05f2d"
},
"origin": "140.235.140.187",
"url": "https://httpbin.org/get"
}
* Connection #0 to host httpbin.org left intact
在这段输出中,可以清晰地看到各阶段的对应关系:
| curl 输出标志 | 对应阶段 |
|---|---|
Host ... was resolved |
DNS 查询 |
Trying ... Connected to |
TCP 连接 |
TLS handshake 相关行 |
TLS 握手 |
> 开头的行 |
HTTP 请求 |
< 开头的行 |
HTTP 响应 |
| JSON 响应体 | 服务器处理结果 |
以 https://httpbin.org/get 为例,一个完整的 URL 可以拆解为以下几个组成部分:
| 组成部分 | 值 | 说明 |
|---|---|---|
| 协议 | https |
指定使用的应用层协议,https 表示通过 TLS 加密的 HTTP 通信 |
| 域名 | httpbin.org |
目标服务器的主机名,后续需要通过 DNS 解析为 IP 地址 |
| 端口 | 443(隐含) |
HTTPS 默认端口为 443,HTTP 默认为 80,省略时使用协议对应的默认端口 |
| 路径 | /get |
服务器上的资源路径,告诉服务器客户端请求的是哪个资源 |
面试提示:完整的 URL 还可以包含查询参数(
?key=value)、片段标识符(#section)等部分。面试时能完整说出 URL 的各组成部分,说明你对 HTTP 协议有扎实的理解。
当用户在地址栏输入内容并按下回车时,浏览器首先需要判断输入的是一个 URL 还是搜索关键词。判断逻辑大致如下:
http:// 或 https:// 开头,直接当作 URL 处理.(如 httpbin.org),浏览器倾向于将其视为域名://、/、: 等 URL 特征字符时,优先作为 URL 处理面试提示:这个判断逻辑看似简单,但它解释了为什么输入
localhost会访问本地服务器,而输入一个普通单词会触发搜索。
在确定输入是 URL 之后,如果用户输入的是 http:// 协议,浏览器会检查该域名是否在 HSTS(HTTP Strict Transport Security) 列表中。如果是,浏览器会在发出请求之前将 http:// 强制替换为 https://。
HSTS 有两种生效方式:
Strict-Transport-Security 响应头告知浏览器在指定时间内只使用 HTTPS 访问该域名。浏览器会将此信息缓存在本地面试提示:HSTS 的核心价值在于防止 SSL 剥离攻击(SSL Stripping)——攻击者在中间将 HTTPS 降级为 HTTP,从而窃取明文数据。Preload List 则解决了”首次访问”的安全盲区。
回顾 curl -v 输出的第一段,可以看到 DNS 解析的结果:
* Host httpbin.org:443 was resolved.
* IPv6: (none)
* IPv4: 198.18.0.40
这三行告诉我们:浏览器(或 curl)将域名 httpbin.org 解析为了 IP 地址 198.18.0.40,没有获取到 IPv6 地址。这个看似简单的结果,背后经历了一套完整的 DNS 查询流程。
DNS 查询采用分层缓存和递归查询机制,完整流程如下:
缓存层级(按查询顺序):
mDNSResponder、Linux 的 systemd-resolved当所有缓存未命中时,递归解析器会执行以下查询过程:
sequenceDiagram
participant B as 浏览器
participant L as 本地DNS缓存
participant R as 递归解析器
participant Root as 根DNS服务器
participant Org as .org DNS服务器
participant Auth as 权威DNS服务器
B->>L: 查询 httpbin.org
L-->>B: 未命中
B->>R: 查询 httpbin.org
R->>Root: 查询 httpbin.org
Root-->>R: 返回 .org NS 记录
R->>Org: 查询 httpbin.org
Org-->>R: 返回 httpbin.org NS 记录
R->>Auth: 查询 httpbin.org
Auth-->>R: 返回 198.18.0.40
R-->>B: 返回 198.18.0.40
面试提示:面试官常问”DNS 用的是 TCP 还是 UDP?”答案是 默认使用 UDP(端口 53),因为 DNS 查询报文通常很小,UDP 的无连接特性更高效。但当响应数据超过 512 字节(如区域传输)时会切换到 TCP。
在实际开发中,有多种 DNS 优化手段可以提升页面加载速度:
<link rel="dns-prefetch" href="//cdn.example.com" />
<link rel="dns-prefetch" href="//api.example.com" />
dns-prefetch 更进一步,提前完成 DNS + TCP + TLS 的全部连接步骤:<link rel="preconnect" href="https://cdn.example.com" />
面试提示:
dns-prefetch和preconnect是前端性能优化的常见考点。关键区别是dns-prefetch只做 DNS 解析,而preconnect会建立完整连接。对于关键资源用preconnect,对于可能用到的资源用dns-prefetch。
DNS 解析拿到 IP 地址后,下一步就是建立 TCP 连接。curl -v 输出中对应的部分:
* Trying 198.18.0.40:443...
* Connected to httpbin.org (198.18.0.40) port 443
Trying 表示正在向目标 IP 的 443 端口发起 TCP 连接,Connected 表示三次握手已经成功完成,连接建立。
TCP 使用三次握手(Three-Way Handshake)来建立可靠连接。这个过程确保通信双方都具备发送和接收数据的能力:
sequenceDiagram
participant C as 客户端
participant S as 服务器
C->>S: SYN (seq=x)
Note right of S: 客户端请求建立连接
S->>C: SYN-ACK (seq=y, ack=x+1)
Note left of C: 服务器确认并请求建立连接
C->>S: ACK (ack=y+1)
Note right of S: 客户端确认,连接建立
三次握手各步骤详解:
x,表示请求建立连接。客户端进入 SYN_SENT 状态y,并将确认号设为 x+1。服务器进入 SYN_RCVD 状态y+1。双方进入 ESTABLISHED 状态,连接建立完成面试中经常会问:”为什么 TCP 需要三次握手,两次不行吗?”
核心原因是防止历史连接请求导致资源浪费。考虑以下场景:
如果只有两次握手,服务器收到旧 SYN 后会直接建立连接并分配资源,但客户端不会响应这个连接,导致服务器资源被白白占用。
有了三次握手,服务器回复 SYN-ACK 后需要等待客户端的 ACK 确认。客户端收到这个 SYN-ACK 后,发现不是自己发起的连接请求,会回复 RST(重置)报文,服务器随即释放资源。
面试提示:三次握手的本质是确认双方的发送和接收能力。第一次握手确认客户端能发送,第二次确认服务器能接收和发送,第三次确认客户端能接收。这是建立可靠双向通信的最小次数。
TCP 连接建立后,由于我们访问的是 https:// 协议,客户端和服务器需要通过 TLS 握手建立加密通道。curl -v 输出中 TLS 握手的完整过程如下:
* ALPN: curl offers h2,http/1.1
* (304) (OUT), TLS handshake, Client hello (1):
* CAfile: /etc/ssl/cert.pem
* CApath: none
* (304) (IN), TLS handshake, Server hello (2):
* TLSv1.2 (IN), TLS handshake, Certificate (11):
* TLSv1.2 (IN), TLS handshake, Server key exchange (12):
* TLSv1.2 (IN), TLS handshake, Server finished (14):
* TLSv1.2 (OUT), TLS handshake, Client key exchange (16):
* TLSv1.2 (OUT), TLS change cipher, Change cipher spec (1):
* TLSv1.2 (OUT), TLS handshake, Finished (20):
* TLSv1.2 (IN), TLS change cipher, Change cipher spec (1):
* TLSv1.2 (IN), TLS handshake, Finished (20):
* SSL connection using TLSv1.2 / ECDHE-RSA-AES128-GCM-SHA256 / [blank] / UNDEF
* ALPN: server accepted h2
* Server certificate:
* subject: CN=httpbin.org
* start date: Jul 20 00:00:00 2025 GMT
* expire date: Aug 17 23:59:59 2026 GMT
* subjectAltName: host "httpbin.org" matched cert's "httpbin.org"
* issuer: C=US; O=Amazon; CN=Amazon RSA 2048 M03
* SSL certificate verify ok.
这段输出信息量很大,包含了 TLS 握手的每一个步骤。其中 (OUT) 表示客户端发出的消息,(IN) 表示从服务器收到的消息,括号中的数字是 TLS 握手消息类型编号。接下来逐步拆解。
TLS 1.2 的握手过程需要 2 个 RTT(往返时间)才能完成,比 TCP 的 1 个 RTT 多了一倍。这也是为什么 HTTPS 比 HTTP 建立连接更慢的原因之一。
第一步:Client Hello(客户端问候)
* (304) (OUT), TLS handshake, Client hello (1):
* CAfile: /etc/ssl/cert.pem
* CApath: none
客户端向服务器发送 Client Hello 消息,包含以下关键信息:
httpbin.org,让服务器知道客户端要访问哪个站点(一台服务器可能托管多个域名)CAfile 指向本地的证书信任库 /etc/ssl/cert.pem,客户端稍后会用它来验证服务器证书。
第二步:Server Hello(服务器问候)
* (304) (IN), TLS handshake, Server hello (2):
服务器从客户端提供的选项中做出选择,回复 Server Hello 消息:
ECDHE-RSA-AES128-GCM-SHA256(从客户端列表中选择的最优方案)第三步:Certificate(服务器证书)
* TLSv1.2 (IN), TLS handshake, Certificate (11):
服务器将自己的数字证书发送给客户端。证书中包含服务器的公钥和身份信息(如 CN=httpbin.org),由受信任的 CA(证书颁发机构)签名。客户端会验证证书的合法性,这一步是防止中间人攻击的关键。
第四步:Server Key Exchange(服务器密钥交换)
* TLSv1.2 (IN), TLS handshake, Server key exchange (12):
由于使用的是 ECDHE 密钥交换算法,服务器需要发送额外的参数——ECDHE 的公钥参数(椭圆曲线参数和服务器的临时公钥)。服务器用自己的 RSA 私钥对这些参数进行签名,确保参数在传输过程中不被篡改。
为什么需要 ECDHE? 因为 ECDHE 提供了前向安全性(Forward Secrecy):每次握手都会生成新的临时密钥对,即使服务器的长期私钥将来被泄露,之前的通信内容也无法被解密。
第五步:Server Hello Done
* TLSv1.2 (IN), TLS handshake, Server finished (14):
服务器发送 Server Hello Done 消息,告知客户端:我这边的握手消息发送完毕了。curl 将此消息显示为 Server finished (14),其中 14 是 TLS 握手消息类型 ServerHelloDone 的编号。
第六步:Client Key Exchange + Change Cipher Spec + Finished
* TLSv1.2 (OUT), TLS handshake, Client key exchange (16):
* TLSv1.2 (OUT), TLS change cipher, Change cipher spec (1):
* TLSv1.2 (OUT), TLS handshake, Finished (20):
这三条消息是客户端连续发出的:
第七步:服务器确认
* TLSv1.2 (IN), TLS change cipher, Change cipher spec (1):
* TLSv1.2 (IN), TLS handshake, Finished (20):
服务器也发送 Change Cipher Spec 和 Finished 消息,确认双方进入加密通信状态。至此,TLS 握手完成,安全通道建立成功。
完整的 TLS 1.2 握手流程如下:
sequenceDiagram
participant C as 客户端
participant S as 服务器
Note over C,S: 第 1 个 RTT
C->>S: Client Hello(TLS版本、加密套件列表、客户端随机数、SNI)
S->>C: Server Hello(选定加密套件、服务器随机数)
S->>C: Certificate(服务器证书 + 公钥)
S->>C: Server Key Exchange(ECDHE 参数)
S->>C: Server Hello Done
Note over C,S: 第 2 个 RTT
C->>C: 验证服务器证书
C->>S: Client Key Exchange(客户端 ECDHE 公钥)
C->>S: Change Cipher Spec
C->>S: Finished(加密的握手摘要)
S->>C: Change Cipher Spec
S->>C: Finished(加密的握手摘要)
Note over C,S: 加密通道建立完成
面试提示:TLS 1.3 将握手从 2-RTT 优化到了 1-RTT,甚至支持 0-RTT 恢复。核心改进是在 Client Hello 中就附带密钥交换参数,减少了一次往返。面试时提到 TLS 1.2 和 1.3 的区别是加分项。
TLS 握手完成后,curl 输出了服务器证书的详细信息:
* Server certificate:
* subject: CN=httpbin.org
* start date: Jul 20 00:00:00 2025 GMT
* expire date: Aug 17 23:59:59 2026 GMT
* subjectAltName: host "httpbin.org" matched cert's "httpbin.org"
* issuer: C=US; O=Amazon; CN=Amazon RSA 2048 M03
* SSL certificate verify ok.
客户端(浏览器或 curl)验证证书时会检查以下几项:
httpbin.org 的证书由 Amazon RSA 2048 M03 签发,而 Amazon 的中间 CA 证书又由 Amazon 的根 CA 签发。客户端沿着证书链向上追溯,直到找到本地信任库(/etc/ssl/cert.pem)中预装的根证书,形成完整的信任链subject(CN=httpbin.org)和 subjectAltName 是否与实际访问的域名一致。输出中的 host "httpbin.org" matched cert's "httpbin.org" 确认了匹配成功start date 到 expire date 范围内才有效。当前日期必须在 2025-07-20 到 2026-08-17 之间最终 SSL certificate verify ok. 表示所有验证均通过。
面试提示:面试中常问”HTTPS 如何防止中间人攻击?”核心就是证书验证机制。中间人无法伪造受信任 CA 签名的证书,因此即使截获通信也无法冒充服务器。如果证书验证失败,浏览器会显示安全警告,阻止用户访问。
在 TLS 握手过程中,还嵌套了一个 ALPN(Application-Layer Protocol Negotiation,应用层协议协商) 过程:
* ALPN: curl offers h2,http/1.1
* ALPN: server accepted h2
第一行表示客户端在 Client Hello 中告知服务器:”我支持 HTTP/2(h2)和 HTTP/1.1,优先使用 HTTP/2。”第二行表示服务器在 Server Hello 中回复:”我选择 HTTP/2。”
ALPN 的作用是在 TLS 握手阶段就确定应用层协议,避免建立连接后再协商导致的额外往返。这也是为什么 HTTP/2 在实践中几乎总是与 HTTPS 一起使用——ALPN 需要 TLS 扩展来承载。
面试提示:HTTP/2 规范本身并不强制要求 TLS,但所有主流浏览器都只在 HTTPS 连接上支持 HTTP/2。原因之一就是 ALPN 提供了一种优雅的协议升级方式,不需要像 HTTP/1.1 的
Upgrade头那样多一次往返。
握手完成后,curl 输出了最终选定的加密套件:
* SSL connection using TLSv1.2 / ECDHE-RSA-AES128-GCM-SHA256 / [blank] / UNDEF
ECDHE-RSA-AES128-GCM-SHA256 这一长串名称实际上由四个组件组成,每个组件在加密通信中承担不同角色:
| 组件 | 值 | 作用 |
|---|---|---|
| 密钥交换 | ECDHE | 椭圆曲线 Diffie-Hellman 临时密钥交换,让双方在不安全的信道上协商出共享密钥,”临时”意味着每次连接使用新密钥对,提供前向安全性 |
| 认证 | RSA | 使用 RSA 算法验证服务器身份,即用服务器的 RSA 私钥签名握手参数,客户端用证书中的公钥验证签名 |
| 加密 | AES128-GCM | 使用 128 位密钥的 AES 对称加密算法,GCM 模式同时提供加密和数据完整性校验(AEAD),无需额外的 MAC 计算 |
| 摘要 | SHA256 | 用于 PRF(伪随机函数)生成密钥材料和握手过程中的消息验证 |
这四个组件协同工作:ECDHE 负责安全地交换密钥,RSA 确保通信对象的身份可信,AES128-GCM 负责对实际数据进行高速加密和完整性保护,SHA256 保障握手过程和密钥派生的安全性。
面试提示:面试官可能会问”对称加密和非对称加密在 HTTPS 中分别用在哪里?”答案是:非对称加密(RSA/ECDHE)用于握手阶段的身份验证和密钥交换,对称加密(AES)用于后续的数据传输。原因是非对称加密计算成本高,不适合大量数据传输;对称加密速度快但需要双方共享密钥,而密钥交换正是靠非对称加密来安全完成的。
TLS 握手完成并通过 ALPN 协商确定使用 HTTP/2 后,客户端开始发送 HTTP 请求。curl -v 输出中对应的部分:
* using HTTP/2
* [HTTP/2] [1] OPENED stream for https://httpbin.org/get
* [HTTP/2] [1] [:method: GET]
* [HTTP/2] [1] [:scheme: https]
* [HTTP/2] [1] [:authority: httpbin.org]
* [HTTP/2] [1] [:path: /get]
* [HTTP/2] [1] [user-agent: curl/8.7.1]
* [HTTP/2] [1] [accept: */*]
> GET /get HTTP/2
> Host: httpbin.org
> User-Agent: curl/8.7.1
> Accept: */*
这段输出包含两层信息。以 * 开头的行展示了 HTTP/2 的底层细节:stream 编号为 [1],以及 HTTP/2 特有的伪头部(pseudo-headers),它们以冒号 : 开头。以 > 开头的行是 curl 对请求的可读化展示。
HTTP/2 的四个伪头部各有其作用:
:method: GET:请求方法,对应 HTTP/1.1 请求行中的方法:scheme: https:协议方案,HTTP/1.1 中隐含在连接类型中:authority: httpbin.org:目标主机,替代 HTTP/1.1 中的 Host 头:path: /get:请求路径,对应 HTTP/1.1 请求行中的 URIHTTP/2 相较于 HTTP/1.1 做了多项重大改进,这些改进直接影响页面加载性能:
| 特性 | HTTP/1.1 | HTTP/2 |
|---|---|---|
| 传输格式 | 纯文本协议,请求和响应都是可读文本 | 二进制分帧,将数据拆分为更小的帧进行传输 |
| 多路复用 | 每个请求需要独占一个 TCP 连接,或使用管线化(实际很少使用) | 多个请求/响应可在同一个 TCP 连接上并行传输,互不阻塞 |
| 头部处理 | 每次请求都完整发送所有头部,存在大量重复 | 使用 HPACK 算法压缩头部,维护动态表记录已发送的头部,只传输变化的部分 |
| 服务器推送 | 不支持,客户端必须主动请求每个资源 | 服务器可以在客户端请求之前主动推送相关资源(如 CSS、JS) |
| 请求头格式 | 请求行 + Host 头(GET /get HTTP/1.1) |
伪头部(:method、:scheme、:authority、:path) |
为什么多路复用如此重要? 在 HTTP/1.1 时代,浏览器为了并行加载资源,需要对同一域名开启 6-8 个 TCP 连接。每个连接都需要独立的三次握手和 TLS 握手,开销很大。HTTP/2 在单个连接上使用 stream(流)来并行传输多个请求和响应,每个 stream 有独立的编号(如 [1]),大幅减少了连接建立的开销。
面试提示:面试中常问”HTTP/2 解决了 HTTP/1.1 的哪些问题?”重点回答:队头阻塞(通过多路复用解决连接级别的队头阻塞,但 TCP 层仍存在)、头部冗余(HPACK 压缩)、文本效率低(二进制分帧)。进一步提到 HTTP/3 使用 QUIC 协议彻底解决 TCP 层队头阻塞,是加分项。
服务器处理请求后返回的响应如下:
< HTTP/2 200
< date: Sun, 01 Mar 2026 14:54:46 GMT
< content-type: application/json
< content-length: 256
< server: gunicorn/19.9.0
< access-control-allow-origin: *
< access-control-allow-credentials: true
以 < 开头的行表示从服务器接收到的响应,逐行分析各响应头:
HTTP/2 200:状态行,HTTP/2 是协议版本,200 是状态码,表示请求成功date:服务器生成响应的时间,用于缓存计算和调试content-type: application/json:响应体的 MIME 类型,告诉客户端返回的是 JSON 格式数据,浏览器据此决定如何解析和渲染content-length: 256:响应体的字节长度,客户端据此判断数据是否接收完整server: gunicorn/19.9.0:服务器软件标识,httpbin.org 使用 Python 的 Gunicorn WSGI 服务器access-control-allow-origin: *:CORS 头,允许任何域名的网页通过 JavaScript 访问该接口access-control-allow-credentials: true:CORS 头,允许请求携带凭证(如 Cookie)响应体是一个 JSON 对象:
{
"args": {},
"headers": {
"Accept": "*/*",
"Host": "httpbin.org",
"User-Agent": "curl/8.7.1",
"X-Amzn-Trace-Id": "Root=1-69a45336-01c9a5360d78dba746e05f2d"
},
"origin": "140.235.140.187",
"url": "https://httpbin.org/get"
}
httpbin.org 的 /get 端点会将收到的请求信息”回显”给客户端,方便调试。可以看到服务器收到了我们发送的 Accept、Host、User-Agent 头,origin 是客户端的公网 IP 地址,X-Amzn-Trace-Id 是 AWS 负载均衡器自动添加的请求追踪标识。
下面的时序图展示了 HTTP/2 请求与响应在 stream 中的完整交互:
sequenceDiagram
participant C as 客户端
participant S as 服务器
Note over C,S: TLS 加密通道已建立(HTTP/2)
C->>S: HEADERS 帧(stream 1)<br/>:method: GET<br/>:path: /get<br/>:authority: httpbin.org
Note right of S: 服务器处理请求
S->>C: HEADERS 帧(stream 1)<br/>:status: 200<br/>content-type: application/json
S->>C: DATA 帧(stream 1)<br/>JSON 响应体
Note left of C: 接收完成,解析响应
面试提示:面试官问到 HTTP 状态码时,不要只背 200、404、500。重点理解分类:2xx 成功、3xx 重定向(301 永久 / 302 临时 / 304 缓存未变)、4xx 客户端错误(401 未认证 / 403 无权限 / 404 不存在)、5xx 服务器错误。能结合实际场景解释何时出现这些状态码,远比死记硬背更有说服力。
在响应头中,我们已经看到了服务器的身份标识:
< server: gunicorn/19.9.0
这告诉我们 httpbin.org 使用的是 Gunicorn——一个 Python WSGI HTTP 服务器,版本 19.9.0。但在生产环境中,一个请求从到达服务器到返回响应,通常不会只经过一个软件组件。
一个典型的 Web 服务器架构采用多层处理模型:
客户端请求 → 反向代理(Nginx) → WSGI 服务器(Gunicorn) → Web 应用(Flask/Django)
每一层各司其职:
为什么要分层? 核心原因是关注点分离和并发处理能力不同。Nginx 使用事件驱动模型,能高效处理数万并发连接和静态资源;Gunicorn 通过多进程模型隔离请求,保证一个请求崩溃不会影响其他请求;应用层只需专注于业务逻辑,无需关心连接管理和进程调度。这种分层架构让每个组件都发挥自己的优势,整体性能和稳定性远优于单一组件处理一切。
面试提示:面试中提到”从 URL 到页面渲染”时,服务器处理部分不需要深入展开,但能说出反向代理 → 应用服务器 → Web 框架的分层结构,说明你对后端架构有基本了解,这是全栈思维的体现。
从这一章开始,我们进入了 curl 无法观测的领域——浏览器接收到 HTTP 响应后,如何将 HTML、CSS、JavaScript 转化为用户看到的页面。这是前端面试的核心考点。
浏览器收到服务器返回的 HTML 字节流后,会经历以下解析过程将其转换为 DOM 树:
字节(Bytes) → 字符(Characters) → 词法单元(Tokens) → 节点(Nodes) → DOM 树
各阶段详解:
content-type: text/html; charset=UTF-8 或 HTML 中的 <meta charset="UTF-8">,将原始字节流按指定编码解码为字符串<div>)、结束标签(</div>)、属性(class="container")、文本内容等<html> 是根节点,<head> 和 <body> 是其子节点,层层嵌套面试提示:DOM(Document Object Model)不仅是 HTML 的内存表示,也是 JavaScript 操作页面的接口。
document.getElementById()等 API 本质上就是在这棵树上进行查找和操作。
当 HTML 解析器遇到 <link rel="stylesheet"> 或 <style> 标签时,浏览器会启动 CSS 解析,构建 CSSOM(CSS Object Model)树。解析过程与 DOM 类似:
字节(Bytes) → 字符(Characters) → 词法单元(Tokens) → 节点(Nodes) → CSSOM 树
CSSOM 树的每个节点包含样式规则,并且具有层叠特性——浏览器会按照优先级合并来自不同来源的样式(浏览器默认样式 → 外部样式表 → 内联样式 → !important)。
CSSOM 树的一个关键特性是层叠计算需要完整的 CSS 信息。浏览器必须等到所有 CSS 下载并解析完毕后,才能确定每个元素的最终样式。这就是为什么 CSS 被称为渲染阻塞资源——在 CSSOM 构建完成之前,浏览器不会渲染任何内容。
面试提示:这也是为什么推荐将 CSS 放在
<head>中的原因——让浏览器尽早发现并下载 CSS,减少渲染阻塞时间。
DOM 树和 CSSOM 树都构建完成后,浏览器将它们合并为渲染树(Render Tree):
DOM 树 + CSSOM 树 → 渲染树(Render Tree)
渲染树的构建规则:
<head>、<meta>、<script> 等不产生视觉输出的节点不会进入渲染树;设置了 display: none 的元素及其子元素也会被排除::before 和 ::after 等伪元素虽然不在 DOM 树中,但会被添加到渲染树中注意 visibility: hidden 和 display: none 的区别——前者的元素仍在渲染树中(占据空间但不可见),后者的元素完全不在渲染树中(不占据空间)。
面试提示:这是面试高频考点。”display:none 和 visibility:hidden 有什么区别?”从渲染树的角度回答:
display:none的元素不在渲染树中,不参与布局;visibility:hidden的元素在渲染树中,参与布局但不绘制像素。
渲染树构建完成后,浏览器需要经历三个阶段才能将页面呈现到屏幕上:
Layout(布局 / 回流)
浏览器遍历渲染树,计算每个节点的几何信息——精确的位置(x, y 坐标)和大小(宽度、高度)。这个过程从根节点开始,递归计算每个元素的盒模型(content → padding → border → margin)。
触发回流的常见操作:
width、height、padding、margin)top、left)offsetWidth、scrollTop、getComputedStyle())Paint(绘制 / 重绘)
根据布局计算的几何信息和渲染树中的样式信息,将每个节点绘制为实际的像素。绘制涉及填充颜色、绘制边框、渲染阴影、显示文本和图片等视觉效果。
触发重绘但不触发回流的操作:修改 color、background-color、visibility、box-shadow 等不影响几何信息的样式属性。
Composite(合成)
浏览器将页面分成多个图层(Layer),每个图层独立绘制后,由 GPU 按正确的顺序合成最终画面。某些 CSS 属性(如 transform、opacity、will-change)会让元素提升为独立图层,修改这些属性时只需重新合成,无需回流和重绘,性能最优。
面试提示:性能优化的核心原则——回流的开销 > 重绘的开销 > 合成的开销。尽量使用
transform代替top/left实现动画,使用opacity代替visibility实现淡入淡出,因为前者只触发合成,后者会触发重绘甚至回流。
JavaScript 是影响渲染性能最关键的因素之一。当 HTML 解析器遇到 <script> 标签时,默认行为是暂停 HTML 解析,等待脚本下载并执行完毕后才继续。
为什么要暂停? 因为 JavaScript 可以通过 document.write() 修改 DOM 结构,如果解析器不暂停,可能会导致解析结果与脚本执行后的 DOM 不一致。
为了减少脚本对解析的阻塞,HTML5 引入了 async 和 defer 属性:
| 属性 | HTML 解析 | 下载时机 | 执行时机 | 执行顺序 |
|---|---|---|---|---|
| 无 | 阻塞 | 立即开始下载 | 下载完成后立即执行,阻塞解析 | 按文档顺序 |
async |
不阻塞 | 并行下载 | 下载完成后立即执行,短暂阻塞解析 | 不保证顺序 |
defer |
不阻塞 | 并行下载 | HTML 解析完成后、DOMContentLoaded 事件前执行 | 按文档顺序 |
使用建议:
async,不关心执行顺序,尽快执行即可defer,保证按顺序执行且不阻塞解析<body> 底部,不加属性CSS 与渲染阻塞
CSS 不会阻塞 DOM 解析(HTML 解析器可以继续构建 DOM 树),但会阻塞渲染(浏览器不会在 CSSOM 构建完成前绘制页面)。此外,如果 <script> 标签在 <link> 标签之后,浏览器会等待 CSS 下载完成后才执行脚本,因为脚本可能需要查询元素的样式信息。
面试提示:
asyncvsdefer是前端面试必考题。记住核心区别:async下载完就执行(不保证顺序),defer等 DOM 解析完再按顺序执行。实际项目中defer用得更多,因为大多数脚本需要操作 DOM 且存在依赖关系。
将以上所有步骤串联起来,浏览器的完整渲染管线如下:
flowchart LR
A[HTML] --> B[DOM 树]
C[CSS] --> D[CSSOM 树]
B --> E[渲染树]
D --> E
E --> F[Layout 布局]
F --> G[Paint 绘制]
G --> H[Composite 合成]
H --> I[显示]
整个管线的关键要点:
面试提示:面试中被问到”从输入 URL 到页面渲染经历了什么”时,能从网络层讲到渲染层,最后以渲染管线收尾,展现出对浏览器工作原理的完整理解。重点是理解每个阶段的输入和输出,以及它们之间的依赖关系。
| 阶段 | 核心任务 | 关键协议/技术 |
|---|---|---|
| URL 解析 | 解析协议、域名、端口、路径 | HSTS |
| DNS 解析 | 域名 → IP 地址 | DNS, DoH |
| TCP 连接 | 建立可靠连接 | TCP 三次握手 |
| TLS 握手 | 建立加密通道 | TLS 1.2, ECDHE |
| HTTP 请求 | 发送/接收数据 | HTTP/2 |
| 服务器处理 | 生成响应 | Nginx, gunicorn |
| 浏览器渲染 | 解析并绘制页面 | DOM, CSSOM, GPU 合成 |
浏览器首先对 URL 进行解析,提取协议、域名和路径等信息,随后通过 DNS 将域名解析为 IP 地址。接着与服务器建立 TCP 连接(三次握手),如果是 HTTPS 还需要进行 TLS 握手以建立加密通道。连接建立后,浏览器发送 HTTP 请求,服务器处理请求并返回 HTML 响应。最后,浏览器解析 HTML 构建 DOM 树,解析 CSS 构建 CSSOM 树,两者合并为渲染树,经过布局计算和绘制,最终通过 GPU 合成将页面显示在屏幕上。
Q1:为什么 HTTPS 比 HTTP 慢?如何优化?
HTTPS 多了 TLS 握手环节(1-2 个 RTT),增加了连接建立时间。优化手段包括:开启 TLS 1.3(1-RTT 甚至 0-RTT)、使用 Session Resumption 复用会话、部署 OCSP Stapling 减少证书验证延迟、启用 HTTP/2 多路复用减少连接数。
Q2:什么是回流和重绘?如何减少?
回流(Reflow)是指元素的几何属性(尺寸、位置)发生变化,浏览器需要重新计算布局;重绘(Repaint)是指元素外观改变但不影响布局。回流必然触发重绘,代价更高。减少方法:批量修改 DOM、使用 transform 代替 top/left 动画、避免频繁读取布局属性、使用 will-change 提升合成层。
Q3:HTTP/2 相比 HTTP/1.1 有哪些改进?
核心改进包括:多路复用(一个连接并行传输多个请求)、头部压缩(HPACK 算法)、服务器推送(Server Push)、二进制分帧(替代文本协议)、流优先级控制。这些特性解决了 HTTP/1.1 的队头阻塞问题,大幅提升了页面加载性能。
Q4:DNS 解析慢怎么办?
优化策略包括:使用 dns-prefetch 预解析关键域名、减少页面中的域名数量、选择快速的公共 DNS 服务器(如 1.1.1.1 或 8.8.8.8)、启用 DNS over HTTPS(DoH)提升安全性和稳定性、利用本地 DNS 缓存和 CDN 就近解析。
Q5:首屏优化有哪些手段?
首屏优化是一个综合工程:网络层面使用 CDN、开启 Gzip/Brotli 压缩、利用 HTTP 缓存;资源层面进行代码分割(Code Splitting)、懒加载非关键资源、内联关键 CSS;渲染层面采用 SSR/SSG 减少客户端渲染时间、使用骨架屏提升感知速度、预加载关键字体和图片。