Skip to content

输入 URL 回车到页面加载完成, 发生了什么

  1. 判断地址栏 (调试地址栏: chrome://omnibox) 内容是搜索关键字, 还是请求 URL; 如果是搜索关键字, 则组合为携带搜索关键字的新 URL, 使用默认的搜索引擎; 如果是请求 URL, 则加上 https:// 协议字段, 组合为新 URL
  2. beforeunload 事件, 用户回车后, 会触发 beforeunload 事件, beforeunload 事件允许页面卸载前执行数据清理等操作, 也可以询问用户是否离开当前页面, 用户可以通过 beforeunload 事件取消导航 (页面跳转)
  3. 浏览器进入加载状态, 表现为标签页上的加载图标, 但页面未被替换, 需要等待提交文档阶段, 页面才被替换
  4. 浏览器渲染进程通过进程间通信 (IPC) 将请求 URL 发送给网络进程
  5. 网络进程先检查本地缓存是否缓存了请求资源, 如果有缓存, 则直接返回请求资源给浏览器进程 (强制缓存), 如果没有缓存, 则发送网络请求
  6. DNS 解析: 对 URL 进行 DNS 解析, 以获取服务器 IP 地址和端口号; HTTP 的默认端口号是 80, HTTPS 默认端口号是 443, 如果是 HTTPS 协议, 还需要建立 TLS 或 SSL 连接
  7. 建立 TCP 连接: 进入 TCP 队列, 通过三次握手与服务器建立连接 (进入 TCP 队列: chrome 限制一个域名最多同时建立 6 个 TCP 连接, 如果一个域名同时有 10 个请求, 那么有 4 个请求会排队等待)
  8. 浏览器发送 HTTP 请求: 浏览器生成请求行 (get/push/... 请求方法, URL, 协议), 请求头, 请求体等, 并将 cookie 等数据附加到请求头中, 发送 HTTP 请求给服务器
    • RESTful: get, post, put, delete, patch, ...
    • 应用层: 加 HTTP 头部, 包括请求方法, URL, 协议等
    • 传输层: 加 TCP 头部, 包括源端口号, 目的端口号等
    • 网络层: 加 IP 头部, 包括源 IP 地址, 目的 IP 地址等
  9. 服务器收到 HTTP 请求: 服务器生成响应行, 响应头, 响应体等, 发送 HTTP 响应给浏览器网络进程
    1. 服务器网络层解析出 IP 头部, 将数据包向上交付给传输层
    2. 服务器传输层解析出 TCP 头部, 将数据包向上交付给应用层
    3. 服务器应用层解析出请求头和请求体
    • 如果需要重定向, 则直接返回 301 或 302 状态码, 同时在响应头的 Location 字段中指定重定向地址, 浏览器根据状态码和 Location 字段进行重定向操作
    • 如果不需要重定向, 服务器根据请求头中的 if not match 字段值判断请求资源是否被更新 (协商缓存), 如果没有更新, 则返回 304 状态码, 不返回请求资源; 如果有更新, 则同时返回 200 状态码和请求资源
    • 如果希望使用强缓存, 则设置响应头字段 Cache-Control: Max-age=2000, 例如 nginx 配置文件 add_header Cache-Control "public, immutable"; 对应的响应头字段 Cache-Control: public, immutable
    1. 关于是否断开连接: 数据传输完成, TCP 四次挥手断开连接, 如果浏览器或服务器在 HTTP 头部设置 Connection: Keep-Alive 字段, 则会建立持久的 TCP 连接, 节约下一次 HTTP 请求时建立连接的时间, 提高资源加载速度
    2. 关于重定向: 浏览器收到服务器返回的响应头后, 网络进程解析响应头, 如果状态码是 301 或 302, 则网络进程获取响应头的 Location 字段值 (重定向的地址), 发送新的 HTTP/HTTPS 请求
    3. 关于响应数据类型: 浏览器根据 HTTP 响应头的 Content-Type 字段值判断响应数据类型, 并根据响应数据类型决定如何处理响应体; 如果 Content-Type 字段值是下载类型: Content-Type: application/octet-stream, 则提交给浏览器的下载管理器, 同时该 URL 请求的导航 (页面跳转) 结束, 如果 Content-Type 字段值是 HTML 类型: Content-Type: text/html; charset=utf-8, 则网络进程通知浏览器进程分配一个渲染进程进行页面渲染
  10. 分配渲染进程: 浏览器进程检查新 URL 和已打开 URL 的域名是否相同, 如果相同则复用已有的渲染进程, 如果不同则创建新的渲染进程
  11. 提交文档阶段: 浏览器发送 提交文档 消息给渲染进程, 渲染进程收到消息后, 和网络进程建立数据传输的管道, 文档数据传输完成后, 渲染进程返回 确认提交 消息给浏览器进程, 提交文档 后, 开始解析 DOM, 解析 CSS, 生成渲染树, 绘制并显示页面等
  12. 更新浏览器状态: 浏览器进程收到 确认提交 消息后, 更新浏览器状态: 包括安全状态, 地址栏的 URL, 前进后退的历史状态, 并更新页面, 此时页面是空白页
  13. 渲染文档: 渲染进程解析文档, 加载子资源; HTML 转换为 DOM 树, CSS 转换为 CSSOM 树, DOM 树和 CSSOM 树合并为渲染树; 根据布局, 计算每个节点的位置, 宽高 (回流相关), 颜色 (重绘相关) 等; 绘制并显示页面

HTTP 超文本传输协议

HTTP: C/S 模型, 基于 TCP/IP, 是无状态协议 (两次请求间, 服务器不会保存任何数据)

HTTP/1.1

  • HTTP/1.0 是短连接, 每次 HTTP 请求都需要: 建立 TCP 连接, 传输数据和断开 TCP 连接 3 个阶段; HTTP/1.1 新增持久连接, 特点是一个 TCP 连接上可以发送多次 HTTP 请求, 只要浏览器或服务器没有明确断开连接, 该 TCP 连接就会一直保持; HTTP/1.1 中持久连接默认开启, 如果不想使用持久连接, 可以在 HTTP 请求头中设置 Connection: Close 字段
  • chrome 限制同一个域名最多同时建立 6 个 TCP 连接
  • 使用 CDN 内容分发网络实现域名分段
  • 不成熟的 HTTP 管线化: HTTP/1.1 的管线化是指将多个 HTTP 请求批量发送给服务器, 虽然可以批量发送请求, 但是服务器仍需要根据请求顺序依次响应; TCP 持久连接虽然可以减少连接建立和断开的次数, 但是需要等待当前请求完成后, 才能发送下一个请求; 如果 TCP 通道中某个请求没有及时完成, 则会阻塞后续所有请求 (队头阻塞问题),
  • 支持虚拟主机: HTTP/1.0 中, 一个域名绑定一个唯一的 IP 地址, 一个服务器只能绑定一个域名; 随着虚拟主机技术的发展, 一个物理主机可以虚拟化为多个虚拟主机, 每个虚拟主机有单独的域名, 这些虚拟主机 (域名) 公用同一个 IP 地址; HTTP/1.1 的请求头中增加了 Host 字段, 表示域名 URL 地址, 服务器可以根据不同的 Host 字段, 进行不同的处理
  • 支持动态大小的响应数据: HTTP/1.0 中, 需要在响应头中指定传输数据的大小, 例如 Content-Length: 1024, 这样浏览器可以根据指定的传输数据大小接收数据; 随着服务器技术的发展, 很多页面内容是动态生成的, 数据传输时不清楚传输数据的大小, 导致浏览器不清楚是否已接收完所有的数据, HTTP/1.1通过引入 Chunk Transfer 分块传输机制解决该问题, 服务器将传输数据分割为若干个任意大小的数据块, 每个数据块发送时, 附加上一个数据块的长度, 最后使用一个 0 长度的数据块作为数据发送结束的标志, 提供对动态大小的响应数据的支持
  • 客户端 Cookie, 安全机制: HTTP/1.1 还引入了客户端 Cookie 和安全机制

HTTP/2.0

HTTP/1.1 对带宽的利用率不理想, 原因如下:

  1. TCP 的慢启动: TCP 建立连接后开始发送数据, TCP 先使用较慢的发送速率, 并逐渐增加发送速率, 以探测网络带宽 (合适的发送速率), 直到稳态 (拥塞避免状态); CUBIC 仍使用慢启动, BBR 不使用慢启动, 通过主动测量瓶颈带宽 Bottleneck Bandwidth 和最小 RTT 以动态调整发送速率; 慢启动导致页面首次渲染时间增加
  2. 同时建立多条 TCP 连接时, 这些连接会竞争带宽, 影响关键资源的加载速度
  3. HTTP/1.1 队头阻塞问题: HTTP/1.1 使用持久连接, 虽然多个 HTTP 请求可以公用一个 TCP 管道, 但是同一时刻只能处理一个请求, 当前请求完成前, 后续请求只能阻塞; 例如某个请求耗时 5s, 则后续所有请求都需要排队等待 5s
  4. 协议开销大: header 携带的内容过多, 且不能压缩, 增加了传输成本

HTTP/2.0 实现思路: 一个域名只使用一个 TCP 长连接传输数据, 整个页面资源的加载只需要一次 TCP 慢启动, 同时避免了多个 TCP 连接竞争带宽的问题; HTTP/2.0 实现了资源的并行请求, 可以发送请求给服务器, 而不需要等待其他请求完成;

  1. HTTP 多路复用技术, 引入二进制分帧层, 并行处理请求, 浏览器的请求数据包括请求行, 请求头, 如果是 POST 方法, 还包括请求体; 请求数据传递给二进制分帧层后, 转换为若干个带有请求 ID 编号的帧, 通过 TCP/IP 协议栈发送给服务器, 服务器收到请求帧后, 将所有 ID 相同的帧合并为一个完整的请求, 并处理该请求; 类似的, 服务器的二进制分帧层将响应数据转换为若干个带有响应 ID 编号的帧, 通过 TCP/IP 协议栈发送给浏览器, 浏览器收到响应帧后, 将所有 ID 相同的帧合并为一个完整的响应
  2. 请求优先级: HTTP/2.0 支持请求优先级, 发送请求时, 标记该请求的优先级, 服务器收到请求后, 优先处理优先级高的请求
  3. 服务器推送: HTTP/2.0 服务器推送 (Server Push) 允许客户端请求某个资源 (例如 index.html) 时, 服务器推送其他资源 (例如 style.css, main.js), 不需要客户端再次请求; 可以提高页面加载速度
  4. 头部压缩: HTTP/2.0 对请求头和响应头进行 (gzip) 压缩
  5. 可重置: HTTP/2.0 可以在不中断 TCP 连接的前提下, 取消当前的请求或响应

HTTP/3.0

  1. 随着丢包率的增加, HTTP/2.0 的传输效率降低, 2% 丢包率时, HTTP/2.0 的传输效率可能低于 HTTP/1.1
  2. TCP 三次握手, TLS 一次握手, 浪费 3 到 4 个 RTT

HTTP/3.0 (QUIC, Quick UDP Internet Connection) 基于 UDP, 实现类似 TCP 的多路数据流, 可靠传输等特性

网络模型

  • OSI 七层模型: 应用层, 表示层, 会话层, 传输层, 网络层, 数据链路层, 物理层
  • TCP/IP 五层模型: 应用层, 传输层, 网络层, 数据链路层, 物理层
  • 常见端口号
    • 22: SSH
    • 53: DNS
    • 80: HTTP
    • 443: HTTPS
    • 3306: MySQL
    • 5173: Vite 服务器
    • 5432: PostgreSQL
    • 6379: Redis
    • 8080: Webpack 服务器
    • 8888: Nginx
    • 9200: ElasticSearch
    • 27017 MongoDB

浏览器缓存

浏览器缓存, 也称为客户端缓存

HTTP 缓存是保存资源副本的技术, 复用资源, 减少等待时间, 提高页面性能, 减少网络流量, 降低服务器压力; 浏览器或服务器判断请求的资源已被缓存时, 直接返回; HTTP 缓存分为私有缓存 (浏览器缓存) 和代理缓存 (共享缓存)

浏览器缓存分为强缓存和协商缓存, 强缓存的优先级高于协商缓存

强缓存

强缓存命中时, 不会发送请求到服务器, 直接从客户端缓存中获取资源, 返回状态码 200(from memory|disk cache), 强缓存使用 Cache-ControlExpires 两个字段, Cache-Control 的优先级高于 Expires

  • Cache-Control: max-age=161043261
  • Expires: Mon Jan 01 2025 04:32:51 GMT+0800 (GMT+8:00)

协商缓存 (对比缓存)

协商缓存会发送请求到服务器, 服务器根据请求头的 Last-Modified/If-Modified-SinceETag/If-None-Match 两对字段判断协商缓存是否命中, If-None-Match/ETag 的优先级高于 If-Modified-Since/Last-Modified, 如果命中, 服务器返回 304 Not Modified, 响应体为空; 如果未命中, 服务器返回 200 OK, 响应体中携带更新的资源

  • 先试图命中强缓存, 再试图命中协商缓存
  • 强缓存和协商缓存的相同点: 如果命中, 都从客户端缓存中加载资源
  • 强缓存和协商缓存的不同点: 强缓存不会发送请求到服务器, 协商缓存会发送请求到服务器

TCP

  • TCP 是面向连接的, 可靠的, 基于字节流的传输层协议
  • UDP 是无连接的, 不可靠的, 基于数据报的传输层协议
  1. 数据分段: 数据在发送端分段, 在接收端重组, TCP 确定分段的大小, 控制分段和重组
  2. 到达确认: 接收端收到分段后, 返回发送端一个确认, 确认号等于分段序号 +1
  3. 流量控制, 拥塞控制
  • 发送端的发送窗口
  • 接收端的接收窗口
  1. 失序处理: TCP 对收到的分段排序
  2. 重复处理: TCP 丢弃重复的分段
  3. 数据校验: TCP 使用首部校验和, 丢弃错误的分段

三次握手常见问题

  1. 为什么要三次握手, 两次握手不可以吗

两次握手是最基本的; 三次握手中, 客户端向服务器握手两次, 可以防止已失效的连接请求发送到服务器, 导致服务器资源的浪费

  1. 如果连接已建立, 客户端突然故障了怎么办

TCP 有一个保活计时器 (通常是 2h), 服务器每次收到客户端的请求后, 都会重置保活计时器; 如果 2h 内未收到客户端的请求, 服务器会每隔 75s 发送一个探测包, 如果连续发送 10 个探测包后仍未收到客户端的响应, 则服务器判断客户端故障, 关闭 TCP 连接

四次挥手常见问题

  1. 为什么建立连接握手三次, 而断开连接挥手四次

建立连接时, 第二次握手时, 服务器将 ACK 和 SYN 合并发送给客户端, 可以少一次握手

断开连接时, 第一次挥手时, 服务器收到客户端的 FIN=1, 仅表示客户端不再发送数据, 但仍可以接收数据; 第二次挥手时, 服务器可能有剩余数据未发送, 需要 FIN_WAIT_2 发送剩余数据和第三次挥手, 通知客户端剩余数据发送完, 服务器将 ACK 和 FIN 分开发送给客户端

  1. 为什么客户端最后需要等待 TIME-WAIT (2MSL)
    1. MSL, Maximum Segment Lifetime 最大分段寿命, 是一个 TCP 包在网络中最长存活时间, 不是固定值
    2. 第三次挥手时, 服务器发送 ACK=FIN=1 (可能丢失), 希望收到客户端的响应 ACK
    3. 第三次挥手后, 客户端收到服务器发送的 ACK=FIN=1, 返回一个没有数据的响应 ACK (也可能丢失)
    4. 服务器一个 MSL 后, 没有收到客户端的响应 ACK, 则会重新发送一次 ACK=FIN=1, 客户端可以在 2MSL 内收到服务器重新发送的 ACK=FIN=1; 客户端收到服务器重新发送的 ACK=FIN=1 后, 重置 2MSL 计时器

TCP, UDP 对比

TCPUDP
面向连接无连接
点对点一对一, 一对多, 多对一, 多对多
字节流数据报
有序无序
流量控制, 拥塞控制
可靠不可靠