HTTP协议之缓存(三)

广义上的缓存不仅仅是Web缓存,还包括了像数据库缓存,服务端缓存,浏览器缓存,应用缓存等。我对于缓存的理解是,一份资源或数据,在他不改变的情况下,只有第一次的传输时有意义的。如果需要重新计算或发送一份完全相同的资源,会造成很大的浪费。这里要谈到的Web缓存指的是HTTP协议中规定的对请求资源的备份和缓存。

<!--more-->

参考:

1. 缓存的类别

在进行事务的时候,如果缓存发现请求的资源已经被存储,它会拦截请求,返回该资源的拷贝,而不会去源服务器重新下载。这么做的好处是:

  • 减轻了服务器的压力,服务器不必为来自同一个客户端的资源请求进行重复处理
  • 提高了客户端的加载速度,从本地或者就近的缓存中读取资源,比从遥远的服务器获取资源要快得多

缓存可以是单用户独有的(私有缓存),也可以是多用户共享的(公有缓存):

1.1. 私有缓存

私有缓存是个人的缓存,不需要很大的动力或存储空间,因此可以做的很小。通常的,我们所说的浏览器缓存就是私有缓存:浏览器将常用文档存储缓存在个人电脑的磁盘和内存中。打开谷歌浏览器输入在地址栏chrome://version/,然后就可以看见一个“个人资料路径”的目录地址,里面就是浏览器为我们保存的缓存,但是可能看见的并不是常规的html,img,css这些类型的文件。

C:\Users\admin\AppData\Local\Google\Chrome\User Data\Default\Cache

浏览器会记录访问过的网页,当再次访问这个URL时,如果网页没有更新(怎么判断网页有没有更新这个问题,是由浏览器缓存规则规定的,下面会提到),就不会向服务器发送请求,而是直接使用本地缓存的网页,这样就不用发起任何网络请求了。

私有缓存能够有效减少网络请求,在某些时候,即使一个页面文档更新了,但是文档里面的引用资源(如样式表,脚本文件)等也是可以直接使用缓存的。但是,私有缓存的便利也会带来一些问题,比如在开发中,怎么刷新,页面的修改都不会生效(然后就找BUG找到怀疑人生...)。因此了解浏览器的缓存规则是缓存这章学习的一个重点。

1.2. 公共缓存

公共缓存是特殊的共享代理服务器,被称为缓存代理服务器。代理服务器向服务器请求并将资源备份在本地,然后接收来自多个用户的请求并代替服务器返回请求资源,这么做相当于把服务器的压力分担给了缓存代理服务器。

可能你会有疑问,这样的做法每次不还是会请求服务器吗,并且还多了一倍的请求(客户端到缓存,缓存到服务器)。这样做是有道理的,传输一个不包含主体的304响应报文,显然比传输一个完整的资源主体要快多了吧。通常地,缓存代理服务器距离用户较近,从而将多个从客户端直接到遥远的服务器的请求缩短为多个客户端到较近的代理服务器的请求,而代理服务器到源服务器的请求就只需要一份就可以了。

而实际中,缓存代理服务器也是层次化的,距离客户端最近的代理,可能并不是直接从源服务器获取资源,而是从另外一个公共的缓存代理服务器请求资源,这么做的理由是:在靠近客户端的地方使用小型廉价的缓存,而在更高层次中则逐步采用更大,功能更强的缓存。

有些网络结构也会构造更复杂的网状缓存,而不是简单的层次结构,关于公共缓存的实现这里就不深入了,我们主要的目的是了解如何使用公共缓存。

1.3. 内容分发网络

内容分发网络(CDN)是构建在网络之上的内容分发网络,基本原理就是广泛采用各种缓存服务器,关键技术主要有内容存储和分发技术。貌似CDN已经脱离了缓存的范围,但是这里我觉得还有有比较跟缓存一起对比学习。 待补充...

2. 缓存的控制

服务器可以通过HTTP定义的几种方式来指定文档在过期之前可以将其缓存多长时间。

2.1. Cache-Control

