HTTP缓存协议实战

程序员成长指北

共 10455字,需浏览 21分钟

 ·

2022-05-27 23:29

大厂技术  高级前端  Node进阶

点击上方 程序员成长指北,关注公众号

回复1,加入高级Node交流群


一、什么是缓存


缓存,又称作Cache,我们把临时存储数据的地方叫做缓存池,缓存池里面放的数据就叫做缓存。当用户需要使用这些数据,首先在缓存中寻找,如果找到了则直接使用。如果找不到,则再去其他数据源中查找。


二、为什么要使用缓存技术


缓存的本质就是用空间换时间,以临时存储的数据暂时代替数据源中读取最新的数据,这种方式带来的好处在不同的场景下是不一样的。


举个例子:


当我们需要喝水时,我们会拿出一个水杯,去水龙头接一杯水来喝。大家可以思考一下,为什么用杯子来喝水,而不是直接用嘴巴在水龙头接水喝。


用杯子喝水确实存在一些既有的问题,比如杯子里面的水容易变凉,而水龙头流出的水确是恒温的。我们可以想象一下,公司里的同事们排队在水龙头下面喝水的场面,确实有点滑稽,我们宁愿接受杯子里的水会变凉这个既有问题。


用杯子喝水有以下几个优势:


  • 用杯子喝水解决了总是要去找水龙头的问题,因为杯子可以一次接更多的水。

  • 用杯子喝水更不容易洒出来,不容易浪费水。

  • 用杯子喝水比趴在水龙头下喝水更优雅。


我们把杯子看成一个缓存池,杯中的水看成缓存,我们接受了杯中水会变凉的问题,相当于牺牲了数据的实时性。把这些优势换一个方式来描述,于是使用缓存的优势变成了下面几个:


  • 降低了系统压力;

  • 节省了资源消耗;

  • 优化用户体验。


三、HTTP缓存的作用


网络的其中一个特点就是不稳定性,很多用户受到网速慢的困扰。


服务器在大量用户访问的场景下实时计算数据也很容易产生瓶颈,导致服务变慢。从缓存技术具备的优势来看,很适合解决网络服务不稳定的问题。


四、HTTP缓存协议


协议是沟通过程中双方都遵守并且使用的一种规则。举个栗子,客户端和服务器两位大兄弟在新款机型问题上进行了几次沟通?


客户端:大哥,新款nex发布没?

服务器:老弟,还没发,你记住,别老来问我!


一周后......


客户端:大哥,我又来了,最新情况如何?

服务器:跟上次一样。


一个月后.....


客户端:大哥,这都一个月了,怎么样了啊?!

服务器:已经开售啦!


在这个例子里面,客户端与服务端沟通过程中就遵循某种规则,我们来看一下。


  • 数据部分:机型的内容;

  • 协议部分:1)别老来问我,2)最新情况如何,3)跟上次一样。


服务端说的这些话,客户端都能看懂并且明白这些话中所蕴含的意义,这就是客户端与服务端之间达成的某种通讯协议。


4.1 HTTP消息头


在介绍HTTP缓存协议之前,我们先来了解一下HTTP消息头的基础知识。我们对HTTP/HTTPS的数据请求都比较熟悉,在HTTP的数据请求中有一种信息叫做“头部信息”。


头部信息是在客户端请求或者服务端响应是传递给对方的一种信息。我们来看一下HTTP协议的组成部分。

HTTP 请求的组成

状态行、请求头、消息主体三部分组成。


HTTP 响应的组成

状态行、响应头、响应正文。


其中,请求头和响应头就是我们这里说的“头部信息”或者又叫“消息头”。那么头部信息有什么作用呢?


4.2 请求头


协议头
作用
Cookie携带cookie信息
User-Agent携带ua信息
Referer标识来源
Content-Length标识请求数据大小


如图所示:


4.3 响应头


协议头作用
server服务器的名称
data数据时间
Content-Length响应数据长度
Location重定向时的地址


如图所示:


我们今天要讲的缓存协议——Cache-Control, 也是放在消息头中进行控制的。


4.4 缓存协议


在第一节中,我们介绍了使用缓存技术的三个优势,在网络数据交换的过程中,使用缓存技术同样有这三个优势。


