前言
http的缓存是老生常谈的问题,基本面试专用的,看到的文章挺多的,但都是一些原理性的文章,基本没有真正实践过怎么缓存,就会形成一种道理大家都懂,但谁真正实践过呢,从而到用的时候,却发现不会怎么做缓存,如何配置呢,也一无所知。所以本文从理论和实践上试了一下怎么认识缓存。这里有借鉴前辈们的经验文章,但对于生产环境上服务器的缓存还是懵懂的,请各位大佬赐教,同时欢迎各位大佬指正。
缓存的规则
我们知道HTTP的缓存属于客户端缓存,后面会提到为什么属于客户端缓存。所以我们认为浏览器存在一个缓存数据库,用于储存一些不经常变化的静态文件(图片、css、js等)。我们将缓存分为强制缓存和协商缓存。下面我将分别详细的介绍这两种缓存的缓存规则。
强制缓存
当缓存数据库中已有所请求的数据时。客户端直接从缓存数据库中获取数据。当缓存数据库中没有所请求的数据时,客户端的才会从服务端获取数据。
协商缓存
又称对比缓存,客户端会先从缓存数据库中获取到一个缓存数据的标识,得到标识后请求服务端验证是否失效(新鲜),如果没有失效服务端会返回304,此时客户端直接从缓存中获取所请求的数据,如果标识失效,服务端会返回更新后的数据。
小贴士:
我们可以看到两类缓存规则的不同,强制缓存如果生效,不需要再和服务器发生交互,而对比缓存不管是否生效,都需要与服务端发生交互。
两类缓存规则可以同时存在,强制缓存优先级高于对比缓存,也就是说,当执行强制缓存的规则时,如果缓存生效,直接使用缓存,不再执行对比缓存规则。
缓存的方案
上面的内容让我们大概了解了缓存机制是怎样运行的,但是,服务器是如何判断缓存是否失效呢?我们知道浏览器和服务器进行交互的时候会发送一些请求数据和响应数据,我们称之为HTTP报文。报文中包含首部header和主体部分body。与缓存相关的规则信息就包含在header中。boby中的内容是HTTP请求真正要传输的部分。举个HTTP报文header部分的例子如下:
HTTP/1.1 200 OK
Server: nginx/1.14.0
Date: Sat, 09 Mar 2019 02:59:02 GMT
Content-Type: application/javascript; charset=utf8
Content-Length: 1544092
Last-Modified: Sat, 09 Mar 2019 00:52:32 GMT
Connection: keep-alive
ETag: "5c830e50-178f9c"
Accept-Ranges: bytes
Accept: */*
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
Connection: keep-alive
Host: xlink-iot.qinyuan.cn
If-Modified-Since: Fri, 08 Mar 2019 06:56:18 GMT
If-None-Match: "5c821212-178f9c"
Referer: https://xlink-iot.qinyuan.cn/iot/
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36
接下来我们将对HTTP报文中出现的与缓存规则相关的信息做出详细解释。(我们依旧分为强制缓存和协商缓存两个方面来介绍)
强制缓存
对于强制缓存,服务器响应的header中会用两个字段来表明——Expires和Cache-Control
Expires
Exprires的值为服务端返回的数据到期时间。当再次请求时的请求时间小于返回的此时间,则直接使用缓存数据。但由于服务端时间和客户端时间可能有误差,这也将导致缓存命中的误差,另一方面,Expires是HTTP1.0的产物,故现在大多数使用Cache-Control替代。
Cache-Control
Cache-Control有很多属性,不同的属性代表的意义也不同。
private:客户端可以缓存
public:客户端和代理服务器都可以缓存
max-age=t:缓存内容将在t秒后失效
no-cache:需要使用协商缓存来验证缓存数据
no-store:所有内容都不会缓存。
协商缓存
协商缓存需要进行对比判断是否可以使用缓存。浏览器第一次请求数据时,服务器会将缓存标识与数据一起响应给客户端,客户端将它们备份至缓存中。再次请求时,客户端会将缓存中的标识发送给服务器,服务器根据此标识判断。若未失效,返回304状态码,浏览器拿到此状态码就可以直接使用缓存数据了。
对于协商缓存来说,缓存标识我们需要着重理解一下,下面我们将着重介绍它的两种缓存方案。
Last-Modified
Last-Modified:
服务器在响应请求时,会告诉浏览器资源的最后修改时间。
if-Modified-Since:
浏览器再次请求服务器的时候,请求头会包含此字段,后面跟着在缓存中获得的最后修改时间。服务端收到此请求头发现有if-Modified-Since,则与被请求资源的最后修改时间进行对比,如果一致则返回304和响应报文头,浏览器只需要从缓存中获取信息即可。
从字面上看,就是说:从某个时间节点算起,是否文件被修改了
- 如果真的被修改:那么开始传输响应一个整体,服务器返回:200 OK
- 如果没有被修改:那么只需传输响应header,服务器返回:304 Not Modified
Last-Modified 有个问题,因为如果在服务器上,一个资源被修改了,但其实际内容根本没发生改变,会因为Last-Modified时间匹配不上而返回了整个实体给客户端(即使客户端缓存里有个一模一样的资源)。为了解决这个问题,HTTP1.1推出了Etag。
Etag
Etag:
服务器响应请求时,通过此字段告诉浏览器当前资源在服务器生成的唯一标识(生成规则由服务器决定)
If-None-Match:
再次请求服务器时,浏览器的请求报文头部会包含此字段,后面的值为在缓存中获取的标识。服务器接收到次报文后发现If-None-Match则与被请求资源的唯一标识进行对比。
- 不同,说明资源被改动过,则响应整个资源内容,返回状态码200。
- 相同,说明资源无心修改,则响应header,浏览器直接从缓存中获取数据信息。返回状态码304.
缓存的优点
- 减少了冗余的数据传递,节省宽带流量
- 减少了服务器的负担,大大提高了网站性能
- 加快了客户端加载网页的速度
这也正是HTTP缓存属于客户端缓存的原因。
问题
强缓存
1.服务器怎么返回数据到期时间,怎么缓存(需要服务器进行配置)
Cache-Control
let http = require('http')
let url = require('url')
let fs = require('mz/fs')
let path = require('path');
let p = path.resolve(__dirname);
http.createServer(async function(req, res) {
let {pathname} = url.parse(req.url);
let realPath = path.join(p, pathname);
console.log(realPath, '来请求服务了')
try{
let statObj = await fs.stat(realPath);
res.setHeader('Cache-Control','max-age=180') //强制缓存 180s内不需要再次请求服务器
//res.setHeader('Cache-Control','no-cache')
res.setHeader('Content-Type',require('mime').getType(realPath)+';charset=utf8')
fs.createReadStream(realPath).pipe(res)
}catch(e) {
res.statusCode = 404;
res.end('404')
}
}).listen(3000)
// 我们请求一个本地的文件index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<link rel="stylesheet" href="./index.css">
<!-- <meta http-equiv="Cache-control" content="no-cache"> -->
</head>
<body>
<img src="/he.png"/>
</body>
<script src="./index.js"></script>
</html>
返回头:
Cache-Control: max-age=180
Connection: keep-alive
Content-Type: text/html;charset=utf8
Date: Sat, 09 Mar 2019 06:42:58 GMT
Transfer-Encoding: chunked
上述中的文件,针对强制缓存,除html文件外其他的资源在180s内均在缓存中读取,另外强调一点:主网页只有对比缓存没有强制缓存,html文件每次都是重新请求服务器文件即使设置了<meta http-equiv="Cache-Control" content="max-age=180" />
expires
//Expires:Sun, 22 Jul 2018 02:43:42 GMT
//备注:如果Cache-Control和Expires同时存在,Cache-Control说了算
res.setHeader('Expires', new Date(Date.now() + 10*1000).toGMTString()) //强制缓存的另一种方式
2.客户端需要如何设置
没有,暂时找不到
协商缓存
//对比缓存 为了更加明显的看到对比缓存,我们将在以下的代码中都将强制缓存关闭
//res.setHeader('Cache-Control','no-cache')
//响应头设置了res.setHeader('Last-Modified',statObj.ctime.toGMTString())
//请求头就会带上req.headers['if-modified-since']
let http = require('http')
let url = require('url')
let util = require('util')
let fs = require('mz/fs')
let stat = util.promisify(fs.stat);
let path = require('path');
let p = path.resolve(__dirname);
http.createServer(async function(req, res) {
let {pathname} = url.parse(req.url);
let realPath = path.join(p, pathname);
console.log(realPath)
try{
let statObj = await fs.stat(realPath);
console.log(statObj)
// res.setHeader('Cache-Control','max-age=10') //强制缓存 10s内不需要再次请求服务器
res.setHeader('Cache-Control','no-cache')
res.setHeader('Content-Type',require('mime').getType(realPath)+';charset=utf8')
res.setHeader('Expires', new Date(Date.now() + 10*1000).toGMTString()) //强制缓存 因为上面设置了no-cache,所以这里的设置其实无效
let since = req.headers['if-modified-since'];
if (since === statObj.ctime.toGMTString()) {
res.statusCode = 304 //服务器的缓存
res.end();
} else {
res.setHeader('Last-Modified',statObj.ctime.toGMTString())
fs.createReadStream(realPath).pipe(res)
}
}catch(e) {
res.statusCode = 404;
res.end('404')
}
}).listen(3000)
返回头
Cache-Control: no-cache
Connection: keep-alive
Content-Type: text/html;charset=utf8
Date: Sat, 09 Mar 2019 07:22:17 GMT
Last-Modified: Sat, 09 Mar 2019 07:04:41 GMT
Transfer-Encoding: chunked
在浏览器打开localhost:3000/index.html 刷新看到就是304,我们返回状态码304,浏览器就乖乖地去读缓存中的文件了。我们稍微改动一下index.html就可以看到 200
Etag
//对比缓存
//Etag内容的标识
// 响应头设置了res.setHeader('Etag',statObj.size.toString()); 这里设置的是文件大小
//请求头就会带上req.headers['if-none-match'];
let http = require('http');
let util = require('util');
let fs = require('fs');
let stat = util.promisify(fs.stat);
let url = require('url');
let path = require('path');
let p = path.resolve(__dirname);
// 比较内容 stat.size
// 第一次请求Etag:内容的标识
// 第二次在请求我的时候 if-none-match
http.createServer(async function(req,res){
let {pathname} = url.parse(req.url);
let realPath = path.join(p,pathname);
try{
let statObj = await stat(realPath);
console.log(realPath)
res.setHeader('Cache-Control','no-cache');
let match = req.headers['if-none-match'];
if(match){
if(match === statObj.size.toString()){
res.statusCode = 304;
res.end();
}else{
res.setHeader('Etag',statObj.size.toString());
fs.createReadStream(realPath).pipe(res);
}
}else{
res.setHeader('Etag',statObj.size.toString());
fs.createReadStream(realPath).pipe(res);
}
}catch(e){
res.statusCode = 404;
res.end(`not found`);
}
}).listen(3000);
两种缓存的区别
- 强制缓存
- 设置强制缓存的方式就是 res.setHeader('Cache-Control','max-age=10')
- res.setHeader('Expires', new Date(Date.now() + 10*1000).toGMTString())
- 以上两种以第一种方式取决定作用
- 对比缓存
- 通过时间对比 Last-Modified ---- if-modified-since
- 通过标识对比 Etag ---- if-none-match
总结
缓存只能服务端配置!!!!,目前暂时没有找到客户端能做的事,html5的meta标签好像在也没什么用,感谢大佬指正