您的位置:首页 > 理论基础 > 计算机网络

懂你网络系列6之HTTP缓存

2020-06-06 05:40 183 查看

一.概述

通过重新使用以前获取的资源,可以显着提高网站和应用程序的性能。Web缓存减少了等待时间和网络流量,因此减少了显示资源表示形式所需的时间。通过使用HTTP缓存,网站变得更加敏感。

二.与缓存相关的HTTP首部字段

1. 通用首部字段(就是请求报文和响应报文都能用上的字段)

2. 请求首部字段

3.响应首部字段

4.实体首部字段

三.缓存关键实现的进化史

http本地缓存(强缓存)

在 http1.0 时代,给客户端设定缓存方式可通过两个字段——“Pragma”和“Expires”来规范。虽然这两个字段早可抛弃,但为了做http协议的向下兼容,你还是可以看到很多网站依旧会带上这两个字段。

1.Pragma
Pragma是HTTP / 1.0标头,未指定用于HTTP响应,因此,如果请求中省略标头字段Cache-Control,尽管它的行为与之相同,但它不是常规HTTP / 1.1 标头的可靠替代。使用仅适用于HTTP / 1.0客户端的向后兼容性。如下图使用fidder抓包工具,调试微软的官方网站,这个网站为了向下兼容,出现了“Pragma”和“Expires字段

通过fidder设置请求参数:

//以下头部信息前者缓存半天,后者不保存。
Cache-Control:pulic,max-age=43200
Pragma:no-cache


再次请求:

再次请求时发现,还是会向服务器发起请求,此时Cache-Control字段都没了,值保留Pragma字段,即Pragma优先级大于Cache-Control

2.Expires

有了Pragma来禁用缓存,自然也需要有个东西来启用缓存和定义缓存时间,对http1.0而言,Expires就是做这件事的首部字段。

Expires的值对应一个GMT(格林尼治时间),比如Sat, 23 May 2020 12:44:25 GMT来告诉浏览器资源缓存过期时间,如果还没过该时间点则不发请求。

修改操作同上,我们通过Pragma禁用缓存,又给Expires定义一个还未到期的时间,刷新页面时发现均发起了新请求,这意味着Pragma字段的优先级会更高。有兴趣的可以去试一下

但是响应报文中Expires所定义的缓存时间是相对服务器上的时间而言的,如果客户端上的时间跟服务器上的时间不一致(特别是用户修改了自己电脑的系统时间),那缓存时间可能就没啥意义了。所以就出现新的缓存实现,Cache-Control

3.Cache-Control
针对上述的“Expires时间是相对服务器而言,无法保证和客户端时间统一”的问题,http1.1新增了 Cache-Control 来定义缓存过期时间。

注意:若报文中同时出现了 Expires 和 Cache-Control,则以 Cache-Control 为准。又由1可知,优先级Pragma -> Cache-Control ,也就是说优先级从高到低分别是 Pragma -> Cache-Control -> Expires 。

Cache-Control也是一个通用首部字段,这意味着它能分别在请求报文和响应报文中使用。在RFC中规范了 Cache-Control 的格式为:

"Cache-Control" ":" cache-directive

作为请求首部时,cache-directive 的可选值有:

作为响应首部时,cache-directive 的可选值有:

Cache-Control 允许自由组合可选值,例如:

Cache-Control: max-age=3600, must-revalidate

它意味着该资源是从原服务器上取得的,且其缓存(新鲜度)的有效时间为一小时,在后续一小时内,用户重新访问该资源则无须发送请求。 当然这种组合的方式也会有些限制,比如 no-cache 就不能和 max-age、min-fresh、max-stale 一起搭配使用。

http缓存校验字段(协商缓存)

上述的首部字段均能让客户端决定是否向服务器发送请求,比如设置的缓存时间未过期,那么自然直接从本地缓存取数据即可(在chrome下表现为200 from cache),若缓存时间过期了或资源不该直接走缓存,则会发请求到服务器去。

我们现在要说的问题是,如果客户端向服务器发了请求,那么是否意味着一定要读取回该资源的整个实体内容呢?