1)降低系统压力


使用HTTP缓存技术,可以有效的降低服务端的压力,服务端不需要实时计算数据并返回数据。


2)节省资源消耗


使用HTTP缓存技术,可以有效的避免大量的重复数据传输,降低流量消耗。


3)优化用户体验


使用HTTP缓存技术,本地缓存可以以较快的速度加载,减少用户等待时间。


在讲HTTP协议如何实现缓存之前,我们先来讲一下缓存类型。HTTP缓存一般被分为两类,私有缓存和共享缓存。


4.4.1 私有缓存


缓存被存储在设备本地或者独立的账户体系下,仅供当前用户使用,他可以用来降低服务器压力,提高用户体验,甚至实现离线浏览。



4.4.2 共享缓存


共享缓存是在代理服务器或者其他中间服务器中进行二次缓存的数据,一般这里我们常见的是CDN,这种缓存可以被多个用户访问,用来减少流量和延迟。



对于一次网络数据交互,本地缓存和共享缓存可以同时存在,HTTP协议中规定了如何进行控制这些缓存的使用和更新。在HTTP中,控制缓存有两种字段:一个是Pragma;另一个是cache-control。


Pragma 是一个在 HTTP/1.0 中定义的字段,从mozilla官网文档上查询,Pragma 支持现有的几乎所有浏览器。


但是作为旧时代的产物,cache-control正在逐步的替代它。cache-control 是从 HTTP/1.1开始引入的协议。有些前端开发者会选择在cache-control的基础上增加Pragma 来向下兼容,事实上android的webview即支持Pragma 又支持cache-control。


而当Pragma 和 cache-control 同时出现时,Pragma 的优先级大于cache-control 当然,这不是今天的重点,有兴趣的同学可以自行查阅相关资料。


下面我们就具体的来讲一下cache-control缓存协议的具体定义。HTTP协议规定,服务端通过响应头中的cache-control将缓存方式通知给客户端,同时客户端也可以通过请求头中的cache-control来将自己的缓存需求通知给服务器。


4.4.3 响应头中的cache-control


响应头中的cache-control一般有如下取值:

  • Cache-control: public

  • Cache-control: private

  • Cache-control: no-cache

  • Cache-control: no-store

  • Cache-control: no-transform

  • Cache-control: must-revalidate

  • Cache-control: proxy-revalidate

  • Cache-Control: max-age=

  • Cache-control: s-maxage=


4.4.4 请求头中的cache-control


请求头中的cache-control一般有如下取值:

  • Cache-Control: max-age=

  • Cache-Control: max-stale[=]

  • Cache-Control: min-fresh=

  • Cache-control: no-cache

  • Cache-control: no-store

  • Cache-control: no-transform

  • Cache-control: only-if-cached


mozilla开发者网站将这些取值分为如下几个类别进行描述。


4.4.5 可缓存性控制


public

表明响应可以被任何对象(包括:发送请求的客户端,代理服务器,等等)缓存,即使是通常不可缓存的内容。(例如:1.该响应没有max-age指令或Expires消息头;2. 该响应对应的请求方法是 POST 。)


private

表明响应只能被单个用户缓存,不能作为共享缓存(即代理服务器不能缓存它)。私有缓存可以缓存响应内容,比如:对应用户的本地浏览器。


no-cache

在发布缓存副本之前,强制要求缓存把请求提交给原始服务器进行验证(协商缓存验证)。


no-store

缓存不应存储有关客户端请求或服务器响应的任何内容,即不使用任何缓存。


4.4.6 缓存有效性控制


max-age=

设置缓存存储的最大周期,超过这个时间缓存被认为过期(单位秒)。与Expires相反,时间是相对于请求的时间。


s-maxage=

覆盖max-age或者Expires头,但是仅适用于共享缓存(比如各个代理),私有缓存会忽略它。


max-stale[=]

表明客户端愿意接收一个已经过期的资源。可以设置一个可选的秒数,表示响应不能已经过时超过该给定的时间。


min-fresh=

表示客户端希望获取一个能在指定的秒数内保持其最新状态的响应。