缓存控制方式保存在报文的Cache-Control首部行中,根据优先级递减的顺序,下面是该首部行的取值列表:

  • no-store,禁止缓存对响应进行复制
  • no-cache,可以对响应进行复制,但在与原始服务器进行新鲜度再验证之前,缓存不能将该资源提供给客户端
  • must-revalidate,在事先没有跟原始服务器进行再验证的情况下,不能提供该资源的陈旧副本,但可以提供该资源的新鲜副本(这里有点没明白,缓存不是只保存新鲜副本吗?)
  • max-age,表示从服务器传递文档开始,该文档处于新鲜状态的时间(单位:秒),如果设置为Cache-Control: max-age=0则表示不缓存

2.2. Expries

另外还可以添加Expries首部行,指定资源的过期时间,这个过期时间指的过期日期而不是新鲜度秒数,由于很多服务器的时间并不同步,因此最好使用新鲜度秒数,而不是绝对过期时间来保存缓存,这也是Expries优先级不如Cache-Control的原因

2.3. 试探性过期

最后,服务器不添加任何过期信息,让缓存自己确定过期时间。如果响应报文中即没有Cache-Control,也没有Expries,那么缓存就会根据某些算法,计算一个试探性最大试用期。这些算法可能将资源的最后修改时间作为依据,来推测在未来的某段时间内资源改变的可能性,从而生成资源过期时间,这里就不细究了。

2.4. 浏览器限制

浏览器都有刷新(F5或者ctrl+r,貌似也称为重载)的功能,可以强制对私有缓存或公共缓存中可能存在的过期内容进行刷新,其原理是:刷新按钮会发布一个新鲜度再验证请求(再验证的问题也在后面),其具体行为取决于特定的浏览器,文档以及缓存的设置。 另外,浏览器还提供了强制刷新的功能ctrl+F5,表示无条件地从服务器获取新资源,而不在意缓存是否已过期。这个功能在开发的时候遇见某些缓存的问题很好用哦。

3. 缓存的处理流程

缓存的基本工作流程是什么样子的呢?对于一条普通的HTTP GET请求报文的缓存处理可以分为下面几个部分:

  • 接收,缓存从网络中读取抵达的请求报文
  • 解析,缓存对报文进行解析,提取URL和首部行
  • 查询,缓存检测URL资源是否有本地资源可用,如果没有,就像源服务器发起请求获取一份,跳过下一步;如果有,直接进行下一步。本地副本可能存放在内存,本地磁盘甚至是附近的另外一台计算机中
  • 新鲜度检测,缓存查看已缓存副本是否足够新鲜,如果不是,则询问服务器是否有更改
  • 创建响应,缓存用新的首部和已缓存的主体来构建一条响应报文
  • 发送,缓存通过网络将响应发送给客户端
  • 日志,缓存可以选择性的创建一个日志来描述这个事务

这个流程当然不是上面这短短的一个列表就能描述清楚的,比如如何解析请求,如何查询缓存是否存在等问题的内部细节,我们现在仍一无所知。但是针对上面这个流程,我觉得目前最需要理解的是新鲜度检测

前面提到使用缓存对服务器和客户端都有好处,但是原始服务器的内容可能发生变化,如果不及时进行缓存新鲜度检测,则访问获得的资源则可能是过期的,这显然不是我们想要的。如果缓存提供的总是过期的资源,就会变得毫无意义。因此HTTP必须提供某些机制来保证缓存数据和服务器数据之间的一致性,这些机制可分为两类:

  • 文档过期
  • 服务器再验证

3.1. 文档过期

前面提到,HTTP通过特殊的报文首部行Cache-ControlExpries首部可以对缓存进行控制,他们的主要作用是在每个资源前增加了一个“过期日期”。在缓存资源副本持续时间过期之前,缓存都可以将这些资源视为新鲜的(因为这个过期时间本身是由服务器设置并告知的),且可以以任意频率使用这些副本,而无须与服务器联系。

由于Cache-Control可以定义文档的最大使用期,设置更细致,而Expries使用的是依赖于服务器的绝对过期时间,因此,目前建议使用前者。

不过,需要注意的是,当用户点击了刷新按钮之后,不论缓存资源是否已经过期,浏览器都会向源服务器发送请求,服务器此时就会进行再验证。

