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 秒实验
- 把服务端的
Date改成用本地时间格式化,看客户端解析会不会出错。 - 把
Age改成 700(超过max-age=600),想想缓存该怎么处理。 - 想一想:如果两台机器的时钟差五分钟,用
Expires和用max-age各会出什么问题?
常见误区
- 「
Date是内容最后修改的时间」——那是Last-Modified。 - 「时间可以写成本地时区」——必须 GMT,否则就是非法值。
- 「
Date变了缓存就该更新」——缓存看的是Last-Modified和ETag。
自测三问(点开对答案)
1. `Date` 和 `Last-Modified` 各说的是什么? —— `Date` 是这条报文生成的时刻;`Last-Modified` 是资源内容最后修改的时刻。 2. HTTP 日期必须用什么时区? —— GMT。服务器自己的时区与报文无关。 3. `Age` 表示什么? —— 这份缓存副本已经在缓存里存了多少秒。时间讲完了。下一个字段是个只有一行有效内容的遗留产物。
- ☐ (本节完,下一节 6.3.4 Pragma)