stale-while-revalidate=

 表明客户端愿意接受陈旧的响应,同时在后台异步检查新的响应。秒值指示客户愿意接受陈旧响应的时间长度。


stale-if-error= 

表示如果新的检查失败,则客户愿意接受陈旧的响应。秒数值表示客户在初始到期后愿意接受陈旧响应的时间。


4.4.7 重新验证和重新加载


must-revalidate

一旦资源过期(比如已经超过max-age),在成功向原始服务器验证之前,缓存不能用该资源响应后续请求。


proxy-revalidate

与must-revalidate作用相同,但它仅适用于共享缓存(例如代理),并被私有缓存忽略。


4.4.8 其他控制


no-transform

不得对资源进行转换或转变。Content-Encoding、Content-Range、Content-Type等HTTP头不能由代理修改。例如,非透明代理或者如Google's Light Mode可能对图像格式进行转换,以便节省缓存空间或者减少缓慢链路上的流量。no-transform指令不允许这样做。


only-if-cached

表明客户端只接受已缓存的响应,并且不要向原始服务器检查是否有更新的拷贝。


从这些描述以及分类中可以看出来,可缓存性控制+缓存有效性控制+其他控制 ,这几个控制维度是不冲突的,可以共同实现缓存的实现方式限定。


事实上cache-control确实是可以同时接受多个取值的,多个不同的指令可以搭配使用来对缓存进行控制。如果使用了相矛盾的多个指令取值,那么指令就会按照优先级进行缓存控制。


比如no-store和max-age这两种在行为上矛盾的指令取值放在一起下发,那么终端就只会按照no-store来进行缓存。


4.4.9 协议工作实战分析


专业的运维人员,一定很了解这些描述所表达的意思。然而作为客户端或者前端的我们,光是看这些专业术语,可能很难理解不同配置取值下实际的缓存效果。


因此为了搞明白取值对实际缓存效果的影响。我使用两台电脑,分别搭建了一个静态资源服务器(源服务器),一个代理服务器,通过模拟线上服务器的场景,来对常见的几种缓存控制模式进行验证。nginx的安装比较简单,此处不在赘述。


静态资源服务器(源服务器)


windows+nginx,配置如下:



代理服务器


windows+nginx,配置如下:



服务器搭建完成后,我们逐个改变cache-control的取值,来模拟几种常见的缓存控制模式,来帮助大家理解这些取值,加深印象。在日常的使用过程中,cache-control更多的是被放在响应头中来控制浏览的缓存行为,因此我们先来验证一下cache-control放在响应头中的情况。


场景:静态资源服务器(源服务器)的响应头中没有添加任何cache-control标识。没有添加标识,其实对应的就是public标识。

public通常可以看成默认值,如果我们不在响应中添加任何有关Cache-control的header,那么这次响应默认的处理逻辑就类似Cache-control: public。

(这里使用"通常","类似"这种不确定的字眼,需要解释一下,如果服务器返回了302或者307这种重定向响应时,添加Cache-control: public会让浏览器把重定向响应也缓存起来,但是如果不添加Cache-control,则不会缓存,也存在不同网络框架或者浏览器做不同处理的可能性)。


public的意思是浏览器或者代理服务器都可以对静态资源服务器(源服务器)返回的资源进行缓存。使用浏览器直接访问静态资源服务器(不经过代理服务器)。


第一次访问



第一次访问,服务器返回了200状态并将静态html传回给客户端。同时,服务器还带上了ETag和Last-Modified两个字段,我们先继续往下看。此时客户端做了几件事情:


  • 缓存了静态资源的内容;

  • 记录了该内容的ETag和Last-Modified。


点击浏览器刷新按钮



点击浏览器的刷新按钮后,客户端浏览器带上了第一次请求时返回的ETag和Last-Modified再次请求了服务器。服务端通过这两个参数认为客户端已经缓存了资源,服务器不需要再次返回资源了。于是服务器返回了304。


那如果有代理服务器掺和进来又是一个什么样的场景呢?还记得我们之前配置的那台代理服务器吗,我们将代理服务的代理缓存时间设定在了10秒。


第一次访问



点击浏览器刷新按钮



