一文讲透HTTP缓存之ETag-程序员宅基地

技术标签: Web开发  html5  http  缓存  

无论是前端、后端或者运维同学,在平时的开发工作中,都会和HTTP缓存打交道,大家或多或少都了解HTTP缓存中的ETag字段,它是资源的特定版本的标识符,可以让缓存更高效,并节省带宽。本文系统性的阐述了ETag的起源、生成原理及使用。看完本文后,对于不了解ETag的同学能够知道ETag的来龙去脉,并能马上上手使用;对于熟悉ETag的同学也能做到温故而知新。

ETag定义及起源

ETag(Entity-Tag,下文简称:ETag)是万维网协议HTTP的一部分,它是 HTTP 为Web 缓存验证提供的多种机制之一,它允许客户端发出条件请求。这种机制允许缓存更有效并节省带宽,因为如果内容没有更改,Web 服务器不再需要发送完整的响应。

ETag 是由 Web 服务器分配给在URL中找到的特定版本资源的不透明标识符。如果该 URL 的资源表示发生了变化,则会重新分配一个新的 ETag。ETag 类似于指纹,可以快速进行比较以确定资源的两种表示是否相同。

ETag的正式提出是在 HTTP/1.1 协议的 rfc7232 文档中,引入 ETag 的目的主要是为了解决 Last-Modified 存在的一些问题:

  1. 一些文件也许会周期性的更改,但是他的内容并不改变(仅仅改变的修改时间),这个时候我们并不希望客户端认为这个文件被修改了,而重新发起GET请求
  2. 某些文件修改非常频繁,比如在秒以下的时间内进行修改,(比方说1s内修改了N次),If-Modified-Since能检查到的粒度是s级的,这种修改无法判断(或者说UNIX记录MTIME只能精确到秒)
  3. 某些服务器不能精确的得到文件的最后修改时间

HTTP/1.1协议虽然提出了 ETag,但并没有规定ETag的内容是什么或者说要怎么实现,唯一规定的是ETag的内容必须放在""内。

强校验和弱校验

ETag的格式为:ETag= [ weak ] opaque-tag,其中[ weak ]表示可选。ETag支持强校验和弱校验,它们的区别在于 ETag 标识符中是否存在一个初始的“W/”(即有无 [ weak ]),格式如下所示:

类型
ETag=“123456789” 强校验
ETag=W/“123456789” 弱校验(W大小写敏感)

强校验 ETag 匹配表明两个资源表示的内容是逐字节相同的,并且所有其它实体字段(例如 Content-Language)也未更改。强 ETag 允许缓存和重组部分响应,就像字节范围请求一样。

弱校验 ETag 匹配仅表明这两种表示在语义上是等效的,这意味着出于实际目的它们是可互换的并且可以使用缓存的副本。但是,资源表示不一定逐字节相同,因此弱 ETag 不适用于字节范围请求。弱 ETag 可能适用于 Web 服务器无法生成强 ETag 的情况,例如动态生成的内容

我们通过下面的例子来看看强、弱校验的匹配对比:

ETag1 ETag2 强校验 弱校验
W/“1” W/“1” 不匹配 匹配
W/“1” W/“2” 不匹配 不匹配
W/“1” “1” 不匹配 匹配
“1” “1” 匹配 匹配

ETag交互过程

ETag由服务器端生成,发送给客户端,客户端再次访问时通过传If-None-Match字段,服务端判断请求中的If-None-Match来验证资源是否修改。下面是协商缓存(ETag)的请求流程:
image.png

  1. 客户端发起GET请求
  2. 服务端接收、处理请求,返回Header值,里面包含ETag字段(如:ETag:“182ed89aac91e00e81c9b0c78de417f6”)
  3. 此时客户端再次发送请求,该请求头中就会携带上 If-None-Match字段,值是ETag返回的内容(如:If-None-Match: “182ed89aac91e00e81c9b0c78de417f6”)。服务器判断发送过来的If-None-Match字段的值与服务端计算出来的ETag值是否匹配,如果匹配,则返回304状态码,此时服务端不返回任何实体数据,即body为空;否则返回200,同时会返回最新的资源和ETag值;

