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

Date 是「这条报文什么时候产生的」,而且必须是 GMT。 它和 Last-Modified 经常被混为一谈,其实完全是两件事。

📝 本节和前面几节的关系

上一节讲连接,这一节讲时间。时间在 HTTP 里有两个完全不同的含义,这一节先把它们分开。

一、只认一种格式

合法的 HTTP 日期只有一种写法:RFC 1123 格式,比如 Sun, 25 Oct 2026 06:00:00 GMT。 历史上还有两种老写法。一种是 RFC 850 风格,形如 Sunday, 25-Oct-26 06:00:00 GMT; 另一种是 asctime 风格,形如 Sun Oct 25 06:00:00 2026。 规范要求「接收时容忍、生成时必须用第一种」。 为什么必须 GMT?因为服务器在哪个时区是它自己的事,报文里写的时刻必须是全世界的共同语言。 写成 +08:00 就是错的,哪怕那个时刻本身没错。 这个规定还有个副作用:日期字符串里有逗号。这就是 6.2.2 说的「不能见到逗号就拆」的最典型例子。

二、Date 与 Last-Modified 的分工

对比项 Date Last-Modified
说的是 这条报文什么时候生成的 这个资源什么时候改的
每个响应都有吗 原则上是,除少数例外 不保证有
变化频率 每次响应都变 只有内容变了才变
用途 给缓存算年龄 给条件请求做凭证
分不清这两个,就会出现「我每次响应都换了 Date,
为什么缓存还是不更新」这种问题——缓存看的是 Last-Modified 和 ETag,
Date 变了只说明这次响应是刚生成的。
实测里两者同时出现:Date 是当下,Last-Modified 是昨天中午。这正说明内容比报文老。

三、Age:这份副本多老了

Date 只说了「什么时候生成的」,但如果这个响应是从缓存里拿的,它可能已经躺了很久。 Age 就是补这个信息:这份副本在缓存里存了多少秒。 真实年龄等于 Age 加上「从生成到现在」的那段,或者更准确地说, Age 已经包含了那段(缓存会自己算好)。所以判断能不能用时,用 Age 和 max-age 比就够了。 Demo 里 Age: 120 配 max-age=600,剩 480 秒可用。这两个数一比,就知道这份缓存还能撑多久。

四、动手 Demo:三种时钟摆在一起看

让服务端回一个带 Date、Age、Cache-Control 的响应,把响应里的 Date、本机 UTC、 本机本地时间并排打出来,再解析 Date 算出时间戳和剩余可用时间。

import email.utils

return reply(u'HTTP/1.1 200 OK',
             [(u'Date', email.utils.formatdate(usegmt=True)),
              (u'Age', u'120'),
              (u'Cache-Control', u'max-age=600')], b'ok')

gmt = email.utils.parsedate_to_datetime(d[u'date'])
print(u'解析回来的时刻', gmt.strftime(u'%Y-%m-%d %H:%M:%S %Z'))
print(u'时间戳', int(gmt.timestamp()))

实跑输出:

--------------------------------------------------------------------
Date 是绝对时刻,而且必须是 GMT
--------------------------------------------------------------------
  响应里的 Date              Sun, 25 Oct 2026 06:24:20 GMT
  本机 UTC 现在              Sun, 25 Oct 2026 06:24:20 GMT
  本机本地时间               2026-10-25 14:24:20
  时区偏移                   UTC+8

  解析回来的时刻             2026-10-25 06:24:20 UTC
  时间戳                     1792909460
  Age                        120 秒

  这份缓存的真实年龄         120 秒(Age 说的)
  剩余可用                   480 秒

  Date 是「这条报文是什么时候产生的」,不是「内容是什么时候改的」——
  后者是 Last-Modified,两回事。
  Age 是「这份副本已经在缓存里躺了多久」,它要加上 Date 才能算出真实时刻。
  HTTP 日期只有一种合法格式(RFC 1123):星期, 日 月 年 时:分:秒 GMT。
  用本地时间写就是错的——服务器在哪个时区,跟报文该写什么没关系。

三个时间:响应里的 Date 是 Sun, 25 Oct 2026 06:22:46 GMT,本机 UTC 现在也是同一时刻, 本机本地时间是 14:22:46。 时区偏移算出来是 UTC+8。 也就是说本机在东八区,但报文里的时间永远是 UTC——这两个数字差八小时,但指的是同一瞬间。 parsedate_to_datetime 把那个字符串解析回来,带上 UTC 时区,时间戳是一个整数。 这就是「同一个时刻的第三种表示」。 Age: 120 配 max-age=600,算出来剩 480 秒。缓存判断能不能用,看的正是这两个数的差。

30 秒实验

  1. 把服务端的 Date 改成用本地时间格式化,看客户端解析会不会出错。
  2. 把 Age 改成 700(超过 max-age=600),想想缓存该怎么处理。
  3. 想一想:如果两台机器的时钟差五分钟,用 Expires 和用 max-age 各会出什么问题?

常见误区

  1. 「Date 是内容最后修改的时间」——那是 Last-Modified。
  2. 「时间可以写成本地时区」——必须 GMT,否则就是非法值。
  3. 「Date 变了缓存就该更新」——缓存看的是 Last-Modified 和 ETag。
自测三问(点开对答案) 1. `Date` 和 `Last-Modified` 各说的是什么? —— `Date` 是这条报文生成的时刻;`Last-Modified` 是资源内容最后修改的时刻。 2. HTTP 日期必须用什么时区? —— GMT。服务器自己的时区与报文无关。 3. `Age` 表示什么? —— 这份缓存副本已经在缓存里存了多少秒。

时间讲完了。下一个字段是个只有一行有效内容的遗留产物。

相关笔记