点击浏览器的刷新按钮时,客户端浏览器带上了第一次请求时返回的ETag和Last-Modified再次请求了服务器。服务端通过这两个参数认为客户端已经缓存了资源,服务器不需要再次返回资源了,于是服务器返回了304。


注意这次刷新时,ngiux-cache-status的状态时HIT标识这次命中了代理服务器的缓存,这次的客户端缓存有效性判断是由代理服务器完成的。


10秒后的第三次刷新



前面说了 代理服务器的缓存有效期,我们配置成了10秒。第三次刷新时服务器依然返回了304,资源不需要更新。


但是这次刷新时,ngiux-cache-status的状态是EXPIRED,这标识代理服务器的缓存已经失效了,不能用来做有效性判断,  这个时候,代理服务器就会将这次的请求透传给静态资源服务器(源服务器),通过静态资源服务器(源服务器)完成的缓存的有效性判断。


在这个过程中,代理服务器又会对自己的缓存进行更新,于是有了下面第四次。


第四次刷新



逻辑图如下;



通过这四次请求,我们能够清晰的了解了整个的逻辑,代理服务器在某些情况下直接代替了静态资源服务器(源服务器)。因为public指令告诉代理服务器,可以缓存数据,于是代理服务器按照配置将数据缓存了10秒,超过10秒后就会重新将请求转发给静态资源服务器(源服务器),同时重新进行缓存。


这时候有的同学会问了,代理服务器有缓存的时间限制,在没有达到时间限制之前是不会重新请求静态资源服务器(源服务器)的,这时候就降低了静态资源服务器(源服务器)的压力。那为什么在上面的例子里面,浏览器一直在请求代理服务器呢?


这里要跟大家说明一下,在上述的案例中,我们其实一直在点击浏览器的刷新按钮,刷新按钮的意思就是让客户端浏览器重新请求服务器来验证缓存内容的有效性。


大家仔细看下所有截图中的Request-Header 是不是都有一个max-age = 0 ,这个指令就是浏览器在刷新请求时,告诉服务器——我本地的缓存可能到期了,你要帮我验证一下。如果你尝试将网址复制到浏览器的新窗口然后点击回车打开url,而不是点击刷新按钮,这个时候就会像下图这样。



浏览器不会访问网络,注意看Status Code 那里括号里面的备注,Status Code:  200 OK (from disk cache)   表示这次的响应数据,其实是从磁盘缓存里面拿的。


在android系统的WebView中,正常情况下是没有提供刷新按钮的(除非开发者自己写一个)那么这种场景下webview就不会请求网络,每次都从磁盘缓存中拿数据,对应在抓包时,就看不到网络请求。


了解了整个逻辑之后,我们再来看mozilla提供的描述,再结合上述的逻辑,是不是就已经有了初步的概念了。


4.4.10 在响应头中的可缓存性控制


public

表明响应可以被任何对象(包括:发送请求的客户端,代理服务器,等等)缓存,即使是通常不可缓存的内容。(例如:1.该响应没有max-age指令或Expires消息头;2. 该响应对应的请求方法是 POST 。)这个其实就是我们刚刚验证的场景。


private

表明响应只能被单个用户缓存,不能作为共享缓存(即代理服务器不能缓存它)。私有缓存可以缓存响应内容,比如:对应用户的本地浏览器。


如果使用private,代表着这个资源,可以被私有用户缓存,缓存不会被共享,实际测试,当标注为private时,浏览器可以进行缓存,但是代理服务器不会缓存这个资源。有些材料里面提到,private是可以指定缓存的user_id的,这种属于比较复杂的配置了,有兴趣的同学可以研究下。


no-cache

强制要求缓存把请求提交给原始服务器进行验证(协商缓存验证)。


这是一个服务端经常使用的指令,也是一个比较容易与no-store混淆的指令,许多前端和客户端的同学都认为当服务端的响应中标注了no-cache,那么客户端就不会进行缓存,每次都会请求服务器获取新的内容。其实只说对了一半。


在这种场景下,浏览器确实会每次都请求服务器,但是并不意味着浏览器不缓存资源,mozilla的官方解释是“把请求提交给原始服务器进行验证”如果缓存没有问题,那么服务器就会返回304,让浏览器继续使用自己本地的缓存”。


