http爆炸重点学习——缓存

忙碌的三月,迟来的总结,今天写写http协议一个非常重要的点——缓存的处理。

缓存的作用是不言而喻的,它是web中各个方面性能优化的利器,关于http中的缓存,权威指南中的介绍非常直观:

  • 缓存减少了冗余的数据传输,节省了你的网络费用。
  • 缓存缓解了网络瓶颈的问题。不需要更多的带宽就能够更快地加载页面。
  • 缓存降低了对原始服务器的要求。服务器可以更快地响应,避免过载的出现。
  • 缓存降低了距离时延,因为从较远的地方加载页面会更慢一些。

其中也有一个最基本的概念:

可以用已有的副本为某些到达缓存的请求提供服务。这被称为缓存命中(cache hit),其他一些到达缓存的请求可能会由于没有副本可用,而被转发给原始服务器。这被称为缓存未命中(cache miss)。

1.一图流

上图中涉及到一些http首部字段,后面会慢慢讲到,首先我们可以把上图拆分成3种最简单的逻辑来分析。

1) 缓存未命中

分两种情况:

  • 缓存必须要在浏览器访问过一次资源后才会产生,所以最简单的情况是第一次请求资源,直接从服务器上成功获取到资源,并设置一系列缓存相关的http头信息,如下图。
  • 再次访问通过http首部字段验证不满足使用缓存的条件,如下图。

2) 缓存命中

  • 缓存直接命中,这种情况也叫浏览器缓存命中(强缓存命中),这种直接返回状态码200(from cache)

3) 缓存再验证命中

  • 缓存再验证命中,这种也叫做协商缓存命中,在强缓存验证不通过的情况下,会像服务器端发送一个协商缓存验证的信息,如果此时服务器发现请求资源未修改,则返回状态码304(Not Modified)告知服务器从缓存中获取资源并更新协商缓存信息。这种情况通缓存未命中的第二种情况相对应。

2.浏览器缓存(强缓存)

下满我们来仔细讨论下http协议中缓存的机制,它是怎样通过一些首部信息来实现调用缓存的。从图中我们可以简单分析得出缓存包含两种类型,这两种类型我们叫做浏览器缓存(强缓存)和协商缓存,我们先讨论第一种缓存。

强缓存相关的首部字段:

key 描述
Cache-Control http1.1规则,优先级最高,可设置一系列缓存的机制
Pragma http1.0字段,指定缓存的方式,以no-cache出现
Expires http1.0字段,设定缓存的过期时间

1)Cache-Control

这个首部是缓存控制中最重要也是最常见的字段,它不仅可以设置缓存如何存储,同时也能告诉浏览器什么时候使用缓存,这个首部也是一个通用首部字段,在请求报文和响应报文都可以使用到,缓存指令是单向的, 这意味着在请求里设置的指令,在响应中不用包含相同的指令。

Cache-Control的语法为:Cache-Control: cache-directive

  • 在请求和响应都可能出现的指令:
cache-directive 描述
max-age=[seconds] 缓存请求的资源,该资源在second时间后缓存过期,单位为秒
no-cache 资源被缓存,但是立即失效,所有用户必须发送验证信息到服务器验证资源是否过期(协商)
no-store 缓存不存储任何关于请求和响应的资源信息
no-transform 针对代理服务器,
让代理服务器不能对资源进行包括Content-Encoding
Content-Range
Content-Typeheader的修改,比如将图片资源的格式修改以便于压缩资源大小
的操作都会被这条指令阻止
  • 只在请求中出现的指令:
cache-directive 描述
max-stale[=seconds] 可以使用已经失效的缓存,可设置时间控制在缓存失效之后second时间内仍然可以使用缓存
min-fresh[=seconds] 客户端希望获取到的资源至少有seconds的新鲜期
only-if-cached 如果有缓存就返回缓存,不和服务器交互,无缓存返回504(Gateway Timeout)
  • 只在响应中出现的指令:
cache-directive 描述
must-revalidate 缓存使用之前必须去验证新鲜度(协商),失败返回504(Gateway Timeout)
proxy-revalidate 同上,适用于代理服务器
public 表明响应可以被任何对象(包括:发送请求的客户端,代理服务器,等等)缓存
private 表明响应只能被单个用户缓存(浏览器),不能作为共享缓存(即代理服务器不能缓存它)
s-maxage[=seconds] 原理同max-age,适用于共享缓存(代理等),在共享缓存设置中会覆盖max-age和老一代Expires头

介绍跟时间设置相关的场景,假如一个资源在1月1日进行了缓存,当前的缓存周期是10天,即在1月10日资源会被再次验证新鲜度,那么我在1月1日有几种设计缓存的方案。

  • Cache-Control: max-age=432000(5 x 24 x 3600 = 5day)
    该情况下,缓存周期为5天,缓存资源将在1月5日过期,无特殊情况下5天内都会去请求浏览器缓存的资源,而不会向服务器发送验证请求,原来的缓存周期会被覆盖。
  • Cache-Control: max-stale =432000(5 x 24 x 3600 = 5day)
    这种情况下,允许在缓存资源失效后5天内都可访问,即缓存资源会在1月15日过期。
  • Cache-Control: min-fresh =86400(1 x 24 x 3600 = 1day)
    这种情况下,缓存资源必须保留一天的新鲜度,即在1月9日就会去向服务器发送资源新鲜度验证请求。

当三种策略同时作用的时候,以最保守的情况做为依据,上述例子中,1月5日之后就会向服务器验证资源新鲜度了。在验证的策略后面会慢慢讲到。

扩展阅读:What’s the difference between Cache-Control: max-age=0 and no-cache?

2)Pragma

