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

Content-Encoding 说明正文为了传输被压过几道、按什么顺序压的。 解码要倒着来,写反了客户端解出来就是一坨乱码。

📝 本节和前面几节的关系

上一节讲方法,这一节讲正文的「形态」。 它和 6.4.3 的 Accept-Encoding 是一对:一个问能不能压,一个说压了什么。

一、值和压缩顺序

常见值是 gzip、compress、deflate、identity。identity 表示没压过,通常干脆不写这个首部。 多个编码时按施加顺序从前往后列。 Content-Encoding: gzip, deflate 的意思是「先 gzip,再 deflate」, 客户端要先 deflate 解,再 gzip 解。 Demo 里实测:630 字节的原文,只 gzip 是 131 字节(20.8%), 只 deflate 是 119 字节(18.9%)。 再套一层变成 142 字节——反而比单层大,因为第二层压的是已经压过的数据,压不动还多了头。

二、它管的是「传输」,不管「文件是什么」

这句话最容易搞混。Content-Encoding: gzip 表示「传输时压过」,不代表这是个 gzip 文件。 如果服务器发的是现成的 .tar.gz,那属于文件格式,该写成 Content-Type: application/gzip, Content-Encoding 反而不该出现——否则客户端会再解一次,把文件解坏。 判断方法很简单:把 Content-Encoding 全部逆序解完,得到的应该是「原始正文」。 如果解完还是 gzip 文件,说明你标错了位置。

三、压缩之后 Content-Length 是谁的长度

是压缩后的长度。这一条和 6.6.4 直接相关:Content-Length 数的是网线上实际发出去的字节。 所以同一份内容,压缩前后 Content-Length 完全不同。 缓存按压缩后的版本存,给不同客户端发不同的版本——正是这件事逼出了 6.5.8 的 Vary。 还有一个坑:Content-Encoding 和 Content-Range 一起用的时候,范围指的是编码后字节流的位置, 不是原文的位置。压缩流不能随便切,所以实际实现里带压缩的资源往往干脆不支持范围请求。

四、动手 Demo:630 字节原文,三种交付方式,再套两层

同一段中文正文,分别用 identity、gzip、deflate 三种方式发,量字节数; 再用 gzip 压一次、deflate 再压一次,看双重编码的首部怎么列、怎么解。

raw = TEXT * 6
gz = gzip.compress(raw, 6)
df = zlib.compress(raw, 6)

def fn(raw_req, st):
    ae = hd.get(u'accept-encoding', u'')
    if u'gzip' in ae:
        return X.reply(u'HTTP/1.1 200 OK',
                       [(u'Content-Encoding', u'gzip')], gz)
    return X.reply(u'HTTP/1.1 200 OK', [], raw)

# 双重编码:先 gzip,再 deflate,首部按施加顺序写
two = zlib.compress(gzip.compress(raw, 6), 6)
back = gzip.decompress(zlib.decompress(two))   # 解码倒着来

实跑输出:

--------------------------------------------------------------------
Content-Encoding:正文被怎么压过
--------------------------------------------------------------------
  原文 630 字节,三种交付方式:
  Content-Encoding       字节     占原文
  identity(不声明)     630      100.0%
  gzip                   131      20.8%
  deflate                119      18.9%

  真发一次 gzip 的,客户端按 Content-Encoding 解压,再核对内容:
    响应 255 字节,首部里 Content-Encoding: gzip
    正文 131 字节,解压后 630 字节
    解压结果与原文一致:True

  多重编码时,Content-Encoding 按「施加顺序」从前往后列,解码要倒着来:
    先 gzip 再 deflate → 声明 gzip, deflate
    发出 142 字节;客户端先 deflate 解,再 gzip 解
    deflate 解完 131 字节,gzip 解完 630 字节,与原文一致:True

  注意两件事:Content-Encoding 是「为了传输而压」,跟文件本身是不是 .gz 无关;
  文件本身的压缩叫 Content-Type 的事。另外顺序写反了,客户端解出来就是一坨乱码。

三行占比:identity 100.0%(630 字节)、gzip 20.8%(131 字节)、 deflate 18.9%(119 字节)。中文正文里重复模式多,压得动。 发一次 gzip:整条响应 255 字节,其中正文 131 字节,解压回 630 字节,与原文逐字节一致。 双重编码:先 gzip 再 deflate,总长 142 字节——比单层 gzip 的 131 还大。 先 deflate 解回 131 字节,再 gzip 解回 630 字节。 142 大于 131 这件事值得记:压第二层通常没用。 真实世界里几乎见不到双重编码,就是因为它不划算还容易写错顺序。

30 秒实验

  1. 把 Accept-Encoding 改成 br,看服务端会不会退回 identity。
  2. 把双重编码的首部写成 deflate, gzip(顺序反了),看解出来是什么。
  3. 想一想:为什么压缩过的资源一般不适合做范围请求?

常见误区

  1. 「Content-Encoding: gzip 表示这是个 gz 文件」——那是 Content-Type 的事。
  2. 「多个编码按列出的顺序解」——按顺序施加,要倒着解。
  3. 「套两层一定更小」——实测 142 比单层 131 还大。
自测三问(点开对答案) 1. `Content-Encoding: gzip, deflate` 该怎么解? —— 先 deflate 解,再 gzip 解——解码顺序与施加顺序相反。 2. 压缩之后的 `Content-Length` 数的是什么? —— 压缩后、实际发到网线上的字节数。 3. 为什么双重编码很少见? —— 第二层压不动已经压过的数据,实测反而从 131 涨到 142 字节。

形态说完了,接下来是语言:同一份内容可能有中文版、英文版、日文版。

相关笔记