no-store

不应存储有关客户端请求或服务器响应的任何内容,即不使用任何缓存。


这个指令就是完全不使用本地缓存,在这种模式下,客户端不会记录任何缓存,包括Etag等,每次都会重新发起请求,并且得到200响应和对应的数据。如果前端希望自己的网页完全不被缓存,那么可以试下这个指令。


以上指令解决了客户端以及代理服务器能不能缓存的问题,有的同学就会有疑问了,如果让客户端进行本地缓存,那么正常情况下如果不去手动刷新,客户端是不会请求服务器的,前端发新版后,客户端如何选择合适的时机请求服务器呢?


这个时候就要用到缓存有效性控制。浏览器和服务器之间的缓存校验是相互的 ,也就是说服务器可以告知浏览器 这个缓存你能用多久,能保留多久。


先来看下服务器是如何通知客户端缓存可以用多久的。缓存有效性控制指令一般会与可缓存性指令共同下发给客户端。



我们在server的header中增加max-age属性,同时,为了避免代理服务器提前将代理缓存置为无效,我们将代理服务器的缓存有效时间设置到100秒,超过静态资源服务器(源服务器)设置的max-age = 20。


第一次请求



我们使用刷新功能刷新浏览器,在20秒内我们持续得到HIT的状态,说明命中了代理服务器的缓存。20秒之后 代理服务器返回EXPIRED 说明代理服务器响应了静态资源服务器(源服务器)的指示,让本地代理失效了,而代理服务器设置的100秒本地缓存时间,这个时候被忽略了。


这次我们依然使用了浏览器的刷新功能来强制浏览器去服务器校验缓存的有效性,也就是说其实在上面的测试中,浏览器每次都是自己忽略max-age,去访问服务器的。

结论:新增的max-age,控制了代理服务器保留的缓存时长,本地代理会忽略配置中的缓存时长直接使用静态资源服务器(源服务器)下发的max-age作为缓存时长。


下面为了测试浏览器如何使用本地缓存,我们用android上的webview来进行实验,因为webview是没有刷新按钮的(除非开发者自己造一个)。


第一次打开;



打开后在后面我们每隔两秒再打开一次;



可以看到20秒内,webview都没有重复请求服务器下载站点的index.html,在上面的截图中,每显示一个favicon.ico就是我打开一次站点链接,因为我没有在源服务器中配置favicon.ico,所以每次打开,webview都在找服务器下载这个资源。


超过20秒后,webview发起了请求,此次服务器返回了304,要求客户端继续使用缓存进行展示,这次max-age指令体现出来了。而webview在这次校验之后,会将本地的缓存再延长20秒的有效期,在下一个20秒后,webview才会再次发起新的缓存验证请求。

总结:客户端webview会在public指令下缓存index.html,然后在max-age要求限制的时间内,都不会发起任何网络请求来校验资源。


在官网商城的一个案例中,网站上线后,运维没有配置任何cache-control协议,在默认public的模式下,客户端webview一直使用本地缓存,开发人员发现前端发版后,客户端无法及时更新页面。于是在每一个打开的网址后面手动拼接了一个时间戳,来强制改变网址,让浏览器的缓存失效,其实只要使用nocache或者max-age作为cache-control协议就可以解决该问题。


除了max-age,cache-control在可缓存性控制指令的基础上还可以增加如下几个控制;


no-transform

源服务端告诉客户端,客户端在缓存数据的时候不可以对文件进行改变,比如压缩,格式修改等...


must-revalidate

源服务端告知客户端,一旦资源过期,在向静态资源服务器(源服务器)发起验证之前,该资源不得使用。


proxy-revalidate

与must-revalidate作用相同,仅仅适用于共享缓存(例如代理)。


max-age=

静态资源服务器(源服务器)告知客户端,X秒内,客户端都不需要对缓存进行校验,可以直接使用。


s-maxage=

静态资源服务器(源服务器)告知代理服务器,代理服务器可以在X秒内使用该缓存,并且不需要进行校验,直接可以使用,但是客户端会忽略这个指令。


