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

分块编码(chunked)让服务端不知道总长度也能开始发送:一块一块往外吐,每块前面写长度, 最后用 0 长度块收尾。

📝 本节和前面几节的关系

上一节的压缩解决「体积」,这一节解决「什么时候能开始发」。两者可以叠加,也可以各自单独用。

一、为什么需要它

没有分块编码时,服务端必须先把整个响应准备好,才能算出 Content-Length,才能开始发。 对一个边查数据库边生成的页面来说,这等于把首字节时间推迟到最后。 分块编码改成了流式:算出一块发一块,客户端收到一块就能先渲染一块。 首字节时间从「全部算完」提前到「第一块算完」。 另一种场景是长度真的不可知——比如网关把上游的流原样转发,它根本不知道上游会发多少。

二、格式:十六进制长度 + CRLF + 数据 + CRLF

每块长这样:<十六进制长度>CRLF<该块数据>CRLF,重复若干次,最后用 0CRLFCRLF 收尾。 Demo 里 2320 字节切成 8 块(7 块 300、1 块 220),分块本身的开销 60 字节,占 2.52%。 块越大,开销占比越低;块太小会让开销飙起来。 注意长度是十六进制,不是十进制。300 字节写成 12c。 这一点手写解析器时最容易写错——写成十进制会解出一堆乱码。

三、三条硬规矩

一、Transfer-Encoding: chunked 与 Content-Length 互斥。 两个同时出现是协议错误,不同实现的处理还不一致, 这正是「请求走私」(request smuggling)攻击的土壤。 二、分块编码是逐跳(hop-by-hop)的,不是端到端。 代理可以解开再重新分块,所以客户端收到的块边界跟服务端发的不一定一样。 三、分块只对 HTTP/1.1 有效。 HTTP/2 有自己的帧长度,根本不需要分段——所以在 HTTP/2 里 Transfer-Encoding 是被禁止的首部。

四、动手 Demo:手工造一个分块响应,再解回来

服务端绕过框架,直接拼一段分块响应发给客户端; 客户端按 chunked 规则一块块解,最后比对内容是否一致。

out = (u'HTTP/1.1 200 OK\r\n'
       u'Transfer-Encoding: chunked\r\n\r\n').encode()
for c in chunks:
    out += (u'%x\r\n' % len(c)).encode() + c + b'\r\n'
out += b'0\r\n\r\n'
# 客户端按规则解
while True:
    size = int(body[pos:e].split(b';')[0], 16)
    if size == 0:
        break

实跑输出:

--------------------------------------------------------------------
分块传输:先报长度再发,一块一块往外吐
--------------------------------------------------------------------
  响应状态行                 HTTP/1.1 200 OK
  Transfer-Encoding          chunked
  Content-Length             (没有)
  块数                       8
  各块大小                   300 300 300 300 300 300 300 220
  解出来的内容               2320 字节
  分块本身的开销             60 字节
  开销占比                   2.52%

  内容是否还原正确           是
  分块编码的价值是「不知道总长也能开始发」——服务端边算边发,
  首字节时间大幅提前。代价是每块多几个字节的十六进制长度行。
  块长用十六进制、后面跟 CRLF、最后一块长度是 0 再加一个空行。
  注意:用了 `Transfer-Encoding: chunked` 就**不能再带** `Content-Length`,两者互斥。

块数 8、各块大小 300 300 … 220、解出来的内容 2320 字节、 内容是否还原正确 是——整条链路真跑通了。 分块本身的开销 60 字节、开销占比 2.52%。 算法是:8 块,每块一个长度行(12c + CRLF 共 5 字节)加末尾 CRLF 2 字节,再加结尾的 0CRLFCRLF。 块越碎,这笔税越重。 注意代码里那个 %x 格式化符——它是十六进制。 若写成十进制,客户端按十六进制解就会读错长度,直接乱码。

30 秒实验

  1. 把块大小从 300 改成 50,重跑,看开销占比涨到多少。
  2. 同时加上 Content-Length 和 Transfer-Encoding, 想想服务端和代理会怎么处理(这就是请求走私的起点)。
  3. 把块长那个格式化符从十六进制换成十进制,重跑,看解出来的是什么。

常见误区

  1. 「分块能省流量」——它只增加开销,省的是等待时间。
  2. 「分块和压缩不能一起用」——完全可以,顺序是先压缩再分块。
  3. 「HTTP/2 也用分块」——不用,帧自带长度,而且 Transfer-Encoding 在 HTTP/2 里被禁止。
自测三问(点开对答案) 1. 分块编码解决什么问题? —— 服务端不知道总长度时也能开始发送,从而提前首字节时间。 2. 块长度用什么进制写? —— 十六进制,后面跟 CRLF。 3. 为什么它和 `Content-Length` 互斥? —— 两者都在声明「正文到哪结束」,同时出现就产生歧义,不同实现理解不同,会导致请求走私。

压缩和分块都在处理「一个内容」。下一节看「一次发好几样东西」。

相关笔记