[HTTP 系列] 第 3 篇 —— HTTP 缓存那些事

[HTTP 系列] 第 3 篇 —— HTTP 缓存那些事

这里是《写给前端工程师的 HTTP 系列》,记得有位大佬曾经说过:“大厂前端面试对 HTTP 的要求比 CSS 还要高”,由此可见 HTTP 的重要程度不可小视。文章写作计划如下,视情况可能有一定的删减,本篇是该系列的第 3 篇 —— 《深入理解 HTTP 的缓存机制》。

从一张图片的响应头说起

一个响应头的例子

下面是一张图片的响应头, 我们复习一下各个字段:

  • Accept-Ranges: 该字段告知客户端, 服务器是否能处理范围请求, 当可以处理时其值为 bytes, 否则为 none.

  • Connection: 该字段决定当前的事务完成后, 是否会关闭网络连接. 如果该值是 keep-alive, 网络连接就是持久的, 不会关闭, 使得对同一个服务器的请求可以继续在该连接上完成. 此外它还可以控制不再转发给代理的首部字段.

  • Content-Length: 该字段表明实体主体的大小, 单位是字节.

  • Content-MD5: 该字段用于检查报文主体在传输过程中是否保持完整性, 以及确认传输到达. 服务端对报文主体执行 MD5 算法, 获取一个 128 位的二进制数, 再通过 base64 编码后将结果写入 Content-MD5 字段值. 因为 HTTP 首部无法记录二进制值, 因此需要通过 Base64 进行处理. 客户端在接收到响应后再对报文主体执行一次相同的 MD5 算法. 将计算值于该字段值比较, 即可判断出报文主体的准确性.

  • Content-Type: 报文主体的格式.

  • Date: 表示创建报文的日期和时间.

  • ETag: 该值是将资源以字符串的形式作唯一标识, 服务器给每份资源分配对应的 ETag 值. 当资源更新时, ETag 值也会更新. ETag 有 强 ETag弱 ETag 之分, 前者一般用于静态文件, 后者的字段值起始会有 W 标志.

  • Last-Modified: 该字段为服务器认定的资源做出修改的日期及时间, 它的精度比 ETag 要低, 也就是如果响应头中同时包含 ETag 和 Last-Modified 时, 会以 ETag 为准.

什么是 HTTP 缓存

当客户端向服务端请求资源时, 会先访问浏览器缓存, 如果浏览器有"要请求资源"的副本, 就可以直接从浏览器缓存中提取, 而不是从原始服务器中提取这个资源.

HTTP 缓存都是在第二次请求开始的. 第一次请求资源时, 服务器返回资源, 并在响应头中回传资源的缓存参数; 后续请求中, 浏览器判断这些请求参数, 命中强缓存就直接 200 from cache, 否则就把请求参数加到请求头中回传给服务器, 看是否命中协商缓存, 命中则返回 304, 并使用浏览器缓存, 否则服务器会返回新的资源.

根据是否需要向服务器重新发起请求来分类, 可分为强制缓存协商缓存; 根据是否可以被单个或者多个用户使用来分类, 可分为私有缓存共享缓存. 这篇文章我们主要来聊强制缓存协商缓存.

强缓存和协商缓存

强缓存

通过上面这张图, 我们知道强缓存由响应头中的 Pragma, Cache-ControlExpires 控制, 因为 Pragma 已经在 HTTP1.1 被废弃了, 这里不做讨论.

对于 Cache-ControlExpires, 如果两者都存在, 且 Cache-Control 设置了 max-age 或者 s-max-age, 那么 Expires 头会被忽略. 也就是说 Cache-Control 的优先级要比 Expires, 这是因为后者用的是服务器时间, 这就导致客户端跟服务端的时间不一致而发生错误; 此外, 在缓存未失效前, Expires 无法获取到修改后的资源.

