📘 第四章 返回结果的HTTP状态码
📄 一句话概括

304 不跳转,它说「你手里那份还有效,别传了」。它是缓存协商的结果,省的是带宽不是往返。

📝 本节和前面几节的关系

上一节 303 要客户端再走一步。这一节的 304 反过来:什么都不用走,就用本地那份。 第六章的 ETag、第七章的 HTTPS 都会再碰到它。

一、两句话完成协商

第一次响应带 ETag: "8e64...",客户端记下来; 第二次请求带 If-None-Match: "8e64...",等于问「这个版本还有效吗」。 服务端比对:一样就回 304(无正文),不一样就回 200 加新内容。 Demo 里三次请求分别是 200/304/200,正好演示了「命中、未命中」。 老一点的写法是 Last-Modified 加 If-Modified-Since,按时间比。 它精度只到秒,而且文件没改但修改时间变了会误判,所以现在优先用 ETag。

二、304 省的是什么、不省什么

省下的是整个正文:Demo 里 620 字节一个字节都不用传。 省不下的是这个往返:客户端仍然要发请求、服务端仍然要回响应。 所以 304 对「小资源」意义不大——省不到 1 毫秒,却多了一次往返的确定开销。 真正让它值钱的是 Cache-Control: max-age:在有效期内客户端根本不发请求,连往返都省了。 304 管的是「过期之后再来问一次」,max-age 管的是「别来问」。 两者配合才是完整缓存策略:新鲜期内不问,过期了带着 ETag 来问,没变就续期。

三、两条容易踩的规矩

一、304 不能带正文,也不该带 Content-Length(带的话必须是 0)。 它只该带缓存相关的首部(ETag、Cache-Control、Date、Vary)。 二、304 不是错误,它是成功的协商结果。 很多自己写客户端的人把 3XX 一律当「要跳转」处理,读到 304 发现没有 Location 就崩了。 还有一条:304 的响应里必须带当时那个 ETag,这样客户端才能续期。漏了它,下次又得重新比一遍。

四、动手 Demo:ETag 命中、不命中各来一次

服务端按内容算 MD5 当 ETag。 客户端第一次不带条件拿到内容,第二次带对 ETag 拿 304,第三次带错的 ETag 又拿回 200。

etag = u'"%s"' % hashlib.md5(content).hexdigest()
inm = hd.get(u'if-none-match')
if inm == etag:
    return (u'HTTP/1.1 304 Not Modified', [(u'ETag', etag)], b'')
return (u'HTTP/1.1 200 OK', [(u'ETag', etag)], content)

实跑输出:

--------------------------------------------------------------------
304 Not Modified:省下的是一个正文,不是往返
--------------------------------------------------------------------
  第一次(无 ETag)              HTTP/1.1 200 OK,正文 620 字节
  第二次(带 If-None-Match)     HTTP/1.1 304 Not Modified,正文 0 字节
  第三次(ETag 不匹配)          HTTP/1.1 200 OK,正文 620 字节

  资源正文                   620 字节
  ETag                       "8e64f102a7c17f6a394c6c39bd0fe164"
  304 省下的                 整个正文 620 字节
  304 省不下的               这个往返本身(仍然要问一次)
  304 的本质是「用一次便宜的问,换掉一次贵的传」。
  它省的是带宽,不是往返次数——所以对超小资源意义不大,对图片和视频很大。
  注意 304 响应**不能带正文**,连 Content-Length 都可以没有。
  If-Modified-Since 是它的老版本(按时间比),ETag 按内容比,更准。

三次的状态是 200(620 字节)、304(0 字节)、200(620 字节)——中间那次一字节正文都没传。 304 省下的 整个正文 620 字节、304 省不下的 这个往返本身——这两行把 304 的边界说清了。 ETag "8e64f102a7c17f6a394c6c39bd0fe164" 是内容的 MD5。 真实系统里常用「版本号 + 文件大小 + 修改时间」拼,比 MD5 便宜。 注意它带双引号——弱 ETag 前面还会多个 W/。

30 秒实验

  1. 把第三次的 If-None-Match 改成 *,看服务端该回什么(标准说资源存在就回 304)。
  2. 改用 If-Modified-Since 实现一遍,对比精度差异。
  3. 把 Cache-Control: max-age=60 改成 no-store,想想 304 还有没有意义。

常见误区

  1. 「304 能省一个往返」——省不了,请求照样发、响应照样回。
  2. 「304 是错误」——它是正常协商结果,属于 3XX 但不需要跳转。
  3. 「304 可以带正文」——不能,带了客户端会误解析。
自测三问(点开对答案) 1. 304 省下的到底是什么? —— 整个响应正文的传输;请求和响应这个往返仍然存在。 2. `Content-Length` 与 `max-age` 在缓存里的分工? —— `max-age` 决定「新鲜期内根本不用问」;过期后带 ETag 来问,没变就回 304。 3. 为什么 304 必须回带 ETag? —— 客户端要靠它续期,否则下次又得重新比对。

304 是不跳转。下一节看「跳转但保住方法」的那个码。

相关笔记