文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

HTTP 中的 ETag 是如何生成的?

2024-12-03 00:56

关注

本文将介绍如何利用 ETag 和 If-None-Match 来实现缓存控制。此外,还将介绍 HTTP 中的 ETag 是如何生成的。不过在此之前,我们得先来简单介绍一下 ETag。

一、ETag 简介

1.1 ETag 是什么

ETag(Entity Tag)是万维网协议 HTTP 的一部分。它是 HTTP 协议提供的若干机制中的一种 Web 缓存验证机制,并且允许客户端进行缓存协商。这使得缓存变得更加高效,而且节省带宽。如果资源的内容没有发生改变,Web 服务器就不需要发送一个完整的响应。

1.2 ETag 的作用

ETag 是一个不透明的标识符,由 Web 服务器根据 URL 上的资源的特定版本而指定。如果 URL 上的资源内容改变,一个新的不一样的 ETag 就会被生成。ETag 可以看成是资源的指纹,它们能够被快速地比较,以确定两个版本的资源是否相同。

需要注意的是 ETag 的比较只对同一个 URL 有意义 —— 不同 URL 上资源的 ETag 值可能相同也可能不同。

1.3 ETag 的语法 

  1. ETag: W/"<etag_value> 
  2. ETag: "<etag_value>

1.4 ETag 的使用

在大多数场景下,当一个 URL 被请求,Web 服务器会返回资源和其相应的 ETag 值,它会被放置在 HTTP 响应头的 ETag 字段中: 

  1. HTTP/1.1 200 OK  
  2. Content-Length: 44  
  3. Cache-Control: max-age=10  
  4. Content-Type: application/javascript; charset=utf-8  
  5. ETag: W/"2c-1799c10ab70" 

然后,客户端可以决定是否缓存这个资源和它的 ETag。以后,如果客户端想再次请求相同的 URL,将会发送一个包含已保存的 ETag 和 If-None-Match 字段的请求。 

  1. GET /index.js HTTP/1.1  
  2. Host: localhost:3000  
  3. Connection: keep-alive  
  4. If-None-Match: W/"2c-1799c10ab70" 

客户端请求之后,服务器可能会比较客户端的 ETag 和当前版本资源的 ETag。如果 ETag 值匹配,这就意味着资源没有改变,服务器便会发送回一个极短的响应,包含 HTTP “304 未修改” 的状态。304 状态码告诉客户端,它的缓存版本是最新的,可以直接使用它。 

  1. HTTP/1.1 304 Not Modified  
  2. Cache-Control: max-age=10  
  3. ETag: W/"2c-1799c10ab70"  
  4. Connection: keep-alive 

二、ETag 实战

2.1 创建 Koa 服务器

了解完 ETag 相关知识后,将基于 koa、koa-conditional-get、koa-etag 和 koa-static 这些库来介绍一下,在实际项目中如何利用 ETag 响应头和 If-None-Match 请求头实现资源的缓存控制。 

  1. // server.js  
  2. const Koa = require("koa");  
  3. const path = require("path");  
  4. const serve = require("koa-static");  
  5. const etag = require("koa-etag");  
  6. const conditional = require("koa-conditional-get");  
  7. const app = new Koa();  
  8. app.use(conditional()); // 使用条件请求中间件  
  9. app.use(etag()); // 使用etag中间件  
  10. app.use( // 使用静态资源中间件  
  11.   serve(path.join(__dirname, "/public"), {  
  12.     maxage: 10 * 1000, // 设置缓存存储的最大周期,单位为秒  
  13.   })  
  14. );  
  15. app.listen(3000, () => {  
  16.   console.log("app starting at port 3000");  
  17. }); 

在以上代码中,我们使用了 koa-static 中间件来处理静态资源,这些资源被保存在 public 目录下。在该目录下,创建了 index.html 和 index.js 两个资源文件,文件中的内容分别如下所示:

2.1.1 public/index.html 

  1. >  
  2. <html lang="zh-cn">  
  3. <head>  
  4.     <meta charset="UTF-8">  
  5.     <meta http-equiv="X-UA-Compatible" content="IE=edge">  
  6.     <meta name="viewport" content="width=device-width, initial-scale=1.0">  
  7.     <title>ETag 使用示例title>  
  8.     <script src="/index.js">script>  
  9. head>  
  10. <body>  
  11.     <h3>ETag 使用示例h3>  
  12. body>  
  13. html> 

