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

内容编码是端到端的压缩:服务端压、客户端解,中间设备不碰。 靠 Content-Encoding 与 Accept-Encoding 两句话协商。

📝 本节和前面几节的关系

上一节说清了「编码前」和「编码后」两个词的差别。 这一节讲最常见的那种编码:gzip,以及它怎么协商、什么时候不该用。

一、两句话完成协商

客户端说能力:Accept-Encoding: gzip, deflate, br(按优先级,可带 q 值)。 服务端说结果:Content-Encoding: gzip。 两边都必须说。客户端没说,服务端就只该发 identity(不编码); 服务端没标,客户端就只能当明文处理,一解就乱。 少了服务端那一句是最常见的故障:浏览器显示一堆乱码, 抓包一看 Content-Encoding: gzip 但客户端没解——通常是中间代理把这一行删了。

二、常见算法与它们的现实地位

算法 Content-Encoding 值 现状
gzip gzip 事实标准,全支持
deflate deflate 历史包袱,实现不一致
Brotli br 2015 年起,省一成到两成
Zstd zstd 2016 年起,压缩快解压更快
不压缩 identity 默认值
书里只有 gzip 和 deflate,后两个是这本书出版之后才普及的(Brotli 2015、Zstd 2016)。
2013 年写书时 br 还不存在,2026 年它已经是主流选择之一。
deflate 的坑是历史遗留:标准说它是 zlib 格式,但早期 IIS 发的是裸 deflate,
导致浏览器不得不两种都试。这件事到今天还在某些老系统上冒头。

三、什么时候不该压

已经压过的格式(JPEG、PNG、MP4、woff2、zip)再 gzip 几乎不缩,白烧 CPU, 有时还变大——因为 gzip 要加十几字节的头和尾。 Demo 里就有这个现象:49 字节的正文加了 gzip 之后变成 59 字节,反而大了 10 字节。 小响应压缩是负收益,这是常被忽略的一条。 另一条是安全:压缩率会泄露内容(BREACH/CRIME),所以带 CSRF token 之类的响应常常关闭压缩, 或者把秘密和用户输入分开压。

四、动手 Demo:三种 Accept-Encoding,三种响应

服务端看 Accept-Encoding 决定压不压。 客户端分别发空、gzip、以及 gzip+deflate+br,看回的内容编码与字节数。

def resp(raw_req, st):
    first, hd, body = H.parse_headers(raw_req)
    accept = hd.get(u'accept-encoding', u'')
    if u'gzip' in accept:
        return (u'HTTP/1.1 200 OK',
                [(u'Content-Type', u'text/html; charset=utf-8'),
                 (u'Content-Encoding', u'gzip'),
                 (u'Vary', u'Accept-Encoding')], gz)
    return (u'HTTP/1.1 200 OK', [(u'Content-Type', u'text/html')], raw)

实跑输出:

--------------------------------------------------------------------
内容编码:服务端压、客户端解
--------------------------------------------------------------------
  客户端说 Accept-Encoding       (没发)
      服务端回的内容编码         identity
      传输字节                   2320
  客户端说 Accept-Encoding       gzip
      服务端回的内容编码         gzip
      传输字节                   104
  客户端说 Accept-Encoding       gzip, deflate, br
      服务端回的内容编码         gzip
      传输字节                   104

  原始内容                   2320 字节
  压缩后                     104 字节(4%)
  省下                       2216 字节

  协商是**客户端先说、服务端再选**:客户端用 `Accept-Encoding` 报能力,
  服务端用 `Content-Encoding` 说实际用了什么。**两边都必须说**,
  少一边就没法解。
  `Vary: Accept-Encoding` 是给缓存看的:同一地址不同编码要分开存,
  否则缓存会把 gzip 版本发给不支持 gzip 的客户端。
  `br`(Brotli)是 2015 年才有的算法,书里没有;它比 gzip 再省一成到两成。

三行分别是:没说 → identity 2320 字节;说 gzip → gzip 104 字节; 说三个 → 还是 gzip 104 字节。服务端只挑了一个,不会叠加。 Vary: Accept-Encoding 是给缓存看的。 没有它,缓存可能把 gzip 版本发给不支持 gzip 的客户端,那个客户端就会看到一堆乱码。 注意这里 2320 字节压到 104 字节,是极端重复样本。 真实页面通常压到两三成,别拿这个比例去估线上带宽。

30 秒实验

  1. 把 BIG 换成一段不重复的中文,重跑,看压缩比降到多少。
  2. 在响应里加 Content-Encoding: gzip 但正文不压,看客户端解压会报什么错。
  3. 算一算:一个 40 字节的响应加 gzip 要多少字节?还值得压吗?

常见误区

  1. 「压缩总是省流量」——小响应压缩后可能更大,因为要加 gzip 头尾。
  2. 「服务端可以自己决定压不压」——不看 Accept-Encoding 就压,会给老客户端发它解不开的字节。
  3. 「deflate 就是原始 deflate」——标准里它是 zlib 包装格式,这里历史上出过血案。
自测三问(点开对答案) 1. 内容编码靠哪两个首部协商? —— 客户端 `Accept-Encoding` 报能力,服务端 `Content-Encoding` 说结果。 2. 为什么 `Vary: Accept-Encoding` 必须加? —— 否则缓存会把同一个地址的 gzip 版本发给不支持压缩的客户端。 3. 小响应压缩可能变大,原因是什么? —— gzip 有固定头尾开销(十几字节),内容太小时压缩收益抵不过开销。

压缩是一次性给全量。下一节看另一种编码:边算边发,不知道总长也能开始。

相关笔记