同一地址可以有多个版本(语言、编码、格式)。
内容协商让服务端按 Accept-* 首部挑一个,并用 Vary 告诉缓存。
上一节按字节范围取,这一节按「哪个版本」取。 两者都建立在 3.3.1 那个「实体有多份表示」的前提上。
一、首部是一张偏好清单
客户端用 Accept(媒体类型)、Accept-Language(语言)、Accept-Charset(字符集)、
Accept-Encoding(编码)四项报偏好。
每项都可以带 q 值:zh-CN,zh;q=0.9,en;q=0.8 表示最想要 zh-CN,其次 zh,再次 en。
q 从 0 到 1,q=0 表示「不要」。
服务端按自己的算法挑一个,回 Content-Language 等首部说明选了哪个。
Demo 里四种请求,服务端分别选了 zh/en/en+gzip/zh。
二、三种协商方式,实际只用一种
| 方式 | 谁来决定 | 现状 |
|---|---|---|
| 服务器驱动 | 服务端按 Accept 挑 | 绝对主流 |
| 客户端驱动 | 服务端给一堆链接 | 少,多用于下载页 |
| 透明协商 | 中间缓存替服务端挑 | 几乎没人用 |
透明协商听着聪明,实际上不可行:缓存要维护每个版本的副本,还要理解每个 Accept 首部的语义, |
||
| 代价远大于收益。 | ||
| 客户端驱动则是把一个 URL 变成「选哪个版本」的菜单, | ||
| 常见于软件下载页(选 Windows/macOS/Linux)和语言选择页。它的好处是没有猜错的风险。 |
三、Vary:最容易被忽略、后果最严重
Vary: Accept-Language, Accept-Encoding 告诉缓存:这个响应随这两个首部变化,必须分开存。
没有它,CDN 或反向代理会把第一个请求的版本缓存下来发给所有人——中文用户看到英文页面,或者更糟的,
把 gzip 版本发给不支持压缩的客户端,直接乱码。
这类故障特别难查,因为服务端日志里一切正常。唯一的办法是查缓存配置。
凡是按请求首部变化的内容,都必须写 Vary。
四、动手 Demo:四种请求,四种响应版本
服务端准备中英双语、压缩与不压缩四个版本,按 Accept-Language 与 Accept-Encoding 组合挑一个。
lang = u'zh' if u'zh' in hd.get(u'accept-language', u'zh') else u'en'
enc = u'gzip' if u'gzip' in hd.get(u'accept-encoding', u'') else u'identity'
text = versions[(lang, enc)]
hdrs = [(u'Content-Language', lang),
(u'Vary', u'Accept-Language, Accept-Encoding')]
实跑输出:
--------------------------------------------------------------------
内容协商:同一地址,不同人拿不同东西
--------------------------------------------------------------------
请求带的 Accept-Language=zh-CN,zh;q=0.9
服务端选的语言 zh
正文大小 49 字节
请求带的 Accept-Language=en-US,en;q=0.9
服务端选的语言 en
正文大小 49 字节
请求带的 Accept-Language=en-US,en;q=0.9 / Accept-Encoding=gzip
服务端选的语言 en
正文大小 59 字节
请求带的 (什么都没发)
服务端选的语言 zh
正文大小 49 字节
服务端按 `Accept-*` 首部挑一个版本,这叫**服务器驱动协商**。
另外两种:客户端驱动(服务端给一堆链接让人点)、
透明协商(中间缓存替服务端选)——后者几乎没人用。
`Vary` 是给缓存看的:它说明「这个响应随哪些请求首部变化」,
缓存必须按这些首部的组合分别存,否则会把英文版发给中文用户。
书里还提到 `q=0.9` 这种**权重**:数字越大越想要,0 表示不要。
四种组合里语言分别选了 zh、en、en、zh——最后一行「什么都没发」走了默认值 zh。
最有价值的是第三行:英文版 49 字节,加了 gzip 之后变成 59 字节。
压缩让小响应变大了 10 字节。这是「别对小响应开压缩」的实证。
Vary 那行在四个响应里都有。它是给缓存看的一句话,对客户端没有任何作用,但少了它缓存就会做错事。
30 秒实验
- 把
Accept-Language改成zh;q=0.1,en;q=0.9,改一下服务端的选择逻辑,让它真的按 q 值排序。 - 把
Vary删掉,用一个中间缓存(或用http.server手写一个)模拟「把第一个响应发给所有人」。 - 算一算:多少字节以下的响应,开 gzip 是亏的(提示:gzip 头尾约 18 字节)。
常见误区
- 「
Accept-Language里第一个就是最好的」——要看 q 值,没有 q 默认算 1。 - 「Vary 是给客户端看的」——它只对缓存有意义。
- 「内容协商能代替语言切换按钮」——服务端猜错时用户没法纠正,所以正规站点还是要有手动切换。
自测三问(点开对答案)
1. q 值是什么意思? —— 偏好权重,0 到 1,越大越想要,0 表示不接受。不写默认是 1。 2. `Vary` 缺了会出什么故障? —— 缓存把某一个版本发给所有客户端,导致语言错乱或编码错乱。 3. 为什么透明协商没人用? —— 缓存要维护每个版本副本并理解 Accept 语义,代价远大于收益。第三章到此结束。第四章看那一串三位数字:状态码。
- ☐ (本章完,下一节 4.1 状态码告知从服务器端返回的请求结果)