在http 1.0时代,Pragma就是用来定义缓存策略的,在RFC文档中,Pragma就只有一个值no-cache,会使得浏览器不对资源进行缓存而直接向服务器发送请求。功能和Cache-Control: no-cache一样,但是有时会为了做http协议的向下兼容,通常都会写上Pragma或者Expires字段。

3)Expires

既然Pragma能关闭缓存功能,自然需要一个首部来开启缓存功能并定制缓存策略,Expires在http1.0中就扮演这个角色。

Expires的值对应一个GMT(格林尼治时间),比如“Mon, 23 Mar 2017 11:14:08 GMT”来告诉浏览器资源缓存过期时间,在这个时间之前都会先去浏览器缓存中获取。

3.协商缓存

关于浏览器缓存的http首部,主要还是决定了客户端是否向服务器端发送请求,比如no-cache就会阻止浏览器的请求发送,或者说在max-age的可用时间内。下面我们要讨论的是,如果客户端确确实实发送了请求到服务器端,那么是否一定意味着我们会完整地去获取一个资源呢。

答案是否定的,在http1.1中,有一些首部做着一些新鲜度检测的工作。设想一下如果客户端发送了请求想要获取一个很大的资源,而这个资源可能在服务器端并位修改过,如果我们还是去重新获取一便,是否浪费了带宽呢?于是就有了协商缓存的概念。

协商缓存相关的首部字段一般成对出现:

1)Last-Modified/If-Modified-Since

格式均为GMT(格林尼治标准时间) :星期 日期 月份 年份 时:分:秒

具体过程如下:

  • 1.浏览器第一次请求一个资源后,会得到资源的内容的同时,在response的首部加上一个Last-Modified,并且标记上这个资源在服务器上最后修改的时间。
  • 2.当再一次请求该资源,浏览器不能命中强缓存的时候,客户端会在请求首部带上If-Modified-Since,值即为上次Last-Modified记录的值,用于判断该资源在该时间时候是否在服务器上有修改。
  • 3.如果没有修改,则命中协商缓存,返回304(Not Modified),但是不会返回资源内容,浏览器会从缓存中去加载这个资源,response的首部中不会再添加Last-Modified,仍是上一次请求存下的值,在下一次请求该资源的时候还是会继续带上相同的Last-Modified首部。
  • 4.如果资源修改了,即没有命中协商缓存,则是返回200(OK),跟步骤1一样,会在response中返回一个带有新值的Last-Modified首部,这个值即为服务器最新修改的时间。

If-Unmodified-Since 缓存校验字段, 语法同上. 表示资源未修改则正常执行更新, 否则返回412(Precondition Failed)状态码的响应. 常用于如下两种场景:

  • 不安全的请求, 比如说使用post请求更新wiki文档, 文档未修改时才执行更新.
  • 与 If-Range 字段同时使用时, 可以用来保证新的片段请求来自一个未修改的文档.

2)Etag/If-None-Match

格式: ETag:"2c1-4a6473f6030c0"

通过某种算法(比如md5),给资源计算出一个唯一标识,和Last-Modified一样,会在response中返回Etag值,标记当前状态的资源。客户端保留Etag标识,并在下一次请求首部中通过If-None-Match去匹配,匹配结果和行为与If-Modified-Since类似,如果修改过,则返回新的标识,状态200(OK),没有则304(Not Modified)。

If-Match 缓存校验字段, 其值为上次收到的Etag值. 常用于判断条件是否满足, 如下两种场景:

  • 对于 GET 或 HEAD 请求, 结合 Range 头字段, 它可以保证新范围的请求和前一个来自相同的源, 如果不匹配, 服务器将返回一个416(Range Not Satisfiable)状态码的响应.
  • 对于 PUT 或者其他不安全的请求, If-Match 可用于阻止错误的更新操作, 如果不匹配, 服务器将返回一个412(Precondition Failed)状态码的响应.

关于If-Unmodified-Since和If-Match没有试验过,参考了浏览器缓存机制剖析

3)为什么有了Last-Modified/If-Modified-Since还需要Etag/If-None-Match呢?

在绝大多数场景下使用Last-Modified已经足够我们去检测一个资源的新鲜度了,那么为什么还需要Etag呢?而且在Last-Modified与ETag一起使用的时候,服务器会优先验证ETag,在Etag满足一致的情况下,才会继续去验证Last-Modified,来决定是返回304还是200.

其实主要是为了解决如下几个问题:

  1. 一些资源文件会周期性的更改,但是内容并不会变,仅仅只改变了其修改时间,这个时候我们并不希望客户端每次都从服务器获取新鲜的资源,而Etag的计算则会保持一致;

  2. 某些文件修改非常频繁,GMT时间单位只能精确到秒级,这种文件的验证会出现问题;

  3. 某些服务器对文件的计算时间不够精确,存在偏差;

这个时候我们使用Etag就能更加准确地标记一个资源在某个时间的状态从而更加精确的去控制缓存。

4.其他

当我们在chrome 58.0 场景下打开开发者工具的时候,在刷新处右键可以看到如下一张图。

  • windows下Ctrl(Mac OS下的command) + R或者F5,即为正常的刷新行为,强缓存和协商缓存都会去验证。
  • Ctrl + shift + R或者Ctrl + F5或者在devtool的Network中disable cache都会在请求首部加上Cache-Control: no-cachePragma: no-cache首部,并且清除掉EtagLast-Modified首部,从而达到硬性重新加载的效果,即跳过缓存验证。
  • 最后一项则会在上一项的基础上把在内存或者磁盘的缓存全部给清除掉。

5.参考资料

浏览器缓存机制剖析
RFC 7234 HTTP/1.1 Caching
http协商缓存VS强缓存
Cache-Control - HTTP | MDN