📘 第三章 HTTP报文内的HTTP信息
📄 一句话概括

同一地址可以有多个版本(语言、编码、格式)。 内容协商让服务端按 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 秒实验

  1. 把 Accept-Language 改成 zh;q=0.1,en;q=0.9,改一下服务端的选择逻辑,让它真的按 q 值排序。
  2. 把 Vary 删掉,用一个中间缓存(或用 http.server 手写一个)模拟「把第一个响应发给所有人」。
  3. 算一算:多少字节以下的响应,开 gzip 是亏的(提示:gzip 头尾约 18 字节)。

常见误区

  1. 「Accept-Language 里第一个就是最好的」——要看 q 值,没有 q 默认算 1。
  2. 「Vary 是给客户端看的」——它只对缓存有意义。
  3. 「内容协商能代替语言切换按钮」——服务端猜错时用户没法纠正,所以正规站点还是要有手动切换。
自测三问(点开对答案) 1. q 值是什么意思? —— 偏好权重,0 到 1,越大越想要,0 表示不接受。不写默认是 1。 2. `Vary` 缺了会出什么故障? —— 缓存把某一个版本发给所有客户端,导致语言错乱或编码错乱。 3. 为什么透明协商没人用? —— 缓存要维护每个版本副本并理解 Accept 语义,代价远大于收益。

第三章到此结束。第四章看那一串三位数字:状态码。

相关笔记