loading

HTTP 缓存优化:深入解析 RFC 9110 中的 ETag 与 Last

  • Home
  • Blog
  • HTTP 缓存优化:深入解析 RFC 9110 中的 ETag 与 Last

HTTP 缓存优化:深入解析 RFC 9110 中的 ETag 与 Last

HTTP 缓存优化:深入解析 RFC 9110 中的 ETag 与 Last-Modified 验证器

2025-11-19

我会使用友好、清晰的简体中文进行说明,并重点介绍如何在实际应用中利用这些字段。

在 HTTP 协议中,验证器字段(Validator Fields) 是用于判断一个缓存的响应是否仍然有效(fresh),或者一个请求的资源是否发生了变化的关键机制。它们帮助客户端(如浏览器)和代理服务器避免重复下载相同的内容,从而提高性能并减少带宽消耗。

RFC 9110 的 8.8 节主要定义了两个主要的验证器字段

ETag (Entity Tag) 实体标签

Last-Modified 最后修改时间

定义 ETag 是一个服务器生成的不透明(opaque)字符串,它代表了所请求资源特定版本的内容。如果资源的内容发生了哪怕一个字节的改变,服务器就应该生成一个新的 ETag。

用途 它是强验证器,因为它基于内容本身。它用于与客户端请求中的条件字段(If-None-Match 或 If-Match)进行比较。

示例 ETag: "686897696a7c67425f67a2558586c"

客户端在随后的请求中发送其拥有的 ETag,询问服务器资源是否仍是旧版本。

客户端发送 If-None-Match: "686897696a7c67425f67a2558586c"

服务器响应

内容未变 返回 304 Not Modified 状态码,不包含消息体。客户端使用缓存。

内容已变 返回 200 OK,包含新的资源内容和新的 ETag。

故障/问题If-None-Match 逻辑错误

问题 服务器忘记检查 If-None-Match 字段,导致每次都发送完整的 200 OK 响应,使 ETag 失去缓存优化的作用。

Node.js/Express 示例(修复后)

// 假设这是您的 Express 路由处理函数

app.get('/api/resource', (req, res) => {

const resourceContent = '

Hello World!

';

const currentEtag = '"686897696a7c67425f67a2558586c"'; // 实际应用中会基于内容动态生成

// 1. 设置 ETag 响应头

res.set('ETag', currentEtag);

// 2. 检查客户端是否发送了 If-None-Match 头部

const clientEtag = req.headers['if-none-match'];

// 3. 如果客户端的 ETag 与服务器当前 ETag 匹配

if (clientEtag === currentEtag) {

// *** 关键步骤:返回 304 Not Modified ***

console.log('ETag matched. Sending 304.');

return res.status(304).send(); // 不要发送消息体

}

// 4. ETag 不匹配或客户端没有发送,返回完整内容

console.log('ETag mismatch or first request. Sending 200.');

res.status(200).send(resourceContent);

});

定义 Last-Modified 是一个 HTTP 日期时间戳,表示资源的最后修改时间。

用途 它是弱验证器,因为它基于时间。它用于与客户端请求中的条件字段(If-Modified-Since 或 If-Unmodified-Since)进行比较。

示例 Last-Modified: Tue, 15 Nov 1994 12:45:26 GMT

客户端在后续请求中发送其拥有的 Last-Modified 时间戳,询问服务器资源在该时间之后是否被修改过。

客户端发送 If-Modified-Since: Tue, 15 Nov 1994 12:45:26 GMT

服务器响应

未在指定时间后修改 返回 304 Not Modified。

已在指定时间后修改 返回 200 OK,包含新的资源内容和新的 Last-Modified。

故障/问题时间戳格式错误或比较不精确

问题 服务器返回的时间戳格式不符合 HTTP 日期格式要求,或者在比较时使用了本地时间而不是 GMT/UTC 时间。

Python/Flask 示例(修复后)

from flask import Flask, request, Response

from datetime import datetime, timezone

app = Flask(__name__)

# HTTP 日期格式:RFC 7231 / RFC 1123

HTTP_DATE_FORMAT = '%a, %d %b %Y %H:%M:%S GMT'

@app.route('/data')

def get_data():

# 1. 假设这是资源的最后修改时间(使用 UTC/GMT 时区!)

last_modified_dt = datetime(2025, 1, 1, 10, 0, 0, tzinfo=timezone.utc)

last_modified_str = last_modified_dt.strftime(HTTP_DATE_FORMAT)

# 2. 设置 Last-Modified 响应头

response = Response('{"data": "some content"}', mimetype='application/json')

response.headers['Last-Modified'] = last_modified_str

# 3. 检查客户端的 If-Modified-Since

if_modified_since = request.headers.get('If-Modified-Since')

if if_modified_since:

try:

# 将客户端时间戳转换为 datetime 对象进行比较

client_dt = datetime.strptime(if_modified_since, HTTP_DATE_FORMAT).replace(tzinfo=timezone.utc)

# 如果客户端的时间 >= 服务器资源的时间,说明客户端的缓存是新的或相同的

if client_dt >= last_modified_dt:

# *** 关键步骤:返回 304 Not Modified ***

print('Time matched. Sending 304.')

return Response(status=304)

except ValueError:

# 忽略格式错误的 If-Modified-Since 头部,继续返回 200

print('Client sent invalid date format.')

# 4. 返回完整内容

print('Sending 200 OK.')

return response

# 运行 Flask 应用

# if __name__ == '__main__':

# app.run(debug=True)

RFC 9110 建议服务器应尽可能提供 ETag 验证器。当服务器同时提供了 ETag 和 Last-Modified 时,HTTP/1.1 客户端必须优先使用 ETag 进行条件请求(即发送 If-None-Match)。

ETag 的优势 精度更高(即使在 1 秒内修改多次、非时间戳相关的修改也能检测到)。

Last-Modified 的优势 实现简单,适用于内容不经常更新且不需要极高精度的场景。

验证器字段关注的是重新验证,而以下字段关注的是缓存策略本身

Cache-Control: max-age 服务器直接告知客户端多少秒内可以直接使用缓存,无需向服务器发送请求。这是最常用的替代/配合方案,它完全跳过了验证步骤,直到缓存过期。

示例响应头

Cache-Control: max-age=3600, public (该资源可以在 3600 秒内被客户端或代理缓存直接使用)

Expires 提供了过期时间点(一个具体的时间),功能类似 max-age,但优先级低于 Cache-Control。

替代方案的结合使用示例 (优先级)

客户端请求 检查本地缓存。

Cache-Control 检查 如果缓存未过期(在 max-age 内),直接使用缓存 (跳过验证器步骤)。

验证器检查 如果缓存已过期,客户端发送带有 If-None-Match (优先) 和/或 If-Modified-Since 的条件请求。

服务器响应 根据验证器字段判断返回 304 或 200。

希望这个解释和示例对您有所帮助!