跳到主要内容

网络与缓存

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 + 新内容 + 新的缓存头

强缓存​

头说明
ExpiresHTTP/1.0,绝对时间。依赖客户端时间,不可靠
Cache-Control: max-age=31536000HTTP/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 风险,已基本淘汰
postMessageiframe / 窗口间通信,必须校验 e.origin
document.domain只能用于主域相同的子域,已被现代浏览器废弃
WebSocket协议本身不受同源策略限制(但服务端应校验 Origin)

安全​

XSS(跨站脚本) —— 注入并执行恶意脚本。三类:

  • 存储型 —— 恶意脚本存进数据库,所有访问者都中招(如评论区)
  • 反射型 —— 脚本在 URL 参数里,服务器原样返回
  • DOM 型 —— 纯前端,如把 location.hash 塞进 innerHTML

防御:

  1. 输出转义(最根本)—— 把 < > & " ' 转成实体。React/Vue 默认就做了,所以用框架天然防大部分 XSS
  2. 不用 innerHTML / dangerouslySetInnerHTML / v-html,必须用就先过 DOMPurify
  3. HttpOnly Cookie —— 即使中招也偷不到会话
  4. CSP(内容安全策略) —— Content-Security-Policy: script-src 'self',从根上禁止执行内联脚本和外域脚本

CSRF(跨站请求伪造) —— 诱导已登录用户的浏览器向目标站点发请求。浏览器会自动带上 Cookie,所以请求看起来是合法的。

防御:

  1. SameSite=Lax 或 Strict(现代浏览器默认 Lax,已大幅缓解)
  2. CSRF Token —— 服务端下发随机 token,前端放在请求头或表单里。攻击者跨站读不到这个 token
  3. 校验 Origin / Referer
  4. 关键操作二次验证(密码、短信码)

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。