3.2. 服务器再验证

服务器再验证指的是缓存在返回它所保存的资源副本之前,会向该资源的的源服务器发送一个再验证请求。通常服务器再验证发生在:

  • 用户点击刷新按钮
  • 服务器接收到条件请求

点击刷新按钮这个问题这里就不谈了,什么是条件请求呢?这个问题在阅读《计算机网络-自顶向下方法》时了解到的,现在终于得到了真正的答案。

向GET请求报文中添加一些特殊的条件首部,就可以发起条件GET,只有条件为真时,服务器才返回对象。

条件首部有If-Match,If-None-Match,If-modified-Since,If-Unmodified-Since,If-Range这五个,从名称上就可以大致得知其含义,其中对缓存再验证最有用的两个是:If-modified-SinceIf-None-Match

3.2.1. If-modified-Since

MDN参考文档 服务器根据在指定日期之后的请求资源的状态采取相应措施

  • 如果已经发生修改过,服务器返回该资源对象,
  • 如果该资源内容没有变化,服务器返回一个304 Not Modified响应码(不会返回资源主体)。此时缓存知道该资源仍旧是最新的,则会再次将该资源标记为新鲜的,并且把该资源返回给客户端
  • 如果服务已经把该资源删除掉了,则会返回404响应,此时缓存也会将该资源副本一并删除

If-modified-Since,可以与前面介绍的文档过期时间很好的结合起来使用。如果文档没有发生变化,则可以更新缓存资源副本的过期时间(通过响应首部Last-Modified),从而延长缓存的生命周期,有效减少向服务器的请求次数。

3.2.2. If-None-Match

MDN参考文档 单独使用日期进行服务器再验证有时候略显乏力:

  • 日期的精度(秒级)可能不够准确(这里我有个以为:如果是更新频率为亚秒级别的资源,为什么还要使用缓存呢?)
  • 资源的改动并不是十分重要的,没必要每次修改都返回给客户端(相当于服务器可以有选择性地决定是否更新缓存)

为了解决上面问题,服务器为资源提供了特殊的版本标识符(ETag),并且当资源改变可以修改相应的标识符(当然也可以不修改,这个完全由服务器决定了)。服务器对于If-None-Match的处理与If-modified-Since类似,区别在于使用请求报文的标识符(包含在If-None-Match首部中)而不是修改日期进行比较。

由于标识符让服务器有了更多的选择,因此If-None-Match的优先级要大于If-modified-Since,如果请求报文中同时包含这两个首部,服务器会优先验证资源标识符而不是资源修改时间,则只有当他们都满足时,服务器才返回304 NOT Modified

另外,需要注意的仍旧是F5刷新的问题。尽管刷新会忽略文档过期时间向服务器发送请求,但是这个请求是会进行服务器再验证的(即可能返回304响应)。但是,如果使用强制刷新ctrl + F5,则浏览器不会发送条件GET而是直接向服务器请求最新的资源。

貌似有在代码层方面对强制刷新进行服务器再验证的,这里就没有深入了。

4. 缓存的设置

不同的服务器设置缓存首部行的方式存在差异。除了可以在服务端修改配置文件直接设置缓存,也可以在页面文档中使用标签指定缓存。

4.1. Apache设置缓存

可以在mod_header模块中设置,我用的是Apache,应该是需要先打开headers_module,然后在httpd.conf中设置,具体的语法参考文档

<IfModule headers_module>
    Header set Cache-control no-cache
</IfModule>

服务器这边我还是不要折腾了。

4.2. HTML文档设置

HTML文档的meta标签之前的文章中提到过,我们可以使用http-equiv来显示地指定HTTP首部行,当然也设置缓存信息了

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

浏览器在解析文档的时候,发现相关的指令就会按照对应的缓存控制来缓存当前文档。

5. 小结

本文整理了缓存的相关概念,主要是了解了缓存的原理和处理流程,对于缓存的实际操作和应用却没有进行实践。缓存跟前面整理的资源和报文联系十分紧密,也是前端开发中一个很需要注意的问题,因此还有必要进行深入的学习,慢慢来吧,接下来就是关于HTTP中的用户识别了。