例如当我们多次访问百度首页时,在控制面板中可以看到部分资源返回状态码是 304,其中Request Headers 和 Response Headers 出现了 ETag/If-None-Match 字段,这说明该资源走的是协商缓存策略。

以上就是协商缓存(ETag)的交互过程,接下来我们就来看看ETag的生成原理。

ETag生成原理

虽然HTTP协议没有规定ETag的生成方法,但为了避免使用过期的缓存数据,用于生成 ETag 的方法应保证(尽可能)每个 ETag 是唯一的。生成的 ETag 常用方法包括使用资源内容的抗冲突 散列函数、最后修改时间戳的散列值或一个修订号

我们接下来介绍的是koa框架中ETag的生成原理,其它框架/服务器生成的方式可能不太一致,但我们只需要了解其实现思路即可。

// https://github.com/koajs/ETag/blob/master/index.js

// 核心代码:生成ETag的函数(下面会具体分析)

const calculate = require('ETag')

// koa中间件,对ctx进行了处理

module.exports = function ETag (options) {

  return async function ETag (ctx, next) {

    await next()

    const entity = await getResponseEntity(ctx) // 获取body内容

    setETag(ctx, entity, options) // 生成ETag【重点】

  }

}

async function getResponseEntity (ctx) {

  // dosomething,最终返回body:return body

}

function setETag (ctx, entity, options) {

  if (!entity) return

  ctx.response.ETag = calculate(entity, options) // 生成ETag

}
https://github.com/jshttp/ETag/blob/master/index.js

// 核心代码:生成ETag的函数(承接上面)

module.exports = ETag

var crypto = require('crypto')

var Stats = require('fs').Stats

var toString = Object.prototype.toString

/** 为非Stats类型创建ETag */

function entitytag (entity) {

  if (entity.length === 0) {

    // fast-path empty

    return '"0-2jmj7l5rSw0yVb/vlWAYkK/YBwk"'

  }

  // compute hash of entity

  var hash = crypto.createHash('sha1').update(entity, 'utf8')

      .digest('base64').substring(0, 27)

  // compute length of entity

  var len = typeof entity === 'string'

    ? Buffer.byteLength(entity, 'utf8')

    : entity.length

  // 重点:长度(16进制)+hash(entity)值

  return '"' + len.toString(16) + '-' + hash + '"'

}

/** 生成ETag */

function ETag (entity, options) {

  // support fs.Stats object

  var isStats = isstats(entity)

  var weak = options && typeof options.weak === 'boolean' ? options.weak : isStats   

  // generate entity tag

  var tag = isStats ? stattag(entity) : entitytag(entity)

  // 弱ETag 比 强ETag 多了个 W/

  return weak ? 'W/' + tag : tag

}

/** 确定对象是否是 Stats 类型 */

function isstats (obj) {

  // genuine fs.Stats

  if (typeof Stats === 'function' && obj instanceof Stats) {

    return true

  }

  // quack quack

  return obj && typeof obj === 'object' &&

    'ctime' in obj && toString.call(obj.ctime) === '[object Date]' &&

    'mtime' in obj && toString.call(obj.mtime) === '[object Date]' &&

    'ino' in obj && typeof obj.ino === 'number' &&

    'size' in obj && typeof obj.size === 'number'

}

/** 为 Stats 类型创建ETag */

function stattag (stat) {

  var mtime = stat.mtime.getTime().toString(16)

  var size = stat.size.toString(16)

  // 重点:文件大小的16进制+修改时间

  return '"' + size + '-' + mtime + '"'

}

以上就是ETag的生成原理,总结如下:

ETag生成结论

1. 对于静态文件(如css、js、图片等),ETag的生成策略是:文件大小的16进制+修改时间

2. 对于字符串或Buffer,ETag的生成策略是:字符串/Buffer长度的16进制+对应的hash值

