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

If-Modified-Since 是「如果这个时间之后没改过,就别传正文了」。 它是协商缓存的老办法,凭证是 Last-Modified,精度只到秒。

📝 本节和前面几节的关系

上一节管写,这一节管读。两者语义正相反,一起记住不容易混。

一、它怎么省流量

客户端第一次拿到资源,记住响应里的 Last-Modified。 下次请求同一个资源时,把这个时间放进 If-Modified-Since。 服务端一比对:内容在这个时间之后没变过,就回 304 Not Modified,正文 0 字节。 客户端继续用本地那份。 Demo 里全量响应 182 字节,304 响应 113 字节,每次省 37.9%。正文越大,这个比例越可观; 正文只有 38 字节时,省的还不到一半。

二、三种非命中情况

带的是更早的时间:说明客户端手上的副本可能已经旧了,回 200 全量。 Demo 里带前一天的时间,果然 200。 格式不合法:按「当作没发」处理,也回 200 全量。 这种宽松处理是必需的——一个写错的日期不该让所有缓存失效。 资源已经被删:回 404,不是 304。这点容易被忽略:304 只能表示「还在,且没变」。

三、秒级精度带来的真实故障

Last-Modified 只能精确到秒。 如果文件在同一秒内被改了两次,时间戳不变,服务端就会认为「没改过」,回 304,客户端一直拿那份旧的。 这个 bug 在构建和发布流程里很常见:脚本快速生成两个版本的文件,第二个版本永远发不出去。 排查时抓包一看,全是 304,会以为服务端没更新——其实是时间戳没变。 解决办法只有两个:换 ETag(If-None-Match 配内容哈希),或者发布时人为把时间戳往后拨一秒。 前者是对的,后者是应急。

四、动手 Demo:四种条件,看 304 什么时候出现

服务端资源最后修改时间写死,客户端分别不带条件、带最新时间、带旧时间、带一个垃圾值去请求, 看各自的响应大小。

ims = hd.get(u'if-modified-since', u'')
if ims == LM:
    return reply(u'HTTP/1.1 304 Not Modified',
                 [(u'Last-Modified', LM)], b'')
return reply(u'HTTP/1.1 200 OK',
             [(u'Content-Type', u'text/html; charset=utf-8'),
              (u'Last-Modified', LM)], DOC)

实跑输出:

--------------------------------------------------------------------
If-Modified-Since:内容没变就别再传一遍
--------------------------------------------------------------------
  【不带条件】
    If-Modified-Since        (没发)
    结果                     HTTP/1.1 200 OK | 正文 38 字节 | 响应共 182 字节

  【带的是最新时间】
    If-Modified-Since        Sat, 24 Oct 2026 12:00:00 GMT
    结果                     HTTP/1.1 304 Not Modified | 正文 0 字节 | 响应共 113 字节

  【带的是旧时间】
    If-Modified-Since        Fri, 23 Oct 2026 12:00:00 GMT
    结果                     HTTP/1.1 200 OK | 正文 38 字节 | 响应共 182 字节

  【带一个格式不合法的值】
    If-Modified-Since        yesterday
    结果                     HTTP/1.1 200 OK | 正文 38 字节 | 响应共 182 字节

  服务端资源最后修改         Sat, 24 Oct 2026 12:00:00 GMT
  全量响应                   182 字节(正文 38)
  304 响应                   113 字节(正文 0)
  每次省下                   37.9%

  这个字段的语义是「如果资源在这个时刻**之后**没改过,就回 304」。
  所以带一个比 Last-Modified 更早的时间,一定是 200 全量。
  格式不合法的值按「当作没发」处理——宽松处理是这里的基本要求,
  否则一个写错的日期就会让所有缓存失效。
  精度只到秒,这就是 5.3.2 说的那个坑:同一秒改两次,条件请求看不出变化。

不带条件:200,正文 38 字节,响应共 182 字节。 带最新时间:304,正文 0 字节,响应共 113 字节。省 37.9%。 带旧时间(前一天):200,正文 38 字节。因为资源在那之后改过。 带 yesterday 这个垃圾值:照样 200 全量。服务端把它当没发。 注意 304 也要带 Last-Modified——客户端下次还要用这个凭证。

30 秒实验

  1. 把资源时间改成未来,再带当前时间去请求,看会不会 304。
  2. 把 Last-Modified 换成同一秒内的两个不同内容,复现那个精度坑。
  3. 想一想:304 响应能不能带正文?带了会怎样?

常见误区

  1. 「If-Modified-Since 是精确到毫秒的」——只到秒。
  2. 「条件不满足要回错误」——回 200 全量就行,不是错误。
  3. 「304 表示资源不存在」——表示「还在,但没变」。
自测三问(点开对答案) 1. `If-Modified-Since` 的作用是什么? —— 告诉服务端「我这里有一份某时刻的副本,如果之后没改过就别传正文了」。 2. 带一个比 `Last-Modified` 更早的时间会怎样? —— 回 200 全量,因为服务端无法确认客户端那份是不是最新的。 3. 秒级精度会造成什么故障? —— 同一秒内改两次,时间戳不变,服务端回 304,客户端一直拿旧文件。

时间凭证讲完了。下一个用的是更精确的凭证:ETag。

相关笔记