无论是前端、后端或者运维同学,在平时的开发工作中,都会和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)的请求流程:

  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) returnctx.response.ETag = calculate(entity, options) // 生成ETag}
https://github.com/jshttp/ETag/blob/master/index.js// 核心代码:生成ETag的函数(承接上面)module.exports = ETagvar crypto = require('crypto')var Stats = require('fs').Statsvar toString = Object.prototype.toString/** 为非Stats类型创建ETag */function entitytag (entity) {if (entity.length === 0) {// fast-path emptyreturn '"0-2jmj7l5rSw0yVb/vlWAYkK/YBwk"'}// compute hash of entityvar hash = crypto.createHash('sha1').update(entity, 'utf8').digest('base64').substring(0, 27)// compute length of entityvar 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 objectvar isStats = isstats(entity)var weak = options && typeof options.weak === 'boolean' ? options.weak : isStats   // generate entity tagvar tag = isStats ? stattag(entity) : entitytag(entity)// 弱ETag 比 强ETag 多了个 W/return weak ? 'W/' + tag : tag}/** 确定对象是否是 Stats 类型 */function isstats (obj) {// genuine fs.Statsif (typeof Stats === 'function' && obj instanceof Stats) {return true}// quack quackreturn 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.jsmodule.exports = function conditional () {return async function (ctx, next) {await next()// 调用 ctx 上的fresh属性if (ctx.fresh) { ctx.status = 304ctx.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.methodconst s = this.ctx.status// GET or HEAD for weak freshness validation onlyif (method !== 'GET' && method !== 'HEAD') return false// 2xx or 304 as per rfc2616 14.26if ((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 = freshfunction fresh (reqHeaders, resHeaders) {// fieldsvar modifiedSince = reqHeaders['if-modified-since']var noneMatch = reqHeaders['If-None-Match']// unconditional requestif (!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.4var cacheControl = reqHeaders['cache-control']if (cacheControl && CACHE_CONTROL_NO_CACHE_REGEXP.test(cacheControl)) {return false}// If-None-Matchif (noneMatch && noneMatch !== '*') {var ETag = resHeaders['ETag']if (!ETag) {return false}var ETagStale = truevar 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 = falsebreak}}if (ETagStale) {return false}}// if-modified-sinceif (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());// 使用 etagapp.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生成算法,可以直接在上述的源码中进行修改,其它服务器请参考 Nginx、Apache。

参考

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

一文讲透HTTP缓存之ETag相关推荐

  1. 10自带sftp服务器_一文讲透FTP和SFTP的区别

    阅读本文约需要10分钟,您可以先关注我们或收藏本文,避免下次无法找到. FTP和SFTP都是文件传输协议,我们知道FTP使用的是20和21端口,SFTP使用的是22端口.另外,SFTP前面的S应该是S ...

  2. js打印线程id_一文讲透“进程,线程和协程”

    一文讲透"进程,线程和协程" 本文从操作系统原理出发结合代码实践讲解了以下内容: 什么是进程,线程和协程? 它们之间的关系是什么? 为什么说Python中的多线程是伪多线程? 不同 ...

  3. 双线macd指标参数最佳设置_一文讲透双线MACD指标及其实战运用

    原标题:一文讲透双线MACD指标及其实战运用 船长的舍得交易体系技术理论模型中,我们要用到两大指标,分别是均线系统和双线MACD指标. 很多小伙伴都喜欢用双线MACD这个指标,但是90%的人都不知道其 ...

  4. 【敏捷开发】一文讲透敏捷管理中的DoR、DoD与AC

    文章目录 一.需求侧:DoR 案例: DoR是什么? 如何建立DoR的标准? DoR样例 1.需求 2.交互 3.架构 二.研发侧:DoD DoD是什么? 如何建立DoD的标准? DoD样例 三.用户 ...

  5. 一文讲透『大神修炼心法』!35岁让自己过的越来越好!

    Cocos 的老铁,如果你这几天没有被麒麟子给卷到?那说明你还没有真正进入 Cocos 圈子里来.为什么这么说呢?看下面. 3月1号 23:57 | 2800+字 麒麟子全方位解读 Cocos Cyb ...

  6. 【hadoop】一文讲透hdfs的delegation token

    1.概述 转载并且补充:一文讲透hdfs的delegation token 最近我也在研究这个,学习一下. 1.1 起因 我最近在做FLink kerberos认证.我在flink配置文件中配置正确的 ...

  7. 一文讲透植物内生菌研究怎么做 | 微生物专题

    内容导览 1. 隐秘而强大的植物内生菌 2. 难以区分的植物内生菌 3. 更好的植物内生细菌测序方法 3.1 LNA-16S测序鉴定内生细菌原理 3.2 LNA-16S测序鉴定内生细菌占比高达99% ...

  8. cstring只获取到第一个数_一文讲透 Dubbo 负载均衡之最小活跃数算法

    (给ImportNew加星标,提高Java技能) 作者:why技术(本文来自作者投稿) 本文是对于Dubbo负载均衡策略之一的最小活跃数算法的详细分析.文中所示源码,没有特别标注的地方均为2.6.0版 ...

  9. itstime后面跟什么_一文讲透什么是引流

    这个问题老生常谈,都快腻了,还是有人时不时问老马.究其原因,很多人从想做引流.到动手操作,整个流程都是懵逼的状态. 引流不难,难的是一直卡在某个阶段,或者一直停留在那里.这样,你做再多次引流,还是患得 ...

最新文章

  1. Centos 7.4 中http-2.4 的基本实现和 https 的实现
  2. How to configure a different backend system for OData consumption
  3. NLP事件抽取综述(上中下):中文事件抽取、开放域事件抽取、事件数据生成、跨语言事件抽取、小样本事件抽取、零样本事件抽取等类型
  4. axios vue 动态date_Web前端Vue系列之-Vue.js 实战
  5. R7-6 A-B (20 分)
  6. 华为云MVP:来自工业制造领域的微服务与云平台实践
  7. 深度学习福利入门到精通第二讲——AlexNet模型
  8. 为什么说Java中要慎重使用继承
  9. 2019年湖南-粤港澳大湾区投资贸易洽谈周4月举行
  10. java读取配置文件和获得项目根目录
  11. 软工网络15团队作业4——Alpha阶段敏捷冲刺-3
  12. 菲律宾国防部长洛伦扎纳参观中国海军539编队芜湖舰
  13. Hitfilm Express下载
  14. 办公技巧:PDF转DWG格式的两种简单方法
  15. AI简史 | 星际英雄传说
  16. 怎么将flv格式转换成mp4,四个步骤完成转换
  17. 2022最新批量删除微博教程!支持批量删除微博批量删除清空点赞关注评价以及粉丝
  18. c语言标准差函数std,std函数
  19. 【C语言典例】——day4:加油站加油【Switch】
  20. iOS:苹果企业证书通过网页分发下载安装app

热门文章

  1. 4字节 经纬度_经纬度表示方法
  2. web前端开发需要哪些工具和需要学习什么?
  3. 初效过滤器初阻力和终阻力
  4. C++ primer plus 学习中的疑惑与思考(1)
  5. linux的strdup与strndup
  6. php实现图片相似搜索
  7. Android 自定义View (一)
  8. html js 点击隐藏div,javascript实现显示和隐藏div方法汇总
  9. 后验概率与条件概率区别
  10. 【路径规划】基于matlab A_star算法机器人动态避障【含Matlab源码 2571期】