ETag如何生效

上面代码介绍了如何生成ETag,如果想让生成的ETag生效,还需要用到另一个koa中间件:koa-conditional-get,该中间件核心源码如下:

// https://github.com/koajs/conditional-get/blob/master/index.js

module.exports = function conditional () {

  return async function (ctx, next) {

    await next()

    // 调用 ctx 上的fresh属性

    if (ctx.fresh) { 

      ctx.status = 304

      ctx.body = null

    }

  }

}

上面的代码中,我们看到koa-conditional-get中间件实际上调用了 ctx 上的fresh属性,如果该属性返回 true ,则将状态码重置为304,同时清空body。我们接着看 ctx.fresh 属性是怎么进行判断的,代码如下:

// https://github.com/koajs/koa/blob/master/lib/request.js

// ctx中fresh属性如下

const fresh = require('fresh') // 真正判断的函数

get fresh () {

    const method = this.method

    const s = this.ctx.status



    // GET or HEAD for weak freshness validation only

    if (method !== 'GET' && method !== 'HEAD') return false



    // 2xx or 304 as per rfc2616 14.26

    if ((s >= 200 && s < 300) || s === 304) {

      return fresh(this.header, this.response.header) // 重点

    }



    return false

  }

ctx.fresh 属性的核心内容是引入了第三方库(fresh)来判断资源是否足够新鲜,fresh库的核心代码如下:

// https://github.com/jshttp/fresh/blob/master/index.js

// fresh 核心代码如下

module.exports = fresh

function fresh (reqHeaders, resHeaders) {

  // fields

  var modifiedSince = reqHeaders['if-modified-since']

  var noneMatch = reqHeaders['If-None-Match']

  // unconditional request

  if (!modifiedSince && !noneMatch) {

    return false

  }

  // Always return stale when Cache-Control: no-cache

  // to support end-to-end reload requests

  // https://tools.ietf.org/html/rfc2616#section-14.9.4

  var cacheControl = reqHeaders['cache-control']

  if (cacheControl && CACHE_CONTROL_NO_CACHE_REGEXP.test(cacheControl)) {

    return false

  }

  // If-None-Match

  if (noneMatch && noneMatch !== '*') {

    var ETag = resHeaders['ETag']

    if (!ETag) {

      return false

    }

    var ETagStale = true

    var matches = parseTokenList(noneMatch)

    for (var i = 0; i < matches.length; i++) {

      var match = matches[i]

      if (match === ETag || match === 'W/' + ETag || 'W/' + match === ETag) {

        ETagStale = false

        break

      }

    }

    if (ETagStale) {

      return false

    }

  }

  // if-modified-since

  if (modifiedSince) {

    var lastModified = resHeaders['last-modified']

    var modifiedStale = !lastModified || !(parseHttpDate(lastModified) <= parseHttpDate(modifiedSince))

    if (modifiedStale) {

      return false

    }

  }

  return true

}

根据源码,将fresh函数的代码判断逻辑进行了整理,总结如下。

只要满足右边紫色部分的判断条件之一,fresh即为true。需要注意的是,判断条件的优先级是从上到下,即如果ETagLast-Modified字段同时存在,ETag的优先级更高

ETag实战

上一节中我们分析了如何让ETag策略生效,加下来我们将通过一个例子来更加直观的了解ETag:

const Koa = require('koa');

const etag = require('koa-etag');

const conditional = require('koa-conditional-get');

const app = new Koa();

 // 放在 use(etag) 上面

app.use(conditional());

 // 使用 etag

app.use(etag());



 // 请求响应

app.use(async function(ctx, next){

  await next();

  ctx.body = {

    name: 'tobi',

    species: 'ferret',

    age: 2

  };

});



app.listen(3000, () => console.log('port 3000'));

从上图中的访问结果来看,ETag策略已生效。我们这里只给出了最基础的用法,其它用法可根据业务进行调整。

总结

