输入 URL 回车到页面加载完成, 发生了什么
- 判断地址栏 (调试地址栏: chrome://omnibox) 内容是搜索关键字, 还是请求 URL; 如果是搜索关键字, 则组合为携带搜索关键字的新 URL, 使用默认的搜索引擎; 如果是请求 URL, 则加上
https://
协议字段, 组合为新 URL - beforeunload 事件, 用户回车后, 会触发 beforeunload 事件, beforeunload 事件允许页面卸载前执行数据清理等操作, 也可以询问用户是否离开当前页面, 用户可以通过 beforeunload 事件取消导航 (页面跳转)
- 浏览器进入加载状态, 表现为标签页上的加载图标, 但页面未被替换, 需要等待提交文档阶段, 页面才被替换
- 浏览器渲染进程通过进程间通信 (IPC) 将请求 URL 发送给网络进程
- 网络进程先检查本地缓存是否缓存了请求资源, 如果有缓存, 则直接返回请求资源给浏览器进程 (强制缓存), 如果没有缓存, 则发送网络请求
- DNS 解析: 对 URL 进行 DNS 解析, 以获取服务器 IP 地址和端口号; HTTP 的默认端口号是 80, HTTPS 默认端口号是 443, 如果是 HTTPS 协议, 还需要建立 TLS 或 SSL 连接
- 建立 TCP 连接: 进入 TCP 队列, 通过三次握手与服务器建立连接 (进入 TCP 队列: chrome 限制一个域名最多同时建立 6 个 TCP 连接, 如果一个域名同时有 10 个请求, 那么有 4 个请求会排队等待)
- 浏览器发送 HTTP 请求: 浏览器生成请求行 (get/push/... 请求方法, URL, 协议), 请求头, 请求体等, 并将 cookie 等数据附加到请求头中, 发送 HTTP 请求给服务器
- RESTful: get, post, put, delete, patch, ...
- 应用层: 加 HTTP 头部, 包括请求方法, URL, 协议等
- 传输层: 加 TCP 头部, 包括源端口号, 目的端口号等
- 网络层: 加 IP 头部, 包括源 IP 地址, 目的 IP 地址等
- 服务器收到 HTTP 请求: 服务器生成响应行, 响应头, 响应体等, 发送 HTTP 响应给浏览器网络进程
- 服务器网络层解析出 IP 头部, 将数据包向上交付给传输层
- 服务器传输层解析出 TCP 头部, 将数据包向上交付给应用层
- 服务器应用层解析出请求头和请求体
- 如果需要重定向, 则直接返回 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
- 关于是否断开连接: 数据传输完成, TCP 四次挥手断开连接, 如果浏览器或服务器在 HTTP 头部设置
Connection: Keep-Alive
字段, 则会建立持久的 TCP 连接, 节约下一次 HTTP 请求时建立连接的时间, 提高资源加载速度 - 关于重定向: 浏览器收到服务器返回的响应头后, 网络进程解析响应头, 如果状态码是 301 或 302, 则网络进程获取响应头的
Location
字段值 (重定向的地址), 发送新的 HTTP/HTTPS 请求 - 关于响应数据类型: 浏览器根据 HTTP 响应头的
Content-Type
字段值判断响应数据类型, 并根据响应数据类型决定如何处理响应体; 如果Content-Type
字段值是下载类型:Content-Type: application/octet-stream
, 则提交给浏览器的下载管理器, 同时该 URL 请求的导航 (页面跳转) 结束, 如果Content-Type
字段值是 HTML 类型:Content-Type: text/html; charset=utf-8
, 则网络进程通知浏览器进程分配一个渲染进程进行页面渲染
- 分配渲染进程: 浏览器进程检查新 URL 和已打开 URL 的域名是否相同, 如果相同则复用已有的渲染进程, 如果不同则创建新的渲染进程
- 提交文档阶段: 浏览器发送
提交文档
消息给渲染进程, 渲染进程收到消息后, 和网络进程建立数据传输的管道, 文档数据传输完成后, 渲染进程返回确认提交
消息给浏览器进程,提交文档
后, 开始解析 DOM, 解析 CSS, 生成渲染树, 绘制并显示页面等 - 更新浏览器状态: 浏览器进程收到
确认提交
消息后, 更新浏览器状态: 包括安全状态, 地址栏的 URL, 前进后退的历史状态, 并更新页面, 此时页面是空白页 - 渲染文档: 渲染进程解析文档, 加载子资源; 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 对带宽的利用率不理想, 原因如下:
- TCP 的慢启动: TCP 建立连接后开始发送数据, TCP 先使用较慢的发送速率, 并逐渐增加发送速率, 以探测网络带宽 (合适的发送速率), 直到稳态 (拥塞避免状态); CUBIC 仍使用慢启动, BBR 不使用慢启动, 通过主动测量瓶颈带宽 Bottleneck Bandwidth 和最小 RTT 以动态调整发送速率; 慢启动导致页面首次渲染时间增加
- 同时建立多条 TCP 连接时, 这些连接会竞争带宽, 影响关键资源的加载速度
- HTTP/1.1 队头阻塞问题: HTTP/1.1 使用持久连接, 虽然多个 HTTP 请求可以公用一个 TCP 管道, 但是同一时刻只能处理一个请求, 当前请求完成前, 后续请求只能阻塞; 例如某个请求耗时 5s, 则后续所有请求都需要排队等待 5s
- 协议开销大: header 携带的内容过多, 且不能压缩, 增加了传输成本
HTTP/2.0 实现思路: 一个域名只使用一个 TCP 长连接传输数据, 整个页面资源的加载只需要一次 TCP 慢启动, 同时避免了多个 TCP 连接竞争带宽的问题; HTTP/2.0 实现了资源的并行请求, 可以发送请求给服务器, 而不需要等待其他请求完成;
- HTTP 多路复用技术, 引入二进制分帧层, 并行处理请求, 浏览器的请求数据包括请求行, 请求头, 如果是 POST 方法, 还包括请求体; 请求数据传递给二进制分帧层后, 转换为若干个带有请求 ID 编号的帧, 通过 TCP/IP 协议栈发送给服务器, 服务器收到请求帧后, 将所有 ID 相同的帧合并为一个完整的请求, 并处理该请求; 类似的, 服务器的二进制分帧层将响应数据转换为若干个带有响应 ID 编号的帧, 通过 TCP/IP 协议栈发送给浏览器, 浏览器收到响应帧后, 将所有 ID 相同的帧合并为一个完整的响应
- 请求优先级: HTTP/2.0 支持请求优先级, 发送请求时, 标记该请求的优先级, 服务器收到请求后, 优先处理优先级高的请求
- 服务器推送: HTTP/2.0 服务器推送 (Server Push) 允许客户端请求某个资源 (例如 index.html) 时, 服务器推送其他资源 (例如 style.css, main.js), 不需要客户端再次请求; 可以提高页面加载速度
- 头部压缩: HTTP/2.0 对请求头和响应头进行 (gzip) 压缩
- 可重置: HTTP/2.0 可以在不中断 TCP 连接的前提下, 取消当前的请求或响应
HTTP/3.0
- 随着丢包率的增加, HTTP/2.0 的传输效率降低, 2% 丢包率时, HTTP/2.0 的传输效率可能低于 HTTP/1.1
- 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-Control
和 Expires
两个字段, 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-Since
和 ETag/If-None-Match
两对字段判断协商缓存是否命中, If-None-Match/ETag
的优先级高于 If-Modified-Since/Last-Modified
, 如果命中, 服务器返回 304 Not Modified, 响应体为空; 如果未命中, 服务器返回 200 OK, 响应体中携带更新的资源
- 先试图命中强缓存, 再试图命中协商缓存
- 强缓存和协商缓存的相同点: 如果命中, 都从客户端缓存中加载资源
- 强缓存和协商缓存的不同点: 强缓存不会发送请求到服务器, 协商缓存会发送请求到服务器
TCP
- TCP 是面向连接的, 可靠的, 基于字节流的传输层协议
- UDP 是无连接的, 不可靠的, 基于数据报的传输层协议
- 数据分段: 数据在发送端分段, 在接收端重组, TCP 确定分段的大小, 控制分段和重组
- 到达确认: 接收端收到分段后, 返回发送端一个确认, 确认号等于分段序号 +1
- 流量控制, 拥塞控制
- 发送端的发送窗口
- 接收端的接收窗口
- 失序处理: TCP 对收到的分段排序
- 重复处理: TCP 丢弃重复的分段
- 数据校验: TCP 使用首部校验和, 丢弃错误的分段
三次握手常见问题
- 为什么要三次握手, 两次握手不可以吗
两次握手是最基本的; 三次握手中, 客户端向服务器握手两次, 可以防止已失效的连接请求发送到服务器, 导致服务器资源的浪费
- 如果连接已建立, 客户端突然故障了怎么办
TCP 有一个保活计时器 (通常是 2h), 服务器每次收到客户端的请求后, 都会重置保活计时器; 如果 2h 内未收到客户端的请求, 服务器会每隔 75s 发送一个探测包, 如果连续发送 10 个探测包后仍未收到客户端的响应, 则服务器判断客户端故障, 关闭 TCP 连接
四次挥手常见问题
- 为什么建立连接握手三次, 而断开连接挥手四次
建立连接时, 第二次握手时, 服务器将 ACK 和 SYN 合并发送给客户端, 可以少一次握手
断开连接时, 第一次挥手时, 服务器收到客户端的 FIN=1, 仅表示客户端不再发送数据, 但仍可以接收数据; 第二次挥手时, 服务器可能有剩余数据未发送, 需要 FIN_WAIT_2 发送剩余数据和第三次挥手, 通知客户端剩余数据发送完, 服务器将 ACK 和 FIN 分开发送给客户端
- 为什么客户端最后需要等待 TIME-WAIT (2MSL)
- MSL, Maximum Segment Lifetime 最大分段寿命, 是一个 TCP 包在网络中最长存活时间, 不是固定值
- 第三次挥手时, 服务器发送 ACK=FIN=1 (可能丢失), 希望收到客户端的响应 ACK
- 第三次挥手后, 客户端收到服务器发送的 ACK=FIN=1, 返回一个没有数据的响应 ACK (也可能丢失)
- 服务器一个 MSL 后, 没有收到客户端的响应 ACK, 则会重新发送一次 ACK=FIN=1, 客户端可以在 2MSL 内收到服务器重新发送的 ACK=FIN=1; 客户端收到服务器重新发送的 ACK=FIN=1 后, 重置 2MSL 计时器
TCP, UDP 对比
TCP | UDP |
---|---|
面向连接 | 无连接 |
点对点 | 一对一, 一对多, 多对一, 多对多 |
字节流 | 数据报 |
有序 | 无序 |
流量控制, 拥塞控制 | 无 |
可靠 | 不可靠 |
慢 | 快 |