网络与缓存
HTTP 版本演进
| 版本 | 关键改进 | 遗留问题 |
|---|---|---|
| 1.0 | 每个请求一个 TCP 连接 | 连接无法复用,开销极大 |
| 1.1 | 持久连接(Connection: keep-alive)、管道化、Host 头、缓存控制 | 队头阻塞(HOL):同一连接上的请求必须按序响应 |
| 2.0 | 二进制分帧、多路复用、头部压缩(HPACK)、服务端推送、流优先级 | TCP 层的队头阻塞仍存在:丢包会阻塞该连接上所有流 |
| 3.0 | 底层换成 QUIC(基于 UDP),彻底解决 TCP 队头阻塞,连接迁移,0-RTT 握手 | 普及度和中间设备兼容性 |
HTTP/1.1 的队头阻塞怎么绕过 —— 浏览器对同一域名开 6~8 个并发连接;再不够就域名分片(把资源分散到多个子域名)。注意在 HTTP/2 下域名分片是反优化 —— 它会破坏多路复用,还要多做几次 TLS 握手。
HTTP/2 的多路复用 —— 一个 TCP 连接上可以并行传输多个请求响应,通过 stream ID 区分帧。所以 HTTP/2 下不需要雪碧图、不需要合并文件、不需要域名分片,这些 1.1 时代的优化手段都失效或变成负优化。
为什么 HTTP/2 还有队头阻塞 —— 应用层解决了,但 TCP 层没有:TCP 保证按序交付,一个包丢了,后面的包即使收到也要等重传。HTTP/2 把所有流放在一个 TCP 连接上,反而放大了这个问题。HTTP/3 用 QUIC(UDP + 自己实现的可靠传输)让每个流独立,丢包只影响该流。
🔥 缓存策略
两层:强缓存 → 协商缓存。
请求资源
↓
有缓存且强缓存未过期? ──是──→ 直接用本地缓存(200 from memory/disk cache,不发请求)
↓ 否
发请求带上校验信息(If-None-Match / If-Modified-Since)
↓
服务器判断资源是否变化
↓
没变 → 304 Not Modified(不返回 body)→ 用本地缓存
变了 → 200 + 新内容 + 新的缓存头
强缓存
| 头 | 说明 |
|---|---|
Expires | HTTP/1.0,绝对时间。依赖客户端时间,不可靠 |
Cache-Control: max-age=31536000 | HTTP/1.1,相对时间(秒),优先级高于 Expires |
Cache-Control 的常用值:
max-age=N—— N 秒内直接用缓存no-cache—— ⚠️ 不是「不缓存」,而是「每次都要去服务器验证」(走协商缓存)no-store—— 真正的不缓存,任何地方都不存private—— 只允许浏览器缓存(默认)public—— 允许 CDN 等中间代理缓存immutable—— 配合长max-age,告诉浏览器「永不变更」,即使用户刷新也不发验证请求s-maxage=N—— 专门给代理/CDN 的过期时间,覆盖max-age
no-cache 和 no-store 的区别是高频考点,很多人答反。
协商缓存
| 头(响应 → 请求) | 说明 |
|---|---|
Last-Modified → If-Modified-Since | 基于修改时间,精度只到秒;文件内容没变但时间戳变了也会失效 |
ETag → If-None-Match | 基于内容哈希,精确。优先级高于 Last-Modified |
ETag 的代价 —— 服务器要计算哈希(大文件有开销),且多台服务器生成的 ETag 可能不一致(导致缓存失效),需要统一生成策略。
🔥 实践中的缓存方案
HTML → Cache-Control: no-cache(每次验证,保证能拿到新版本)
JS / CSS / 图片 → Cache-Control: max-age=31536000, immutable
+ 文件名带内容哈希(main.a1b2c3.js)
核心思路:带哈希的静态资源永久缓存,入口 HTML 不缓存。内容变了文件名就变,自然会请求新文件,根本不需要缓存失效机制。这就是所有现代构建工具都往文件名里加哈希的原因。
刷新行为的差异(面试常问):
| 操作 | 效果 |
|---|---|
| 地址栏回车 / 点链接 | 正常走缓存流程(强缓存生效) |
| F5 / 普通刷新 | 跳过强缓存,直接走协商缓存(请求带 Cache-Control: max-age=0) |
| Ctrl+F5 / 硬性重新加载 | 跳过所有缓存(请求带 Cache-Control: no-cache) |
memory cache vs disk cache —— 内存缓存读取快但随标签页关闭失效,通常存小文件和当前页用到的资源;磁盘缓存持久化。浏览器自行决定,开发者无法控制。
HTTPS
TLS 握手流程(RSA 密钥交换,简化版):
1. Client Hello —— 支持的 TLS 版本、加密套件列表、客户端随机数
2. Server Hello —— 选定的版本和套件、服务端随机数、证书(含公钥)
3. 客户端验证证书(CA 链 → 域名 → 有效期 → 吊销状态)
4. 客户端生成 pre-master secret,用服务端公钥加密发送
5. 双方用三个随机数算出相同的会话密钥(对称密钥)
6. 之后用对称加密通信
为什么用「非对称 + 对称」混合 —— 非对称加密慢(RSA 比 AES 慢几百倍),只用它交换对称密钥;实际数据传输用快的对称加密。这是 HTTPS 设计的核心取舍。
为什么需要 CA 证书 —— 光有公钥无法防中间人:攻击者可以把自己的公钥冒充服务器的。CA 用它的私钥给「域名 + 公钥」签名,浏览器内置了受信任的 CA 根证书,可以验证这个签名。CA 解决的是「公钥归属」的信任问题。
TLS 1.3 的改进 —— 握手从 2-RTT 降到 1-RTT(会话恢复时 0-RTT)、废除不安全的算法(RSA 密钥交换、SHA-1、RC4)、强制前向保密(只用 ECDHE)。
HTTP 和 HTTPS 的区别:加密(防窃听)、身份认证(防冒充)、完整性校验(防篡改)。代价是握手延迟和少量 CPU(现代硬件下基本可忽略)。
HSTS —— Strict-Transport-Security 头告诉浏览器「以后只用 HTTPS 访问本站」,防止首次 HTTP 请求被劫持(SSL 剥离攻击)。
跨域
同源策略:协议、域名、端口三者完全相同才算同源。它限制的是读取响应,不是发送请求 —— 跨域请求实际上发出去了,只是浏览器拦截了响应。
:::warning 一个常见误解 「跨域请求没发出去」是错的。简单请求会真的发到服务器并执行,只是浏览器不把响应给 JS。所以跨域不能防 CSRF —— 请求已经带着 Cookie 执行了。 :::
CORS
| 类型 | 条件 | 行为 |
|---|---|---|
| 简单请求 | 方法是 GET/POST/HEAD,且只用安全的头,Content-Type 限于三种(text/plain、multipart/form-data、application/x-www-form-urlencoded) | 直接发,响应头里有 Access-Control-Allow-Origin 才给 JS |
| 预检请求 | 其他情况(如 Content-Type: application/json、自定义头、PUT/DELETE) | 先发 OPTIONS 问服务器允不允许,通过了才发真实请求 |
所以发 JSON 的 POST 会触发预检 —— 这是前端最常遇到的跨域场景。
关键响应头:
Access-Control-Allow-Origin: https://a.com # 不能是 * 如果要带 Cookie
Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Allow-Credentials: true # 允许带 Cookie
Access-Control-Max-Age: 86400 # 预检结果缓存时长,减少 OPTIONS 请求
Access-Control-Expose-Headers: X-Total-Count # 允许 JS 读取的自定义响应头
带 Cookie 的跨域有两个硬要求:前端设 credentials: 'include',后端 Allow-Origin 必须是具体域名不能是 * 且 Allow-Credentials: true。
其他跨域方案
| 方案 | 说明 |
|---|---|
| 代理(Nginx / dev server) | 生产最常用。前端请求同源的路径,服务端转发。彻底绕过同源策略 |
| JSONP | 利用 <script> 不受同源限制。只支持 GET,有 XSS 风险,已基本淘汰 |
postMessage | iframe / 窗口间通信,必须校验 e.origin |
document.domain | 只能用于主域相同的子域,已被现代浏览器废弃 |
| WebSocket | 协议本身不受同源策略限制(但服务端应校验 Origin) |
安全
XSS(跨站脚本) —— 注入并执行恶意脚本。三类:
- 存储型 —— 恶意脚本存进数据库,所有访问者都中招(如评论区)
- 反射型 —— 脚本在 URL 参数里,服务器原样返回
- DOM 型 —— 纯前端,如把
location.hash塞进innerHTML
防御:
- 输出转义(最根本)—— 把
< > & " '转成实体。React/Vue 默认就做了,所以用框架天然防大部分 XSS - 不用
innerHTML/dangerouslySetInnerHTML/v-html,必须用就先过 DOMPurify HttpOnlyCookie —— 即使中招也偷不到会话- CSP(内容安全策略) ——
Content-Security-Policy: script-src 'self',从根上禁止执行内联脚本和外域脚本
CSRF(跨站请求伪造) —— 诱导已登录用户的浏览器向目标站点发请求。浏览器会自动带上 Cookie,所以请求看起来是合法的。
防御:
SameSite=Lax或Strict(现代浏览器默认Lax,已大幅缓解)- CSRF Token —— 服务端下发随机 token,前端放在请求头或表单里。攻击者跨站读不到这个 token
- 校验
Origin/Referer - 关键操作二次验证(密码、短信码)
XSS 和 CSRF 的本质区别:XSS 是在你的站点上执行了攻击者的代码(信任了用户输入);CSRF 是在攻击者的站点上发起了对你的请求(信任了浏览器自动携带的凭证)。XSS 可以用来绕过 CSRF 防御(能执行脚本就能读到 token),所以 XSS 更严重。
点击劫持(Clickjacking) —— 用透明 iframe 覆盖诱导点击。防御:X-Frame-Options: DENY 或 CSP 的 frame-ancestors。
TCP 基础
三次握手:SYN → SYN+ACK → ACK。
为什么不是两次 —— 服务端无法确认客户端是否收到了自己的响应。如果一个延迟很久的旧 SYN 到达服务端,服务端会建立一个客户端并不需要的连接,浪费资源。
四次挥手:FIN → ACK → FIN → ACK。
为什么挥手要四次 —— TCP 是全双工的,关闭要双向各做一次。一方发完 FIN 只表示「我没数据要发了」,对方可能还有数据要发,所以先 ACK 确认,发完数据再发自己的 FIN。
TIME_WAIT 为什么要等 2MSL —— ① 确保最后那个 ACK 能到达对方(丢了对方会重发 FIN);② 让本次连接的所有报文在网络中消失,避免干扰后续使用相同端口的新连接。
TCP 和 UDP —— TCP 面向连接、可靠、有序、有流控和拥塞控制,开销大;UDP 无连接、不保证可靠和顺序,但快且支持广播。实时音视频、DNS、QUIC 用 UDP。