本文首先介绍了ETag的基本定义,它是资源的特定版本的标识符,可以让缓存更高效,并节省带宽;接下来讲述了ETag的起源、验证类型以及服务端与客户端的交互流程;最后,我们通过koa-ETag库的源码具体分析了ETag的生成原理及实战应用,让我们对Etag有了更加直观、清晰的认识。

ETag的提出主要是为了解决Last-Modified验证机制存在的一些问题,但ETag也并非完美,在传统意义上,ETag 只能在单个服务器提供内容的网站上使用,对于像 Apache(2.4版本以下) 或 IIS 等多服务器提供资源的网站,ETag则无法正常工作。此时需要单独对Etag进行配置,请参考 Apache ETag IIS ETag

不同服务器对ETag的生成策略不尽相同,如果你的请求中返回的ETag值与本文的格式不一致,这是正常现象;如果你想自定义ETag生成算法,可以直接在上述的源码中进行修改,其它服务器请参考 NginxApache

参考

https://github.com/koajs/etag

https://github.com/koajs/conditional-get/blob/master/index.js

https://github.com/koajs/koa/blob/master/lib/request.js

https://github.com/jshttp/fresh/blob/master/index.js

https://datatracker.ietf.org/doc/html/rfc7232#section-2.3

https://www.cnblogs.com/yalong/p/15207547.html

https://juejin.cn/post/6844904133024022536#heading-19

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/weixin_42989576/article/details/123695991

智能推荐

非线性方程求解 matlab,MATLAB应用 求解非线性方程-程序员宅基地

文章浏览阅读2.8k次。《MATLAB应用 求解非线性方程》由会员分享,可在线阅读,更多相关《MATLAB应用 求解非线性方程(16页珍藏版)》请在人人文库网上搜索。1、第7章 求解非线性方程7.1 多项式运算在MATLAB中的实现一、多项式的表达n次多项式表达为:,是n+1项之和在MATLAB中,n次多项式可以用n次多项式系数构成的长度为n+1的行向量表示a0, a1,an-1,an二、多项式的加减运算设有两个多项式和..._对于分子分母都含有未知量的非线性方程是否可用matlab 求解

计算机应用简答,计算机应用基础简答题.doc-程序员宅基地

文章浏览阅读523次。《计算机应用基础简答题.doc》由会员分享,提供在线免费全文阅读可下载,此文档格式为doc,更多相关《计算机应用基础简答题.doc》文档请在天天文库搜索。1、计算机应用基础简答题1. 什么是操作系统?操作系统的作用是什么?答:操作系统在计算机结构中处于硬件裸机与软件系统之间的层次上,它不仅管理位于内层的硬件资源,而且管理和协调外层各种软件资源,为用户提供一种高效便捷的应用环境。操作系统是最基础的..._计算机应用简答题csdn

将labelme标注的人体姿态Json文件转成训练Yolov8-Pose的txt格式_labelme骨骼点json转yolo-程序员宅基地

文章浏览阅读1.5k次,点赞13次,收藏17次。最近在训练Yolov8-Pose时遇到一个问题,就是如何将自己使用labelme标注的Json文件转化成可用于Yolov8-Pose训练的txt文件。_labelme骨骼点json转yolo

Linux安装Mysql5.7_linux mysql 5.7-程序员宅基地

文章浏览阅读680次,点赞2次,收藏8次。安装mysql5.7_linux mysql 5.7

北大计算机博士蔡华谦,信科师生在北京大学国球联赛再次折桂-程序员宅基地

文章浏览阅读611次。北京大学国球联赛是北大师生均可参加的全校范围的乒乓球团体赛事,于5月的每周五晚上在邱德拔乒乓球厅分轮次举办。信科师生有着优秀的国球文化氛围。继今年4月夺得北大杯和硕博杯双冠后,信科师生乒乓球队5月再次出征2021年北京大学国球联赛,并最终成功卫冕,在本学期内豪取三连冠。方藤(中)与黄子蔚(右)正在对刘力锋教授(左)进行局间场外指导小组赛中,信科的对手有叉院城环联队、乒协联队等强强联合的劲敌。在连续..._北大蔡华谦

