📘 第六章 HTTP首部
📄 一句话概括

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 秒实验

  1. 把哈希改成只取 4 位,想想要多少份内容才会撞一次。
  2. 给响应加一行 ETag: W/"abc" 前缀,看 If-Range 还认不认。
  3. 想一想:集群里多台机器各自算 ETag,怎么保证同一份内容算出来一样?

常见误区

  1. 「ETag 就是文件哈希」——算法自定,可以是哈希、可以是 inode 加修改时间。
  2. 「弱 ETag 也能做断点续传」——If-Range 只认强 ETag。
  3. 「304 响应不用带 ETag」——不带的话客户端下次没法再用同一个指纹问。
自测三问(点开对答案) 1. ETag 比 Last-Modified 准在哪? —— 按内容算,不受文件时间被随手改掉的影响;粒度也不是秒。 2. `W/"abc"` 和 `"abc"` 有什么不同? —— 前一个是弱校验,只保证语义相等;后一个要求字节完全一致。 3. 304 响应里正文该有多少字节? —— 0 字节,只有首部,通常要带 ETag。

指纹解决了「变没变」,接下来是「变了就去哪儿」。Location 就是这个「哪儿」。

相关笔记