ETag 是服务端给资源算的指纹:内容一样指纹就一样,改一个字节指纹就变。
它比 Last-Modified 准,因为时间只精确到秒,而且会被部署动作搅乱。
上一节按时间判新旧,这一节按内容判。
它是协商缓存的主力,也是 If-Match、If-None-Match、If-Range 三个字段的落点。
一、指纹怎么来
规范没说必须怎么算,只要求一个性质:内容相同则指纹相同,内容不同则指纹不同。
实现上最常见的是对正文做哈希。
Demo 里用 MD5 取前 16 位。同一份内容隔 1.2 秒取两次,指纹都是 "e9e0cd2e9b2352d5";
把正文改一个字,指纹立刻变成 "22365ee55bc64f16"。
这不是巧合,正是它比时间强的地方:文件的修改时间会被 cp、git checkout、部署脚本随手改掉,
内容却一点没动。按时间判会误判成「变了」,按指纹判不会。
二、强指纹与弱指纹
"abc" 是强 ETag,要求字节级完全一致;
W/"abc" 是弱 ETag,只要求语义相等(比如只差空白、只差换行)。
差别在用途上:If-Match 和 If-Range 都要求强 ETag,
因为「把两段拼起来」这种操作容不得半字节偏差;If-None-Match 两者都收,因为它只是问「变没变」。
很多框架默认就生成弱 ETag——它们本来就没算正文哈希,只是拿修改时间加长度拼一个。
看到 W/ 开头不要惊讶,但也别指望它能用于断点续传。
三、304 到底省了多少
Demo 里带当前 ETag 去问,请求 73 字节、响应 93 字节、正文 0 字节,状态码 304。 换成已经过期的旧 ETag,同样 73 字节的请求,响应 149 字节、正文 24 字节,状态码 200。 差别就在那 24 字节正文和 56 字节首部上。 单看一次不多,但静态资源每天被拉几亿次,这就是流量账单上的一大块。 这也是为什么「304 响应也必须带 ETag」——否则客户端下次就没法再用同一个指纹问了。
四、动手 Demo:同一份内容、改一个字、新旧指纹各问一次
服务端用 MD5 前 16 位当 ETag,客户端先裸取两次看指纹稳不稳,再让服务端改一个字看指纹变不变, 最后分别拿新指纹和旧指纹去协商。
def fn(raw, st):
v = st.get(u'v', u'A')
body = doc[v]
tag = u'"%s"' % hashlib.md5(body).hexdigest()[:16]
inm = hd.get(u'if-none-match', u'')
if inm and inm == tag:
return X.reply(u'HTTP/1.1 304 Not Modified', [(u'ETag', tag)], b'')
return X.reply(u'HTTP/1.1 200 OK', [(u'ETag', tag)], body)
# 客户端侧:拿指纹去问,只关心状态码和正文长度
q1 = X.req(u'GET', u'/doc', u'127.0.0.1', extra=[(u'If-None-Match', cur)])
d = X.ask(p, q1)
实跑输出:
--------------------------------------------------------------------
ETag:内容指纹,比时间更准
--------------------------------------------------------------------
同一份内容,先取两次(中间隔一秒多),看 ETag 会不会变:
第一次 ETag: "e9e0cd2e9b2352d5"
第二次 ETag: "e9e0cd2e9b2352d5"
两次相同:True(时间过了 1.2 秒,内容没动)
现在服务器把正文改动一个字符,ETag 立刻变:
改动后 ETag: "22365ee55bc64f16"
与第一次相同:False
拿新 ETag 去问(If-None-Match),服务器只回一行状态:
请求 73 字节,响应 93 字节
HTTP/1.1 304 Not Modified
正文 0 字节(什么都没发)
再拿上面那个已经过期的旧 ETag 去问:
请求 73 字节,响应 149 字节
HTTP/1.1 200 OK
正文 24 字节(指纹不对,得整份重发)
对照上一节 If-Modified-Since 的做法:那个比的是「秒级时间」,
只要文件在那一秒之内动过、或者部署时改了 mtime,就会误判。
ETag 比的是内容本身算出来的哈希,一个字符都不放过。
ETag 有两种写法:`"abc"` 是强校验,字节完全一致才算命中;
`W/"abc"` 是弱校验,语义相等就算命中(比如只差空白)。
弱 ETag 不能拿来配 `If-Range` 做断点续传——这是下一节要看的。
前两次取的是同一份内容,隔了 1.2 秒,指纹一模一样——说明它认的是内容,不是时间。 改一个字之后指纹换成了另一个值,这就是「一个字符都不放过」。 带新指纹去问:请求 73 字节、响应 93 字节、正文 0 字节,304。 带旧指纹去问:响应 149 字节、正文 24 字节,200 加整份。两组请求一样大,差别全在响应该发多少。
30 秒实验
- 把哈希改成只取 4 位,想想要多少份内容才会撞一次。
- 给响应加一行
ETag: W/"abc"前缀,看If-Range还认不认。 - 想一想:集群里多台机器各自算 ETag,怎么保证同一份内容算出来一样?
常见误区
- 「ETag 就是文件哈希」——算法自定,可以是哈希、可以是 inode 加修改时间。
- 「弱 ETag 也能做断点续传」——
If-Range只认强 ETag。 - 「304 响应不用带 ETag」——不带的话客户端下次没法再用同一个指纹问。
自测三问(点开对答案)
1. ETag 比 Last-Modified 准在哪? —— 按内容算,不受文件时间被随手改掉的影响;粒度也不是秒。 2. `W/"abc"` 和 `"abc"` 有什么不同? —— 前一个是弱校验,只保证语义相等;后一个要求字节完全一致。 3. 304 响应里正文该有多少字节? —— 0 字节,只有首部,通常要带 ETag。指纹解决了「变没变」,接下来是「变了就去哪儿」。Location 就是这个「哪儿」。
- ☐ (本节完,下一节 6.5.4 Location)