两个字段本质上都是告知客户端对比本地时间和服务器返回的生存时间来检测缓存是否可用, 如果缓存没有超过它的生存时间, 响应的副本会一直被保存. 当超过指定的时间后, 缓存服务器在请求发送过来时, 转向源服务器请求资源.

下面简单复习一下请求头中的 Cache-Control:

指令指令值说明
max-age=[秒]必填设置缓存存储的最大周期, 超过这个时间缓存被认为过期, 该指令优先级高于 Expires, 并且它传递的是一个相对时间, 而 Expires 传递的是一个未来的时间.
max-stale(=[秒])选填在这个已过期的时间段之内, 客户端愿意接收一个已经过期的资源
min-fresh=[秒]必填表示客户端希望在指定的时间内获取最新的响应.
no-cache选填表示客户端不会接收缓存过的响应, 并且强制代理服务器将把客户端的请求转发给源服务器.
no-store不缓存请求或响应中的任何内容(注意这个才是告知浏览器不缓存资源).
no-transform代理服务器不得对资源进行转换或转变, 比如 Content-Encoding, Content-Range, Content-Type 等字段信息.
only-if-cached客户端只接受已缓存的响应, 并且不要向原始服务器检查是否有更新的拷贝, 若没有命中缓存, 则返回 504 状态码 (Gateway Timeout).

协商缓存

当第一次请求时服务器返回的响应头中符合如下三个条件之一, 浏览器第二次请求时就会与服务器进行协商, 即与服务端对比判断资源是否进行了修改更新.

  • 没有 Cache-Control 和 Expires

  • Cache-Control 和 Expires 已经过期

  • Cache-Control 的属性值为 no-cache 时(即不走强缓存)

如果服务器端的资源没有修改, 就返回 304, 那么浏览器可以使用缓存中的数据, 否则直接返回 200 和新的资源. 跟协商缓存相关的头部属性有 ETag/If-Not-MatchLast-Modified/If-Modified-Since, 他们是成对出现的. 其中 ETagLast-Modified 是请求头中的字段, 而 If-Not-MatchIf-Modified-Since 是响应头中的字段, 下图是对两者的比较.

ETag/If-Not-Match 和 Last-Modified/If-Modified-Since 对比

协商缓存的流程是这样的: 当浏览器第一次向服务器发送请求时, 会在响应头中返回协商缓存的头属性: ETag 和 Last-Modified, 其中 ETag 返回的是一个 hash 值, Last-Modified 返回的是 GMT 格式的最后修改时间; 在后续的请求中, 会在请求头上带上 If-Not-Match 和 If-Modified-Since, 服务器在接收到这两个参数后会做比较, 如果返回的是 304 状态码, 则说明请求的资源没有修改, 浏览器可以直接在缓存中取数据, 否则, 服务器会直接返回数据.

协商缓存请求头

协商缓存响应头

为什么有了 Last-Modified 还需要 ETag 呢? ETag/If-Not-Match 是在 HTTP/1.1 出现的, 主要是修正 Last-Modified 一些不准确的问题:

  • Last-Modified 标注的最后修改只能精确到秒级, 如果某些文件在 1 秒钟以内, 被修改多次的话, 它将不能准确标注文件的修改时间

  • 如果某些文件被修改了, 但是内容并没有任何变化, 而 Last-Modified 却改变了, 导致文件没法使用缓存

  • 有可能存在服务器没有准确获取文件修改时间, 或者与代理服务器时间不一致等情形

HTML 文件如何使用缓存

HTML 禁用缓存:

<meta http-equiv="cache-control" content="no-cache" />

HTML 设置缓存:

<meta http-equiv="Cache-Control" content="max-age=7200" />

用户行为对浏览器缓存的影响

