简述 HTTP 缓存首部及其行为
Posted 码畜的实验室
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了简述 HTTP 缓存首部及其行为相关的知识,希望对你有一定的参考价值。
使用缓存的优点
-
缓存减少了亢余数据的传输,节省了你的网络费用 -
缓存缓解了网络瓶颈的问题,不需要更多的带宽就可能更快的加载页面 -
缓存降低了对原始服务器的要求。服务器可以更快的响应,避免过载的出现 -
缓存降低了距离时延,因为从较远的地方加载页面会更慢一些
缓存的拓扑结构
私有缓存
私有缓存通常指的就是本地的缓存,不单单指浏览器缓存,npm、yarn、brew 等等,这些包管理器的缓存也属于此列。
公用缓存
一般代理服务器可能会允许缓存资源,当用户发送请求的时候,会先经过代理,如果代理上边缓存的资源足够新鲜,就可以直接返回而不需要向原始服务器进行请求。
缓存的处理步骤(摘自 HTTP 权威指南)
-
接收 ---- 缓存从网络中读取抵达的请求报文 -
解析 ---- 缓存对报文进行解析,提取 URL 和各种首部 -
查询 ---- 缓存查看是否有本地副本可用,如果没有,就获取一份副本(并保存在本地) -
新鲜度检测 ---- 缓存查看已缓存副本是否足够新鲜,如果不是,就询问服务器是否有任何更新。 -
创建响应 ---- 缓存会用新的首部和已缓存的主体来构建一条响应报文。 -
发送 ---- 缓存通过网络将响应发回给客户端 -
日志 ---- 缓存可选地创建一个日志文件条目来描述这个事务
其中 新鲜度检测 是重中之重,接下来大部分内容主要说这个问题。
缓存如何处理
在接收到请求报文并解析之后,浏览器首先在缓存中进行查找,判断其是否命中缓存。如果命中缓存且资源足够新鲜,则直接从浏览器自己的缓存中读取对应的资源,不会向服务器发请求。创建响应,并设置响应为 200
,并进行响应头的改造等等。如果资源不够新鲜,就需要进行服务器再验证,再对资源进行一系列的操作。
上面的过程看似很合理,但是在开发中我们还会遇到一些由于缓存导致的问题。有一个很常见的场景:
一般情况下,开发者都会选择使用 CDN 服务器来托管静态资源,来加速静态资源的请求速度。对于这些资源,CDN 一般的设置是允许资源进行缓存。由于缓存命中之后,浏览器会直接从缓存中加载,那么即使线上的静态资源发生了变化,仍然没什么卵用。所以一般的解决办法是在静态资源上加一个哈希值,用来标识静态资源的版本,同时在 html 文件中实时更新资源的版本。这样的解决办法则要求开发者设置不允许浏览器每次加载 html 页面时候都要向服务器验证。当然也不推荐把 html 文档也放到 CDN 上“加速”。
缓存的新鲜度检测
当某个请求命中缓存的时候,响应状态值为 200
。在 Chrome 中的开发工具 network 卡中其 size 值会为 from cache
,判断是否 from cache
是通过其他的方法进行实现的,并不是根据响应的状态值进行判断。
缓存可以通过 Expires
或者 Cache-Control: max-age
这两个响应头来配置,都用来表示资源在客户端允许被缓存的时间。
其中 Expires
是 HTTP/1.0+ 提出的一个用来表资源过期时间的响应头,Cache-Control: max-age
则是 HTTP/1.1 推荐使用的。
他们解决的问题是相同的,不过 Expires
设定的时间是基于服务器时间的绝对时间,也就是会设置一个过期时间,超过这个时间点之后认为资源不够新鲜,需要更新。同时需要注意,使用过期时间的时候,所有的 HTTP 日期和时间都会在格林尼治(GMT)过期。也就是 0 时区。如果用户处在不同的时区内,就需要根据用户所在的时区进行定制过期时间,这样就会带来一系列的玄学问题。
比如期望一个资源在 2018 年 7 月 14 日凌晨 0 点过期,对于在东八区来说,格林尼治时间比东八区时间要晚 8 小时,那么首部则需要设置如下:
Expires: Fri, 13 Jul 2018, 16:00:00 GMT
换算到东八区刚好是 14 日凌晨 0 点。
而 Cache-Control: max-age
则是设置一个相对时间,max-age
的值是资源的最大的合法存活时间,以秒为单位。这个时间不会因为时差问题而导致差异,所以比较推荐使用。
当两个首部同时出现且 HTTP 版本都支持的话,后者的优先级较高。
前面的内容都是假设请求命中强缓存且资源并没有过期,那么如果命中了强缓存但是资源已经过期了呢?当然现实的场景是我们一般会设置缓存时间为一年,也就是 max-age
值为 315360000
。但是这种场景也不是不存在,需要考虑。
这会涉及到一个称为 服务端再验证 的情况。当资源已经过期但是服务端的资源没有任何的变化,那么缓存只需要取得新的首部,包括一个新的过期日期,并对缓存中的首部进行更新。
服务端再验证
Cache-Control
优先级高于 Expires
首部,一般常用的 Cache-Control
的取值有以下三种:
取值 | 含义 |
---|---|
no-store | 不允许缓存 |
no-cache | 在回源验证前不允许复用缓存 |
max-age | 文档最大合法存活时间 |
Cache-Control
可用取值有很多,其中响应可以出现的值有 9 种,请求可以出现的值有 7 种。支持自定义,只要服务端识别就 OK。
对于 Cache-Control: no-store
,意味着完全禁止缓存对响应复制,缓存通常像非缓存代理服务器一样,向客户端转发一条 no-store
响应,然后删除对象。
对于 Cache-Control: no-cache
,意味着响应实际上是可以存储在本地缓存区中的。只是在与原始服务器进行新鲜度再验证之前,缓存不能再提供给客户端使用。其实这个首部使用 do-not-serve-from-cache-without-revalidation 更恰当一些,但是它太长了。HTTP/1.1 同样提供了 Pragma: no-cache
首部,目的是为了兼容于 HTTP/1.0+。但是由于现在 HTTP/1.0+ 基本已经淘汰,故不再深入进行了解。只要是和 HTTP/1.1 或者 HTTP/2 应用进行交互时候,都应该使用 Cache-Control: no-cache
来进行交互。事实上 Pragma
的优先级最高,不过淘汰的东西就让它安静的消失就好了。
对于 Cache-Control: max-age
,用来标识文档最大合法存活时间,对于共享缓存,还会有一个 s-maxage
行为和 max-age
相似,其单位同样为秒。当 max-age
值为 0
时候,每次访问的时候都会进行资源的请求。
对于 Expires
首部,则是 HTTP/1.0+ 提供用于控制缓存的首部,值为一个绝对的 GMT 时间,这个时间需要根据时区再进行转换。
nginx 是一个非常轻量级的 http 服务器,通常被用作负载均衡器。在这个项目[1]中,笔者通过使用 nginx 的 add_header
为响应添加 Cache-Control
首部,并设置不同值,来观察 Cache-Control
不同值的作用,以及不同情况下缓存更新情况。
用条件方法进行再验证
HTTP 的条件方法可以高效的实现再验证。HTTP 允许缓存向原始服务器发送一个“条件 GET”,请求服务器只有缓存的对象和现有的副本不同时,才回送对象主体。只有条件为真时,Web 服务器才会返回对象,否则返回一个 304。
HTTP 定义了 5 个条件请求首部。对缓存在验证来说最有用的 2 个首部是 If-Modified-Since
和 If-None-Match
。所有条件首部都是以前缀 “If-” 开头。下面是缓存再验证中使用的条件请求首部。
If-Modifed-Since
If-Modified-Since
在验证请求通常可以叫做 IMS 请求。只有当这个首部的条件为真(即文档修改过),通常 GET 请求就会成功执行,携带新首部的新文档替换原来的缓存,同时会更新过期时间。如果文档没有被修改过,那么服务端返回一个小的 304 Not Modified 报文。这个报文不会包含文档主体,只会返回需要更新的新首部,一般会是一个新的过期时间。
If-Modified-Since
首部可以和 Last-Modified
服务器响应首部配合工作。服务端使用 Last-Modifed
首部来将最后修改日期附加给所提供的文档。当缓存要对已缓存的文档进行验证的时候,就会在 If-Modified-Since
带上携带有最后修改已缓存副本的日期。
If-None-Match
仅仅只有 IMS 对最后修改日期进行验证还是不够的,因为会有这样的情况:
-
有些文档会被周期性的重写,尽管内容没有变化,但是最后修改时间会变化 -
有些文档被修改了,但是并不重要,不需要重新缓存数据 -
有些服务器不能准确判断资源的最后修改日期 -
最后修改日期一般是秒级的变化,但是某些文件会在一秒变化很多次
为了解决这些问题,HTTP 允许用户对被称作 实体标签(ETag) 的版本标识符进行比较。ETag 目前没有一个明确的生成方法,各方可以自定义。在 Nginx 上可以通过 etag off
指令关闭。
Nginx 官方采用的格式是 文件最后修改时间(hex)
-文件长度(hex)
由于 Etag 没有明确的生成方法,所以就有使用 Etag 来存储用户的 uid 的做法,来弥补使用 cookie 追踪用户的不足。当然现在也有使用浏览器指纹追踪的技术,具体可以参考这个库[2]。
言归正传,当首部中带有 INM 首部的时候,服务端会根据 INM 提供的 Etag 值和当前文件实际的 Etag 值进行对比,如果二者相同的话,就会返回 304 Not Modified。
INM 和 IMS 首部都存在
当 INM 和 IMS 首部都存在的时候,客户端向服务端回送了实体标签和资源过期时间,那么只有两个验证都通过的时候,缓存才会被认为有效,服务端返回 304 Modified。否则需要返回资源并更新缓存。
三种刷新方式
-
打开新页面,不会带缓存相关字段,可以命中本地缓存 -
普通刷新,文档会携带 Cache-Control: max-age=0
,进行请求而不考虑是否过期,但是关联资源可以在不过期的情况下直接读取本地缓存 -
强制刷新,文档会携带 Cache-Control: max-age=0
,和Pragma: no-cache
,关联资源也会刷新 -
打开开发工具,勾选 disable cache 之后的刷新,所有请求都走强制刷新。
参考资料
这个项目: https://github.com/Raoul1996/http-cache.git
[2]库: https://github.com/Valve/fingerprintjs2
推荐阅读
以上是关于简述 HTTP 缓存首部及其行为的主要内容,如果未能解决你的问题,请参考以下文章