剑指offer 替换空格 C语言-程序员宅基地

文章浏览阅读395次,点赞5次,收藏8次。解析:这个题他给了很多方法,但是我还是更倾向于大部分人能想到的那种,就是从头往后依次找空格,找到一个就把它换成%20,那么就是把它当成数组,但是记得要提前遍历一遍,把那个空格需要的空间提前申请上,然后再进行for循环替换。数据范围:0≤���(�)≤1000 0≤len(s)≤1000。例如,当字符串为We Are Happy.则经过替换之后的字符串为We%20Are%20Happy。请实现一个函数,将一个字符串s中的每个空格替换成“%20”。

随便推点

Go 语言 Excel 类库 Excelize 2.0.0 版本发布-程序员宅基地

文章浏览阅读341次。开发四年只会写业务代码,分布式高并发都不会还做程序员? >>> Excelize 是 Go ..._excelize addcomment format

kettle 运行常见的报错_unexpected batch update error committing the datab-程序员宅基地

文章浏览阅读1w次,点赞3次,收藏30次。kettle 加工抽取数据到另一个数据库运行常见的报错:DB2 SQLCODE=-206, SQLSTATE=42703 定义表字段问题 解决办法 检查建表字段(要用大写),在DB2中,默认情况下所有的名称都可包含字符:A-Z(大写) 0到9 @、#、$和_(下划线),名称不能以数字和下划线开头 ;Error: SQLCODE=-302, SQLSTATE=22001, SQLERR..._unexpected batch update error committing the database connection.

Yolov5 多边形标签转换,所有json文件自动转成txt格式[详细过程]_多边形json转txt-程序员宅基地

文章浏览阅读8k次,点赞42次,收藏97次。注:labelme是麻省理工(MIT)的计算机科学和人工智能实验室(CSAIL)研发的图像标注工具,人们可以使用该工具创建定制化标注任务或执行图像标注,项目源代码已经开源。通过labelme对图进行标注后,得到的是json文件,而Yolov5对数据进行模型构建的时候,读取需要的是txt格式的文件。txt_outer_path——保存txt文本的文件夹的绝对路径。json_name——json_name是json文本的名字。json_floder——读取json的文件夹的绝对路径。_多边形json转txt

【图像加密解密】基于菲涅尔衍射相空间实现图像加密解密附Matlab代码-程序员宅基地

文章浏览阅读829次,点赞17次,收藏22次。图像加密解密一直是信息安全领域的重要研究课题。随着技术的不断发展,传统的加密解密方法已经不能满足当今社会对信息安全的需求。因此,基于菲涅尔衍射相空间的图像加密解密方法应运而生,成为当前研究的热点之一。菲涅尔衍射是一种光学现象,通过它可以将物体表面的微小振动转化为光波的振幅和相位的变化。而相空间则是描述光波的振幅和相位信息的数学空间。基于菲涅尔衍射相空间的图像加密解密方法利用了这一原理,将图像信息转化为光波信息,通过菲涅尔衍射的特性进行加密处理,实现对图像信息的安全传输和存储。

Dev-c++Debug,调试程序相关内容(防走坑)_dev c++ debug-程序员宅基地

文章浏览阅读2.3k次,点赞27次,收藏25次。来自小柴“傻逼”的抱怨和debug的好处入门Dev-c++ Debug需更改的操作入门操作后的Debug操作添加的变量为数组或者STL中的容器该如何去操作需要注意的问题和容易出错的点(建议看一下)_dev c++ debug

C# LIST 使用GroupBy分组_c# list groupby-程序员宅基地

文章浏览阅读1w次。https://blog.csdn.net/zhangxiao0122/article/details/88570472根据论坛及博客整理。原有list集合, List<CommodityInfo> commodityInfoList = new List<CommodityInfo>(); public class CommodityInfo { public string StoreID {get; set;} .._c# list groupby

推荐文章

热门文章

相关标签