所谓用户行为对浏览器缓存的影响, 指的就是用户在浏览器如何操作时, 会触发怎样的缓存策略. 主要有 3 种:

  • 打开网页, 地址栏输入地址: 查找 disk cache 中是否有匹配. 如有则使用;如没有则发送网络请求.

  • 普通刷新(Command + R): 因为 TAB 并没有关闭, 因此 memory cache 是可用的, 会被优先使用(如果匹配的话). 其次才是 disk cache.

  • 强制刷新(Command + Shift + R): 浏览器不使用缓存, 因此发送的请求头部均带有 Cache-control: no-cache(为了兼容, 还带了 Pragma: no-cache), 服务器直接返回 200 和最新内容.

私有缓存和共享缓存

私有缓存(浏览器级缓存): 私有缓存只能用于单独用户. 你可能已经见过浏览器设置中的“缓存”选项. 浏览器缓存拥有用户通过 HTTP 下载的所有文档. 这些缓存为浏览过的文档提供向后/向前导航, 保存网页, 查看源码等功能, 可以避免再次向服务器发起多余的请求. 它同样可以提供缓存内容的离线浏览, 并且只能用于单独的用户.

Cache-Control: Private

共享缓存(代理级缓存): 共享缓存可以被多个用户使用. 例如, ISP 或你所在的公司可能会架设一个 web 代理来作为本地网络基础的一部分提供给用户. 这样热门的资源就会被重复使用, 减少网络拥堵与延迟. 共享缓存可以被多个用户使用.

Cache-Control: Public

Vary: Accept-Encoding

当一个资源启用了 gzip 压缩, 并且被代理服务器缓存, 客户端如果不支持 gzip 压缩, 那么在这样的情况下将会得到不正确的数据(也就是压缩过的数据). 这将会使代理服务器缓存两个版本的资源: 一个是压缩过的, 一个是没压缩过的. 正确版本的资源将在请求头发送之后进行传输.

此外: IE 浏览器不缓存任何带有 Vary 头但值不为 Accept-Encoding 和 User-Agent 的资源. 所以通过这种方式添加这个头, 才能确保这些资源在 IE 下被缓存.

加餐: keep-alive

在 http 早期, 每个 http 请求都要求打开一个 tcp socket 连接, 并且使用一次之后就断开这个 tcp 连接.

使用 keep-alive 可以改善这种状态, 即在一次 TCP 连接中可以持续发送多份数据而不会断开连接. 通过使用 keep-alive 机制, 可以减少 tcp 连接建立次数, 也意味着可以减少 TIME_WAIT 状态连接, 以此提高性能和提高 httpd 服务器的吞吐率.

但是, keep-alive 并不是银弹, 长时间的 tcp 连接容易导致系统资源无效占用. 配置不当的 keep-alive, 有时比重复利用连接带来的损失还更大. 所以, 正确地设置 keep-alive timeout 时间非常重要.

keep-alive timeout

Httpd 守护进程, 一般都提供了 keep-alive timeout 时间设置参数. 比如 nginx 的 keepalivetimeout, 和 Apache 的 KeepAliveTimeout. 这个 keepalivetimout 时间值意味着: 一个 http 产生的 tcp 连接在传送完最后一个响应后, 还需要 hold 住 keepalive_timeout 秒后, 才开始关闭这个连接.

当 httpd 守护进程发送完一个响应后, 理应马上主动关闭相应的 tcp 连接, 设置 keepalivetimeout 后, httpd 守护进程会想说: "再等等吧, 看看浏览器还有没有请求过来", 这一等, 便是 keepalivetimeout 时间. 如果守护进程在这个等待的时间里, 一直没有收到浏览发过来 http 请求, 则关闭这个 http 连接.

总结

最后用一张图总结缓存机制:

缓存机制

参考

《图解 HTTP》 -- 上野 宣

HTTP 协议知识点总结

【前端词典】从输入 URL 到展现涉及哪些缓存环节(非常详细)

HTTP Keep-Alive 是什么?如何工作?

一文读懂 http 缓存(超详细)

深入理解浏览器的缓存机制

关于我

PREVIOUS POST

关于我

简析 AMD / CMD / UMD / CommonJS / ES Module

NEXT POST

简析 AMD / CMD / UMD / CommonJS / ES Module