我们试着这么想——客户端上某个资源保存的缓存时间过期了,但这时候其实服务器并没有更新过这个资源,如果这个资源数据量很大,客户端要求服务器再把这个东西重新发一遍过来,是否非常浪费带宽和时间呢?

答案是肯定的,那么是否有办法让服务器知道客户端现在存有的缓存文件,其实跟自己所有的文件是一致的,然后直接告诉客户端说“这东西你直接用缓存里的就可以了,我这边没更新过呢,就不再传一次过去了”。

为了让客户端与服务器之间能实现缓存文件是否更新的验证、提升缓存的复用率,Http1.1新增了几个首部字段来做这件事情。

1. Last-Modified
The Last-Modified 是一个响应首部,其中包含源头服务器认定的资源做出修改的日期及时间。

它通常被用作一个验证器来判断接收到的或者存储的资源是否彼此一致。

由于精确度比 ETag 要低,所以这是一个备用机制。包含有 If-Modified-Since 或 If-Unmodified-Since 首部的条件请求会使用这个字段。

Last-Modified: Sat, 10 May 2020 01:47:00 GMT

客户端会为资源标记上该信息,下次再次请求时,会把该信息附带在请求报文中一并带给服务器去做检查,若传递的时间值与服务器上该资源最终修改时间是一致的,则说明该资源没有被修改过,直接返回304状态码,内容为空,这样就节省了传输数据量 。

如果两个时间不一致,则服务器会发回该资源并返回200状态码,和第一次请求时类似。这样保证不向客户端重复发出资源,也保证当服务器有变化时,客户端能够得到最新的资源。一个304响应比一个静态资源通常小得多,这样就节省了网络带宽。

至于传递标记起来的最终修改时间的请求报文首部字段一共有两个:
⑴ If-Modified-Since: Last-Modified-value
示例为

If-Modified-Since: Sat, 23 Mary 2020 22:00:00 GMT

该请求首部告诉服务器如果客户端传来的最后修改时间与服务器上的一致,则直接回送304 和响应报头即可。

当前各浏览器均是使用的该请求首部来向服务器传递保存的 Last-Modified 值。

⑵ If-Unmodified-Since: Last-Modified-value
告诉服务器,若Last-Modified没有匹配上(资源在服务端的最后更新时间改变了),则应当返回412(Precondition Failed) 状态码给客户端。

与If-Modified-Sincezuo作用相反当遇到下面情况时,If-Unmodified-Since 字段会被忽略:

  • Last-Modified值对上了(资源在服务端没有新的修改);
  • 服务端需返回2XX和412之外的状态码;
  • 传来的指定日期不合法

Last-Modified 存在一定问题,精度为秒如果在一秒钟之内修改数次可能表现为无变化,再者如果在服务器上,一个资源被修改了,但其实际内容根本没发生改变,会因为Last-Modified时间匹配不上而返回了整个实体给客户端(即使客户端缓存里有个一模一样的资源)。即引出了ETag

2.ETag
为了解决上述Last-Modified可能存在的不准确的问题,Http1.1还推出了 ETag 实体首部字段。
ETagHTTP响应报头为资源的特定版本的标识符(比如md5标志)。它使缓存更加有效并节省了带宽,因为如果内容未更改,Web服务器不需要重新发送完整的响应。此外,etag有助于防止资源的同时更新互相覆盖(“空中冲突”)。

如果给定URL上的资源发生更改,则必须生成一个新Etag值。通过比较它们可以确定资源的两种表示形式是否相同。因此,Etag与指纹相似,某些服务器也可能将其用于跟踪目的。跟踪服务器还可以将它们设置为无限期持久。

Etag: "5d8c72a5edda8d6a:3239"

客户端会保留该 ETag 字段,并在下一次请求时将其一并带过去给服务器。服务器只需要比较客户端传来的ETag跟自己服务器上该资源的ETag是否一致,就能很好地判断资源相对客户端而言是否被修改过了。