2.1.2 public/index.js

console.log("大家好,");

在启动完服务器之后,我们打开 Chrome 开发者工具并切换到 Network 标签栏,然后在浏览器地址栏输入 http://localhost:3000/ 地址,接着多次访问该地址(地址栏多次回车)。下图是多次访问的结果:

2.2 ETag 和 If-None-Match

下面将以 index.js 为例,来分析上图中与之对应的 HTTP 报文。对于 index.html 文件,感兴趣的小伙伴可以自行分析一下。接下来我们先来分析首次请求 index.js 文件的报文:

2.2.1 首次请求 — 请求报文 

  1. GET /index.js HTTP/1.1  
  2. Host: localhost:3000  
  3. Connection: keep-alive  
  4. Pragma: no-cache  
  5. Cache-Control: no-cache  
  6. ... 

2.2.2 首次请求 — 响应报文 

  1. HTTP/1.1 200 OK  
  2. Content-Length: 44  
  3. Cache-Control: max-age=10  
  4. ETag: W/"2c-1799c10ab70"  
  5. ... 

在使用了 koa-static 和 koa-etag 中间件之后,index.js 文件首次请求的响应报文中会包含 Cache-Control 和 ETag 的字段信息。

Cache-Control 描述的是一个相对时间,在进行缓存命中的时候,都是利用客户端时间进行判断,所以相比较 Expires,Cache-Control 的缓存管理更有效,安全一些。

2.2.3 10s内 — 请求报文 

  1. GET /index.js HTTP/1.1  
  2. Host: localhost:3000  
  3. Connection: keep-alive  
  4. Pragma: no-cache  
  5. Cache-Control: no-cache  
  6. ... 

2.2.4 10s内 — 响应信息(General) 

  1. Request URL: http://localhost:3000/index.js  
  2. Request Method: GET  
  3. Status Code: 200 OK (from memory cache)  
  4. Remote Address: [::1]:3000  
  5. Referrer Policy: strict-origin-when-cross-origin 

2.2.5 10s内 — 响应信息(Response Headers) 

  1. Cache-Control: max-age=10  
  2. Connection: keep-alive  
  3. Content-Length: 44  
  4. ETag: W/"2c-1799c10ab70" 

由于我们设置了 index.js 资源文件的最大缓存时间为 10s,所以在 10s 内浏览器会直接从缓存中读取文件的内容。需要注意的是,此时的状态码为:Status Code: 200 OK (from memory cache)。

2.2.6 10s后 — 请求报文 

  1. GET /index.js HTTP/1.1  
  2. Host: localhost:3000  
  3. Connection: keep-alive  
  4. If-None-Match: W/"2c-1799c10ab70"  
  5. Referer: http://localhost:3000/  
  6. ... 

因为 10s 之后,缓存已经过期了,而且在 index.js 文件首次请求的响应报文中也返回了 ETag 字段。所以此时浏览器会发起 If-None-Match 条件请求。这类请求可以用来验证缓存的有效性,省去不必要的控制手段。

2.2.7 10s后 — 响应报文 

  1. HTTP/1.1 304 Not Modified  
  2. Cache-Control: max-age=10  
  3. ETag: W/"2c-1799c10ab70"  
  4. Connection: keep-alive  
  5. ... 

因为文件的内容未发生改变,所以 10s 后的响应报文的状态码为 304 Not Modified。此外,响应报文中也返回了 ETag 字段。看到这里,有一些小伙伴可能会有疑惑 —— ETag 到底是如何生成的?接下来,将带大家一起来揭开 koa-etag 中间件背后的秘密。

三、如何生成 ETag

