http缓存

前言

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和响应报文头,浏览器只需要从缓存中获取信息即可。
从字面上看,就是说:从某个时间节点算起,是否文件被修改了

  1. 如果真的被修改:那么开始传输响应一个整体,服务器返回:200 OK
  2. 如果没有被修改:那么只需传输响应header,服务器返回:304 Not Modified

Last-Modified 有个问题,因为如果在服务器上,一个资源被修改了,但其实际内容根本没发生改变,会因为Last-Modified时间匹配不上而返回了整个实体给客户端(即使客户端缓存里有个一模一样的资源)。为了解决这个问题,HTTP1.1推出了Etag。

Etag

Etag:
服务器响应请求时,通过此字段告诉浏览器当前资源在服务器生成的唯一标识(生成规则由服务器决定)

If-None-Match:
再次请求服务器时,浏览器的请求报文头部会包含此字段,后面的值为在缓存中获取的标识。服务器接收到次报文后发现If-None-Match则与被请求资源的唯一标识进行对比。

  1. 不同,说明资源被改动过,则响应整个资源内容,返回状态码200。
  2. 相同,说明资源无心修改,则响应header,浏览器直接从缓存中获取数据信息。返回状态码304.

缓存的优点

  1. 减少了冗余的数据传递,节省宽带流量
  2. 减少了服务器的负担,大大提高了网站性能
  3. 加快了客户端加载网页的速度
    这也正是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);

两种缓存的区别

  1. 强制缓存
  • 设置强制缓存的方式就是 res.setHeader('Cache-Control','max-age=10')
  • res.setHeader('Expires', new Date(Date.now() + 10*1000).toGMTString())
  • 以上两种以第一种方式取决定作用
  1. 对比缓存
  • 通过时间对比 Last-Modified ---- if-modified-since
  • 通过标识对比 Etag ---- if-none-match

总结

缓存只能服务端配置!!!!,目前暂时没有找到客户端能做的事,html5的meta标签好像在也没什么用,感谢大佬指正

参考文档

聊聊web缓存那些事!

HTTP----HTTP缓存机制

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,921评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,635评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,393评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,836评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,833评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,685评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,043评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,694评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,671评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,670评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,779评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,424评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,027评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,984评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,214评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,108评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,517评论 2 343

推荐阅读更多精彩内容