如果服务器发现ETag匹配不上,那么直接以常规GET 200回包形式将新的资源(当然也包括了新的ETag)发给客户端;如果ETag是一致的,则直接返回304知会客户端直接使用本地缓存即可。

那么客户端是如何把标记在资源上的 ETag 传回给服务器的呢?请求报文中有两个首部字段可以带上 ETag 值:

⑴ If-None-Match: ETag-value
示例为:

If-None-Match: "5d8c72a5edda8d6a:3239

告诉服务端如果 ETag 没匹配上需要重发资源数据,否则直接回送304 和响应报头即可。

当前各浏览器均是使用的该请求首部来向服务器传递保存的 ETag 值。

⑵ If-Match: ETag-value
告诉服务器如果没有匹配到ETag,或者收到了“*”值而当前并没有该资源实体,则应当返回412(Precondition Failed) 状态码给客户端。否则服务器直接忽略该字段。

If-Match 的一个应用场景是,客户端走PUT方法向服务端请求上传/更替资源,这时候可以通过 If-Match 传递资源的ETag。

需要注意的是,如果资源是走分布式服务器(比如CDN)存储的情况,需要这些服务器上计算ETag唯一值的算法保持一致,才不会导致明明同一个文件,在服务器A和服务器B上生成的ETag却不一样。

如果 Last-Modified 和 ETag 同时被使用,RFC2616对这种情况做了规定,如果既有If-None-Match又有If-Modified-Since,除非两者不冲突(要求它们的验证都必须通过才会返回304),不然服务器会按常规返回资源实体及200状态码

四.已有缓存实现的对比

头部优势和特点劣势和问题
Expires1、HTTP 1.0 产物,可以在HTTP 1.0和1.1中使用,简单易用。
2、以时刻标识失效时间。
1、时间是由服务器发送的(UTC),如果服务器时间和客户端时间存在不一致,可能会出现问题。
2、存在版本问题,到期之前的修改客户端是不可知的。
Cache-Control1、HTTP 1.1 产物,以时间间隔标识失效时间,解决了Expires服务器和客户端相对时间的问题。
2、比Expires多了很多选项设置。
1、HTTP 1.1 才有的内容,不适用于HTTP 1.0 。
2、存在版本问题,到期之前的修改客户端是不可知的。
Last-Modified1、不存在版本问题,每次请求都会去服务器进行校验。服务器对比最后修改时间如果相同则返回304,不同返回200以及资源内容。1、只要资源修改,无论内容是否发生实质性的变化,都会将该资源返回客户端。例如周期性重写,这种情况下该资源包含的数据实际上一样的。
2、以时刻作为标识,无法识别一秒内进行多次修改的情况。
3、某些服务器不能精确的得到文件的最后修改时间。
ETag1、可以更加精确的判断资源是否被修改,可以识别一秒内多次修改的情况。
2、不存在版本问题,每次请求都会去服务器进行校验。
1、计算ETag值需要性能损耗。
2、分布式服务器存储的情况下,计算ETag的算法如果不一样,会导致浏览器从一台服务器上获得页面内容后到另外一台服务器上进行验证时发现ETag不匹配的情况。

五.用户操作行为与缓存

用户操作Expires/Cache-ControlLast-Modified/Etag
地址栏回车有效有效
页面链接跳转有效有效
新开窗口有效
有效
前进后退有效
有效
F5刷新无效有效
CTRL+F5强刷新无效无效

浏览器中的操作对缓存的影响:

  • 强制刷新 – 当按下ctrl+F5来刷新页面的时候, 浏览器将绕过各种缓存(本地缓存和协商缓存), 直接让服务器返回最新的资源;

  • 普通刷新 – 当按下F5来刷新页面的时候,浏览器将绕过本地缓存来发送请求到服务器, 此时, 协商缓存是有效的

  • 回车或转向 – 当在地址栏上输入回车或者按下跳转按钮的时候, 所有缓存都生效

六.相关缓存的实践

由于在用户在在使用客户端进行网络请求时,有着不同的使用场景,就以上用户在浏览器上的操作行为而言,我们在设置http缓存应用,需要考虑到项目需求,以上关于首部缓存字段均有可能用到。