在前面的示例中,我们使用了 koa-etag 中间件来实现资源的缓存控制。其实该中间件的实现并不复杂,具体如下所示: 

  1. // https://github.com/koajs/etag/blob/master/index.js  
  2. const calculate = require('etag');  
  3. // 省略部分代码  
  4. module.exports = function etag (options) {  
  5.   return async function etag (ctx, next) {  
  6.     await next()  
  7.     const entity = await getResponseEntity(ctx)  
  8.     setEtag(ctx, entity, options)  
  9.   }  

由以上代码可知,在 koa-etag 中间件内部会先通过 getResponseEntity 函数来获取响应实体对象,然后再调用 setETag 函数来生成 ETag。而 setETag 函数的实现很简单,在 setETag 函数内部,会通过 etag 这个第三方库来生成 ETag。 

  1. // https://github.com/koajs/etag/blob/master/index.js  
  2. function setEtag (ctx, entity, options) {  
  3.   if (!entity) return  
  4.   ctx.response.etag = calculate(entity, options)  

etag 这个库对外提供了一个 etag 函数来创建 ETag,该函数的签名如下: 

  1. etag(entity, [options]) 

了解完 etag 函数的参数之后,我们来看一下该函数的具体实现: 

  1. function etag (entity, options) {  
  2.   if (entity == null) {  
  3.     throw new TypeError('argument entity is required')  
  4.   }  
  5.   // 支持fs.Stats对象  
  6.   // isstats 函数的判断规则:当前对象是否包含ctime、mtime、ino和size这些属性  
  7.   var isStats = isstats(entity)  
  8.   var weak = options && typeof options.weak === 'boolean'  
  9.     ? options.weak  
  10.     : isStats  
  11.   // 参数校验  
  12.   if (!isStats && typeof entity !== 'string' && !Buffer.isBuffer(entity)) {  
  13.     throw new TypeError('argument entity must be string, Buffer, or fs.Stats') 
  14.   }  
  15.   // 生成ETag标签  
  16.   var tag = isStats  
  17.     ? stattag(entity) // 处理fs.Stats对象  
  18.     : entitytag(entity)  
  19.   return weak  
  20.     ? 'W/' + tag  
  21.     : tag  

在 etag 函数内部会根据 entity 的类型,执行不同的生成逻辑。如果 entity 是 fs.Stats 对象,则会调用 stattag 函数来创建 ETag。 

  1. function stattag (stat) {  
  2.   // mtime:Modified Time,是在写入文件时随文件内容的更改而更改,是指文件内容最后一次被修改的时间。  
  3.   var mtime = stat.mtime.getTime().toString(16)  
  4.   var size = stat.size.toString(16)  
  5.   return '"' + size + '-' + mtime + '"'  

而如果 entity 参数非 fs.Stats 对象,则会调用 entitytag 函数来生成 ETag。其中 entitytag 函数的具体实现如下: 

  1. function entitytag (entity) {  
  2.   if (entity.length === 0) {  
  3.     return '"0-2jmj7l5rSw0yVb/vlWAYkK/YBwk"'  
  4.   }  
  5.   // 计算实体对象的哈希值  
  6.   var hash = crypto  
  7.     .createHash('sha1')  
  8.     .update(entity, 'utf8')  
  9.     .digest('base64')  
  10.     .substring(0, 27)  
  11.   // 计算实体对象的长度  
  12.   var len = typeof entity === 'string'  
  13.     ? Buffer.byteLength(entity, 'utf8')  
  14.     : entity.length  
  15.   return '"' + len.toString(16) + '-' + hash + '"'  

对于非 fs.Stats 对象来说,在 entitytag 函数内部会使用 sha1 消息摘要算法来生成 hash 值并以 base64 格式输出,而实际的生成的 hash 值会取前 27 个字符。此外,由以上代码可知,最终的 ETag 将由实体的长度和哈希值两部分组成。

需要注意的是,生成 ETag 的算法并不是固定的, 通常是使用内容的散列、最后修改时间戳的哈希值或简单地使用版本号。

四、ETag vs Last-Modified

其实除了 ETag 字段之外,大多数情况下,响应头中还会包含 Last-Modified 字段。它们之间的区别如下:

五、总结

本文首先介绍了 ETag 的相关基础知识,然后以 Koa 为例详细介绍了 ETag 和 If-None-Match 是如何实现缓存控制的。此外,还分析了 koa-etag 中间件内部依赖的 etag 第三方库是如何为指定的实体生成 ETag 对象。最后,列举了 ETag 与 Last-Modified 之间的主要区别。 

 

来源:前端大全内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-后端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