问题又来了,在验证的过程中,服务器是怎么判断浏览器的缓存是否有效的呢?


客户端浏览器在有机会访问服务器的时候就会告诉服务器,我的本地缓存是什么时候的数据(Last-Modified),数据内容是什么(ETag),这样服务端就能根据这两个值来判断客户端的缓存是否是有效的。


我们来模拟一次前端的发版操作,将index.html的内容进行修改;然后使用android webview进行请求。



这一次服务器毫不吝啬的返回了200和数据。大家仔细观察请求头和响应头;


  • 请求头中的if-None-Match 其实就是保持的上次服务器返回的ETag;

  • 请求头中的if-Modified-Match 其实就是保持的上次服务器返回的Last-Modified;


现在这两个值跟服务端的都对应不上了,所以服务器返回了最新的数据和200状态码,并且带上了最新的Etag,Last-Modified。而客户端下一次请求时,就会带上最新的Etag和Last-Modified。


在某些情况下,服务器返回的校验字段会不完整,比如缺失了Etag和Last-Modified中某一个,那么这种情况下的缓存校验就会存在风险。


在PC官网的一个案例中,源站点服务器返回了静态资源的Etag和Last-Modified,但是代理服务器,也就是CDN厂商在返回时将Etag给清除了,导致缺少了Etag校验。在正常情况下,服务器只使用文件的最后一次修改时间来做缓存校验也没啥问题。但是有这么一个用户,他的浏览器内缓存的静态资源损坏了,浏览器每次读取出来的资源无法使用,也就无法正常渲染页面,但是在每次与服务器校验资源的时候,服务器依然会告知客户端304(缓存可用)。这种场景下,只要源站点服务器不进行资源更新,也就是不变动这个Last-Modified,那么用户将永远打不开这个文件。


讲完了这些,差不多整个缓存协议的下行及交互部分大家已经略知一二了。剩下的就是缓存协议的上行部分了,所谓上行部分就是将cache-control写在浏览器访问的请求头上面。


前面我们也提过,浏览器的刷新请求,其实就是在请求头里面加了一个cache-control :max-age = 0 。这其实是告知服务器,客户端希望接收一个存在时间不大于0秒的缓存,一般的源服务器,特别是静态资源服务器,这个时候就会根据客户端的缓存情况返回200或者304。


4.4.11 在请求头中的可缓存性控制


no-cache

告知代理服务器,不直接使用缓存,要求向源服务器发起请求。


no-store

所有的文件都不缓存到本地或者临时文件夹中。


max-age

告知服务器客户端希望接收一个存在时间不大于X秒的资源。


max-statle

告知服务器客户端愿意接受一个超过缓存时间的资源,时间为X秒。


min-fresh

告知服务器客户端希望接收一个在小于X秒内被更新过得资源。


no-transform

告知代理服务器,不允许代理服务器对资源进行压缩,转化,比如有些代理服务器会对图片进行压缩,格式转换。


only-if-cached

告知代理服务器如果代理服务器有缓存内容,就直接给,不用再找源服务器要。


请求头中的缓存控制因为用的比较少,我就不过多的去解读了,有兴趣的同学可以去研究下。


五、总结


HTTP的cache-control协议规定了客户端,代理服务器,源服务器三者之间的缓存交互逻辑。做为客户端开发,经常出现一些与cache相关的问题在排查时无从下手,通过学习了解这部分内容,可以帮助快速的分析定位这部分问题。


前端同学熟悉cache-control的逻辑后,也可以根据业务的形态跟运维讨论自己缓存需求,有效的降低服务器的压力和用户的流量,提高网页打开速度。

Node 社群



我组建了一个氛围特别好的 Node.js 社群,里面有很多 Node.js小伙伴,如果你对Node.js学习感兴趣的话(后续有计划也可以),我们可以一起进行Node.js相关的交流、学习、共建。下方加 考拉 好友回复「Node」即可。



如果你觉得这篇内容对你有帮助,我想请你帮我2个小忙:

1. 点个「在看」,让更多人也能看到这篇文章
2. 订阅官方博客 www.inode.club 让我们一起成长

点赞和在看就是最大的支持

浏览 7
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报