1.Expires / Cache-Control
Expires用时刻来标识失效时间,不免收到时间同步的影响,而Cache-Control使用时间间隔很好的解决了这个问题。 但是 Cache-Control 是 HTTP1.1 才有的,不适用于 HTTP1.0,而 Expires 既适用于 HTTP1.0,也适用于 HTTP1.1,所以说在大多数情况下同时发送这两个头会是一个更好的选择,当客户端两种头都能解析的时候,会优先使用 Cache-Control。

2.Last-Modified / ETag
二者都是通过某个标识值来请求资源, 如果服务器端的资源没有变化,则自动返回 HTTP 304 (Not Changed)状态码,内容为空,这样就节省了传输数据量。

而当资源发生比那话后,返回和第一次请求时类似。从而保证不向客户端重复发出资源,也保证当服务器有变化时,客户端能够得到最新的资源。
其中Last-Modified使用文件最后修改作为文件标识值,它无法处理文件一秒内多次修改的情况,而且只要文件修改了哪怕文件实质内容没有修改,也会重新返回资源内容;

ETag作为“被请求变量的实体值”,其完全可以解决Last-Modified头部的问题,但是其计算过程需要耗费服务器资源

3.from-cache / 304
Expires和Cache-Control都有一个问题就是服务端作为的修改,如果还在缓存时效里,那么客户端是不会去请求服务端资源的(非刷新),这就存在一个资源版本不符的问题.
解决该问题的办法就是-把服务侧ETag的那一套也搬到前端来用——页面的静态资源以版本形式发布,常用的方法是在文件名或参数带上一串md5或时间标记符:

https://hm.baidu.com/hm.js?e23800c454aa573c0ccb16b52665ac26
http://tb1.bdstatic.com/tb/_/tbean_safe_ajax_94e7ca2.js
http://img1.gtimg.com/ninja/2/2016/04/ninja145972803357449.jpg

那么在文件没有变动的时候,浏览器不用发起请求直接可以使用缓存文件;而在文件有变化的时候,由于文件版本号的变更,导致文件名变化,请求的url变了,自然文件就更新了。

这样能确保客户端能及时从服务器收取到新修改的文件。通过这样的处理,增长了静态资源,特别是图片资源的缓存时间,避免该资源很快过期,客户端频繁向服务端发起资源请求,服务器再返回304响应的情况(有Last-Modified/Etag)

而强制刷新一定会发起HTTP请求并返回资源内容,无论该内容在这段时间内是否修改过

而Last-Modified和Etag每次请求资源都会发起请求,哪怕是很久都不会有修改的资源,都至少有一次请求响应的消耗。

对于所有可缓存资源,指定一个Expires或Cache-Control max-age以及一个Last-Modified或ETag至关重要。同时使用前者和后者可以很好的相互适应。

前者不需要每次都发起一次请求来校验资源时效性,后者保证当资源未出现修改的时候不需要重新发送该资源。而在用户的不同刷新页面行为中,二者的结合也能很好的利用HTTP缓存控制特性,无论是在地址栏输入URI然后输入回车进行访问,还是点击刷新按钮,浏览器都能充分利用缓存内容,避免进行不必要的请求与数据传输。

小结

  • 需要兼容HTTP1.0的时候需要使用Expires,不然可以考虑直接使用Cache-Control

  • 需要处理一秒内多次修改的情况,或者其他Last-Modified处理不了的情况,才使用ETag,否则使用Last-Modified。

  • 对于所有可缓存资源,需要指定一个Expires或Cache-Control,同时指定Last-Modified或者Etag。

  • 可以通过标识文件版本名、加长缓存时间的方式来减少304响应。

写在最后

更多缓存实践,可以利用fiddler抓包工具去实践一遍,教程和软件下载地址

参考资料

图解HTTP

MDN | HTTP缓存
浅谈http的缓存机制

HTTP缓存控制小结

使用浏览器缓存|PageSpeed Insights|Google Developers

缓存策略

Config HTTP header for better client performance

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