参考资源
- 图书:《图解HTTP》
- 博客:[1] AlloyTeam Web缓存机制系列
- 博客:[2] 浅谈浏览器http的缓存机制
- 博客:[3] 浏览器缓存知识小结及应用
- Doc: developers.google.com/working-with-indexdb
关于缓存的文章能搜索到很多,标题通常包含关键字 HTTP缓存、浏览器缓存 或 Web缓存。本篇尝试以易懂的、容易操作方式介绍Web缓存的基础知识。
1. 通用概念
1.1 什么是Web缓存
顾名思义Web缓存就是一个Web资源(html、js、图片等)在客户端和服务器之间的副本。研究Web缓存就是研究它们的机制,然后在实际应用中通过合理使用Web缓存提高便利性。
1.2 Web缓存的分类
Web缓存的分类可以按照数据请求过程分为:
-
应用层缓存
- 网页可以从本地文件中读取数据,将常用的数据记录到内存中。
- 利用HTML5提供的技术,将数据存储在localStorage等地方。
-
HTTP缓存
- 跟浏览器缓存是一个概念,它是基于HTTP标准协议实现的一套机制。
-
服务器端缓存
- 分为代理服务器缓存 和 CDN缓存。
-
数据库缓存
- 对关系复杂的是数据库查询能极大的提高响应效率。
其中,HTTP缓存在实际应用受到的关注最多,也是本篇重点介绍的部分。
1.3 Web缓存的作用
- 减少网络带宽,降低运营成本
- 减低服务器压力
- 加快页面响应速度,提供给更好的用户体验
2. 准备
2.1 HTTP基础
2.1.1 HTTP的请求报文
- 报文结构分为为:报文首部、空行、报文主体
- 按照是请求还是响应,报文类型分为:请求报文、响应报文
- 请求报文和响应报文的首部有一些不同
- 请求首部由,请求行、请求首部字段、通用首部字段、实体首部字段、其他 组成
- 请求行,请求方法和使用的HTTP协议
- 请求首部字段,请求特有的首部字段
- 通用首部字段,请求和响应都有的首部字段
- 实体首部字段,用于描述报文主题的字段
- 其他,非协议内但是因为其他用途被补充进来的字段
- 响应首部由,状态行、响应首部字段、通用首部字段、实体首部字段、其他 组成
- 状态行,请求的结果状态码和使用的HTTP协议
- 响应首部字段,响应特有的首部字段
- 通用首部字段,同上
- 实体首部字段,同上
- 其他,同上
- 请求首部由,请求行、请求首部字段、通用首部字段、实体首部字段、其他 组成
2.1.2 响应报文中的状态码
记录在HTTP中的规范多大40多种,最近常使用的有14中,用途的大致介绍如下:
- 2XX 表明请求被正常处理了
- 3XX 表明需要执行某些特殊的请求才能正确处理请求
- 4XX 客户端发生了错误
- 5XX表明服务器发生了错误
后文中需要特别留意的是200和304状态,分别表示请求正常返回和使用浏览器的缓存。
2.2 调试工具Fiddler
2.2.1 安装和配置Fiddler
Fiddler是一款流行的抓包工具,最初只运行在Windows上,但是现在也提供了在其他环境的安装包,但是需要首先安装Mono,基于Mono才能运行Fiddler。如果是在Mac上,可以参考如下这篇文章对Fiddler进行配置:
注:原文存在一些问题和说明不够细致的地方,如下步骤有所补充说明
- 下载Mono
- 配置
// 下载受信任的证书
/Library/Frameworks/Mono.framework/Versions/5.10.1/bin/mozroots --import --sync
// 打开.bash_profile 文件
sudo vi ~/.bash_profile
// 增加执行目录
export MONO_HOME=/Library/Frameworks/Mono.framework/Versions/5.10.1
export PATH=$PATH:$MONO_HOME/bin
// 使.bash_profile应用有效
source ~/.bash_profile
- 下载Fiddler
- 执行
mono32 Fiddler.exe
直接执行mono会报64错误注意查看mono的help说明
2.2.2 使用Fiddler
运行Fiddler以后,可以看到所有的请求的报文信息,也可以在Fiddler下的执行命令行中输入形如 bpu localhost:8080
的命令来调试特定的请求。比如我们访问了 localhost:8080以后,Fiddler会默认在Request请求中出现断点。
可以修改请求首部,也可以通过break on response
将断点停在响应中来修改响应首部,也可以直接break complete
跳过断点。
如上的使用方法可以用来对下文中提到的各种缓存首部字段进行模拟,从而模拟出效果。
3. HTTP缓存
2.1 HTTP缓存的的分类
强缓存
浏览器在加载资源时,先根据这个资源的一些首部判断它是否命中,如果命中,浏览器直接从自己的缓存中读取资源,不会发请求到服务器。
协商缓存
当强缓存没有命中的时候,浏览器一定会发送一个请求到服务器,通过服务器端依据资源的另外一些首部验证这个资源是否命中协商缓存,如果协商缓存命中,服务器会将这个请求返回,但是不会返回这个资源的数据,而是告诉客户端可以直接从缓存中加载这个资源。
一些特点
- 强缓存不发请求到服务器,协商缓存会发请求到服务器。
- 如果命中,都是从客户端缓存中加载资源,而不是从服务器加载资源数据。
- 当协商缓存也没有命中的时候,浏览器直接从服务器加载资源数据。
2.2 强缓存
在Chrome浏览器上打开常用的站点,打开network查看请求,可以看到请求的返回状态是200,但是size一栏显示的是 from disk cache 或 from memory cache的请求,这些请求的响应就属于命中了强缓存。
2.2.1 强缓存的实现
强缓存是利于Expires和Cache-Control这两个通用首部中的字段来控制的。这两个首部字段可以都使用也可以只启用一个,同时存在时Cache-Control的优先级更高。
Expires
- 在HTTP/1.0中提出,由服务器返回用于表示资源的过期时间,描述的是绝对时间
- 在浏览器第一次请求资源的时候,响应的首部中会包含Expires
- 浏览器时收到资源以后,把资源(连同首部)一起缓存下来
- 浏览器再次请求的时候,在缓存中找到该资源然后用Expires跟当前时间比较
- 如果命中,则使用缓存中的资源(使用的首部也是之前缓存的)
- 如果没命中,则从服务器请求资源,并且首部中的字段也会被更新
Cache-Control
- 在HTPP/1.1中提出,配置时间使用的时长是绝对量
Cache-Control: max-age=30
- 在浏览器第一次请求资源的时候,响应的首部中会包含Cache-Control
- 浏览器时收到资源以后,把资源(连同首部)一起缓存下来
- 浏览器再次请求的时候,在缓存中利用首部的Cache-Control计算一个过期时间
- 如果当前时间早于过期就命中,直接使用缓存中的资源
- 否则就从服务器中加载资源
2.2.2 强缓存的配置
(1)可以通过代码来设置是否启用强缓存。
java.util.Date date = new java.util.Date();
response.setDateHeader("Expires",date.getTime()+20000); // 缓存一定时间
response.setDateHeader("Expires", 0); // 不缓存
// 浏览器和缓存服务器都(public:可以缓存, no-cache:不缓存)
response.setHeader("Cache-Control", "public/no-cache");
(2)也可以通过Web服务器来设置是否启用强缓存
多数服务器都提供了配置项可以对是否启用强缓存进行配置,使用时可自行查阅。
2.3 协商缓存
在Chrome的network中查看请求,发现不少是状态为304状态描述为Not Modified,这意味请求命中了协商缓存。
2.3.1 协商缓存的实现
当浏览器请求某个资源没有命中强缓存,就会发请求到服务器去验证协商缓存,如果命中,服务器会返回304告知浏览器使用协商缓存,即使用浏览器的缓存中加载。
协商缓存可以使用 Last Modified 和 If-Modified-Since,以及 Etag 和 If-None_match 这两种方式来实现。
Last Modified 和 If-Modified-Since
- 在浏览器第一次请求服务器的资源时,会在响应首部中增加Last-Modified代表资源在服务器上的最后修改时间
- 浏览器再次请求资源时,会在请求首部上加上If-Modified-Since,它的值是响应首部返回的Last-Modified的值。
- 服务器收到了If-Modified-Since以后,会跟服务器资源的最后修改时间比对,如果发现没变化,则返回304,相应的不会再往响应首部中增加Last-Modified
- 浏览器收到304后,会从浏览器缓存中加载资源
- 如果没有命中协商缓存,浏览器从服务器加载资源,并更新响应首部中Last-Modified的值,下次在请求资源的时候If-Modified-Since就会使用这个新值。
ETag 和 If-None-Match
- 浏览器第一次跟服务器请求一个资源时,服务器返回资源的同时,会在响应首部中增加ETag这个属性,这个属性代表了服务器根据当前请求的资源生成的唯一标识。
- 浏览器再次请求这个资源时,就会在请求首部中增加If-None-Match,使用的是响应首部首部中返回的ETag的值。
- 服务器接收到资源请求时,会根据传过来的If-None-Match和资源生成的新的ETag来进行比较,如果相同则返回304,浏览器收到304后,会从浏览器缓存中加载资源
- 如果If-None-Match新生成的ETag不同,则连同资源和新生成的ETag一起返回给浏览器
说明:
- ETag这个标识是个字符串,只要资源有变化标识就会不同,而不以修改时间为依据。
- ETag跟Last-Modified不同的地方时,不管是否跟之前返回的ETag是否一致都会返回。
2.3.2 协商缓存的管理
多数Web服务器都会开启协商缓存,并且是同时启用 Last Modified 和 If-Modified-Since,以及 ETag 和 If-None-Match 。
分布式系统应该保证多台机器的文件Last-Modified一致,应该尽量关闭掉ETag(因为每台机器生成的ETag都不一样),否则会导致对比失败。
协商缓存必须配合强缓存一起使用,因为不设置强缓存,意味着协商缓存每次都要请求服务器资源会加大服务器的负荷。
2.4 缓存的实践
实践中的积累太少,需要在开发中不断积累实例,待补充...
3 应用层(浏览器)缓存
3.1 应用层缓存介绍
3.1.1 manifest
manifest离线缓存技术是为了在网络连接断开时候也能正常浏览网站,因为设计实时的数据请求部分无法缓存,因此该技术的适用场景有限,即只适用于哪些内容相对固定的页面。
实现上分为客户端和服务器端两个配置:
- 客户端,需要在页面通过形如
来指定缓存内容
- 服务器,配置
index.appcache
文件,里面指定了哪些内容可/不可缓存
3.1.2 Storage, Database, Cache
Storage: localStorage, sessionStorage
两者都是比较新的API,在每个浏览器上的具体使用可能会有不同,这里以Chrome为例。除了可以通过在Application中的对应栏中编辑Storage以外,还可以通过代码来添加和删除属性对。
localStorage 和 sessionStorage的区别碍于作用方位,localStorage会存在浏览器端跨Tab通用,并且不会随着浏览器关闭而丢失;sessionStorage则只在当前的浏览器Tab中有效,关闭以后就丢失了(但是刷新以后仍旧能保持)。
Database: IndexDB, Web SQL
IndexDB是一个客户端的用于存储结构化数据的API,它所涉及的概念跟关系型数据库中非常类似,可以使用前端代码来方便地进行仿
数据库方式的数据管理。可以参考 working-with-indexdb进一步了解IndexDB.
Cache: Service Worker Caches, Application Cache
待补充...
3.2 应用层缓存实践
待补充...