网络之 HTTP缓存详解

473 阅读5分钟

Q1: 为什么要引入缓存

A:

S1 减少 冗余的数据传输,节约网络带宽

S2 减少 服务器的负担

S3 提高 客户端的 加载速度


Q2 如何设置缓存

A:

S1 对于不常变化的资源,可以设置 "强缓存"

  • 服务器设置 响应头字段 "Cache-Control: max-age=N"
  • 服务器设置 响应头字段 "Expires: Thu, 10 Nov 2017 08:45:11 GMT`
  • Cache-control 的优先级高于 Expires
  • 强制缓存的状态码为 200
// node代码
res.setHeader( 'cache-control', 'max-age=N' )
res.setHeader( 'Expires', new Date( Date.now() + N * 1000 ).toGMTString() )

/**
cache-control的相关注意点
  1. max-age 是 "生存时间",时间的计算起点是 响应报文的创建时刻(即 Date字段,也就是离开服务器的时刻),而不是 客户端收到报文的时刻,也就是说 包含了在链路传输过程中  所有节点所停留的时间
  2. max-age的时长单位为 秒
  3. "max-age=0" 一般表示 不使用缓存,请求最新数据,类似于 "no-cache"的效果

  4. 客户端设置 请求头字段 "Cache-Control: max-age=xxxx"
    - 浏览器 "刷新"时,会在请求头自动加上 Cache-Control: max-age=0;
    - Ctrl+F5 的 "强制刷新",会发送 "Cache-Control: no-cache"

cache-control其他值的含义
  1. public:   资源允许被 中间服务器缓存
  2. private: 资源不允许 被中间代理服务器缓存
  3. no-store:  禁止使用缓存,每一次都要重新请求数据
  4. no-cache: 可以缓存,但在使用缓存前 要先去服务器验证是否过期,过期了就得重新获取资源
  5. must-revalidate:  如果缓存不过期就可以继续使用;但过期了 就必须去服务器重新验证

**/

用一张图表示为: 强缓存流程图

S1.2 "强缓存"有以下缺点

  • 如果缓存的文件A 内容发生了变化 + 缓存还未过期 时,资源A 无法获取到最新内容;
  • 在 "强缓存"时间到期时,有可能A的内容并未发生变化,这时候其实也希望能使用 "缓存机制"

所以除了"强缓存", HTTP 还引入了 "协商缓存"机制

S2 设置 "协商缓存"

  • 协商缓存: 由服务器根据 资源内容是否发生变化 来判断 缓存是否失效
  • Last-Modified & If-Modified-Since: Mon, 10 Nov 2018 09:10:11 GMT: 记录 资源最后一次被修改的时间
  • Etag & If-None-Match: 文件内容 标识摘要
  • Etag 的优先级高于 Last-Modified
  • 协商缓存的状态码为 304
// node代码1- 协商缓存
const crypto = require('crypto')
const hash = (value) => {
    return crypto.createHash('md5').update(value).digest('base64')
}

res.setHeader('cache-control','max-age=5')

fs.stat(filePath, (err, statObj) => {
  if (err) {
    res.statusCode = 404;
    return res.end('Not Found')
  }

  let ctime = statObj.ctime.toGMTString()
  res.setHeader('Last-Modified',ctime)    // 标识文件的 最后修改时间
  let since = req.headers['if-modified-since']

  // 根据特定的标识组成md5 比如文件的大小
  let md5 = hash( fs.readFileSync(filePath) )
  res.setHeader('Etag',md5)
  const ifNoneMatch = req.headers['if-none-match']

  // 同时满足内容和修改时间都未变化
  if( ifNoneMatch == md5 && since === ctime ) {
    res.statusCode = 304
    return res.end()
  }

  // .......
   
})


// 代码2- md5示例
const crypto = require('crypto')
let hash = crypto.createHash('md5').update('hello world').digest('base64')
console.log(hash)

/**
Last-Modified & If-Modified-Since 的缺点
  1. 当资源内容撤销修改时,文件内容实质没有发生变化,但还是会 更新Last-Modified值 ==>
      内容没有变,但是修改时间变化了

  2. 时间单位最低是秒, 不能识别1s内的资源内容 发生了变化,  所以 不会更新Last-Modified值 ==>  1s之内的变化 是监控不到的

MD5的特点
  1. md5是摘要算法,而不是加密算法(加密后能解密回来的 才是加密算法)
  2.摘要的内容如果相同,那么摘要的结果就一样(即自变量只有摘要内容,算法是公开通用的)
  3.md5是非可逆的(雪崩效应),不能反解(但是可以通过暴力碰撞出 摘要内容)

ETag的特点
  1. 强ETag 要求资源在字节级别必须完全相符;
     弱ETag 在值前有个“W/”标记,只要求资源在语义上没有变化,但内部可能会有部分发生了改变(如 HTML 里的标签顺序调整,或者多了几个空格)

Q5 什么是代理 和 代理缓存

A:

S1 代理,是指 一个服务器本身不生产内容,而是处于中间位置,转发上下游的请求和响应

S2 通过代理,可以在这个 "承上启下"的环节,设置以下功能:

  • 负载均衡:把外部流量 合理地分散到多台源服务器,以提高整体效率和性能;
  • 健康检查:使用 "心跳" 等机制监控后端服务器,发现有故障就及时 "踢出"集群;
  • 安全防护:保护 被代理的后端服务器,限制IP地址或流量,抵御网络攻击和过载;
  • 数据过滤:拦截上下行的数据,任意指定策略 修改请求或者响应;
  • 内容缓存:暂存/复用服务器响应

S3 代理相关头字段

  • 通用字段via:报文经过一个代理节点,代理服务器就会把自身信息 追加到 字段末尾;
  • X-Forwarded-For / X-Real-IP:获取到 客户端的 真实IP地址;
  • 专门的"代理协议" 可以在不改动原始报文的情况下 传递客户端的真实IP

S4 代理作为服务器 ==> 向客户端转发响应时的 缓存设置:

  • s-maxage:只限定缓存 在代理上能够 生存多久;
  • must-revalidate:只要过期就必须回 源服务器验证;
  • proxy-revalidate:只要求代理的缓存过期后必须验证,客户端不必回源
  • no-transform:代理专用属性, 禁止代理对资源做 转化处理

用图理解为: 代理缓存流程图1

S5 代理作为客户端 ==> 接收源服务器响应的 缓存设置:

  • max-stale:如果代理上的缓存过期了也可以接受,但不能过期太多(x秒内);
  • min-fresh: 缓存必须有效
  • only-if-cached:表示只接受代理缓存的数据,不接受源服务器的响应

用图理解为: 代理缓存流程图2

参考文档

01 珠峰架构-缓存

02 透视HTTP协议- 20~22小节

03 一文读懂前端缓存