~/ ?.log $
返回文章列表
13 min read
更新于 2026年3月6日

我不知道的HTTP(01)— 从输入URL到页面呈现

面试中有一道经典题:"从输入 URL 到页面呈现,中间发生了什么?"

一、问题引入

面试中有一道经典题:“从输入 URL 到页面呈现,中间发生了什么?”

很多人以为答案就是”浏览器发请求,服务器返回 HTML,渲染”,三步结束。但实际上,在第一个字节的 HTTP 数据发出之前,浏览器已经悄悄完成了至少四次网络交互——DNS 查询、TCP 握手、TLS 握手,中间还可能穿插 CDN 的智能调度。那条看似瞬间完成的”请求”,背后是一条由多个阶段串联起来的完整链路。

二、表面认知:大多数人停在这里

让一个前端开发者描述这个过程,回答大概率是:

“浏览器发 HTTP 请求,服务器返回 HTML,解析渲染。”

这不算错,但它把”发 HTTP 请求”之前的所有步骤全跳过了。一个更完整的链路是:

URL 解析 → DNS 查询 → TCP 三次握手 → TLS 握手 → HTTP 请求/响应 → 渲染

前四步全部发生在”发 HTTP 请求”之前。换句话说,“输入 URL 后直接发 HTTP 请求”是一个非常普遍的认知盲区——浏览器发出的第一个网络包,是 DNS 查询包,不是 HTTP 请求包。

在进入具体流程之前,有一个前置知识需要简单交代——网络分层模型。

说白了,网络分层就是把”数据从 A 到 B”这件事拆成几个独立环节,每层只管自己的事。教科书里有 OSI 七层和 TCP/IP 四层两个模型,实际开发中 TCP/IP 四层是真正在用的。

TCP/IP 四层大致对应 OSI 层关键协议
应用层应用 + 表示 + 会话HTTP、DNS、TLS
传输层传输层TCP、UDP
网络层网络层IP、ICMP
链路层数据链路 + 物理层Ethernet、Wi-Fi

换句话说,OSI 和 TCP/IP 的根本区别在于:OSI 是理论参考模型,TCP/IP 是互联网实际运行的协议栈。面试能说清楚这个区别就够了,不需要把七层全背下来。

下面进入正题。

三、往下挖:逐步拆解请求链路

3.1 URL 解析

浏览器拿到输入内容后,先判断这是一个 URL 还是搜索关键词。如果识别为 URL,解析出协议(https)、域名(www.example.com)、端口(HTTPS 默认 443)、路径和查询参数。

这一步决定了后续走 HTTP 还是 HTTPS、默认端口还是自定义端口。如果 URL 是 http:// 而非 https://,后面就不会有 TLS 握手这一步。

3.2 DNS 解析:域名到 IP 的翻译

域名是给人看的,网络通信需要的是 IP 地址。DNS 解析的任务就是把域名翻译成 IP。

这里有一个很多人会忽略的细节——DNS 查询并非每次都要跑到远端 DNS 服务器。在那之前,它会依次检查多级缓存,命中任何一级就立即返回:

(1) 浏览器自身的 DNS 缓存。Chrome 可以在 chrome://net-internals/#dns 查看当前缓存内容和 TTL。

(2) 操作系统缓存。包括 /etc/hosts 文件中的静态映射和系统级 DNS 缓存(macOS 用 dscacheutil,Windows 用 ipconfig /displaydns 可查看)。

(3) 路由器缓存。家用路由器通常会缓存最近的查询结果。

(4) ISP(运营商)本地 DNS 服务器的缓存。

只有全部未命中,才会发起递归查询:本地 DNS 服务器依次向根域名服务器(.)→ 顶级域名服务器(.com)→ 权威 DNS 服务器请求,最终拿到目标 IP。

问题的关键在于——如果网站接入了 CDN,DNS 返回的不是源站 IP,而是一条 CNAME 记录,指向 CDN 厂商的域名(如 xxx.cdnprovider.com)。CDN 的智能 DNS 再根据用户的地理位置和网络状况,返回距离最近的边缘节点 IP。也就是说,CDN 的介入点就在 DNS 解析这一步,对浏览器完全透明——浏览器以为自己在直连目标服务器,实际上连接的是附近的 CDN 节点。

3.3 数据在网络上怎么走

拿到 IP 地址后,数据包从本机到目标服务器需要经过若干网络设备。这里涉及两种转发方式。

在同一个局域网内(比如电脑到路由器),数据包通过 MAC 地址直接转发,这叫二层转发,工作在链路层。一旦跨越网络边界(比如跨运营商、跨城市),路由器就需要根据 IP 地址查找路由表决定下一跳,这叫三层路由,工作在网络层。

说白了,二层转发和三层路由的区别就是:二层靠 MAC 地址在”邻居”之间直接递,三层靠 IP 地址在不同网络之间接力传递。前端开发者通常不需要关心这一层,但理解它有助于排查”本地能通、线上不通”一类的网络问题。

3.4 TCP 三次握手:不只是”确认网络通不通”

拿到 IP 后,浏览器需要和服务器建立 TCP 连接。TCP 是可靠传输协议,而”可靠”的前提是双方先同步各自的初始序列号(ISN)。这就是三次握手的核心目的。

很多人以为三次握手是为了”确认网络通不通”,但实际上它的核心目标是序列号同步

第一次:客户端发送 SYN 包,携带自己的初始序列号(如 seq=100),进入 SYN_SENT 状态。

第二次:服务端回复 SYN+ACK 包,确认客户端序列号(ack=101),同时携带自己的初始序列号(seq=300),进入 SYN_RECV 状态。

第三次:客户端确认服务端序列号(ack=301),双方进入 ESTABLISHED 状态,连接建立。

有了序列号,后续传输中每个数据包才能被正确排序和确认,丢包才能被发现并重传。为什么不能两次就完事?因为两次握手无法让客户端确认”服务端知道我的序列号”这件事——一个已失效的旧 SYN 包抵达服务端后,服务端会误以为是新连接请求并分配资源,造成资源浪费。

3.5 TLS 握手:加密通道的建立

如果是 HTTPS(现在绝大多数网站都是),TCP 连接建立后还不能直接发 HTTP 请求。浏览器和服务器之间需要再完成一次 TLS 握手,协商加密算法、交换密钥。

TLS 1.2 需要额外 2 个 RTT(往返时间),TLS 1.3 优化到了 1 个 RTT,甚至支持 0-RTT 恢复——复用上次会话的密钥直接加密发送数据。

到这里可以算一笔账:一个全新的 HTTPS 请求,在发出第一个字节的 HTTP 数据之前,至少需要 1 RTT(DNS)+ 1 RTT(TCP)+ 12 RTT(TLS)= 34 个 RTT。DNS 缓存命中能省掉一个,TLS 1.3 比 1.2 少一个。这就是首次访问一个新网站总比后续访问慢的根本原因。

3.6 HTTP 请求与响应

TLS 握手完成,终于轮到 HTTP 登场。浏览器构造请求报文:

GET /index.html HTTP/1.1
Host: www.example.com
User-Agent: Chrome/120.0
Accept: text/html
Cookie: session_id=abc123

服务器(或 CDN 边缘节点)处理请求后返回响应。如果 CDN 缓存命中,边缘节点直接返回内容,不需要回源到原始服务器——这也是 CDN 加速的核心逻辑:把内容放到离用户更近的节点,减少网络往返次数

浏览器收到响应后,根据 Content-Type 判断数据类型。如果是 text/html,将 HTML 交给渲染进程,开始解析和渲染。从这里开始,就进入了浏览器渲染流水线的范畴,不在本篇讨论范围内。

四、这意味着什么:用代码观察全链路

理解这条链路不只是为了面试能答上来。浏览器提供了 PerformanceNavigationTiming API,可以精确测量每个阶段的耗时。

下面这段代码可以直接在浏览器控制台运行,它会打印出从 DNS 查询到页面加载完成的各阶段毫秒数:

const entry = performance.getEntriesByType('navigation')[0];

const stages = {
  'DNS 查询': entry.domainLookupEnd - entry.domainLookupStart,
  'TCP 连接': entry.connectEnd - entry.connectStart,
  'TLS 握手': entry.secureConnectionStart > 0 ? entry.connectEnd - entry.secureConnectionStart : 0,
  'TTFB(首字节)': entry.responseStart - entry.requestStart,
  内容下载: entry.responseEnd - entry.responseStart,
  'DOM 解析': entry.domInteractive - entry.responseEnd,
  总耗时: entry.loadEventEnd - entry.startTime,
};

console.table(stages);

典型输出中,DNS 查询在缓存命中时几乎为 0,TLS 握手通常在 30-80ms,内容下载时间取决于资源体积和带宽。如果 DNS 或 TCP 阶段出现异常高的数值,往往意味着网络链路上的某个环节存在瓶颈。

如果你只记住一句话,记住这个:performance.getEntriesByType('navigation')[0] 是观察网络请求全链路耗时的最直接手段,比 Network 面板的瀑布图还精确——它给出的是毫秒级数值,而非可视化近似。

再对比一下旧的 performance.timing API(已标记为 deprecated,但仍可运行):

const t = performance.timing;

console.log('DNS:', t.domainLookupEnd - t.domainLookupStart, 'ms');
console.log('TCP:', t.connectEnd - t.connectStart, 'ms');
console.log('TTFB:', t.responseStart - t.requestStart, 'ms');

两者的区别在于:performance.timing 返回绝对时间戳(从 1970 年起的毫秒数),计算耗时需要手动做差;PerformanceNavigationTiming 返回相对于导航开始的高精度时间,使用更方便也更准确。新项目应该用后者。

五、总结

从输入 URL 到发出 HTTP 请求,中间至少经历了 DNS 查询、TCP 握手、TLS 握手三次网络往返——这些”隐形成本”才是首次加载慢的根本原因。


本系列其他文章:

相关主题:

share.ts

// 觉得这篇文章有帮助?

// 欢迎分享给更多人

export const  subscribe  =  "/rss.xml" ;