bigywater.tech
网络协议 · 76

分块传输编码与 Content-Length:消息体怎么划界

HTTP 的消息体到哪里结束?答案只有三种:Content-Length 写明长度、Transfer-Encoding: chunked 分块、或者干脆等连接关闭。这篇逐个看它们的字节、开销和用处,再故意写错长度,看错位和截断是怎么发生的——这也是请求走私类漏洞的根源。

12 字节
example.com 的 589 字节分块响应里框架开销
6.00% → 0.06%
同样 100000 字节内容,chunked 每块 100 字节与每块 16 KiB 的框架开销
1001 ms vs 0 ms
Content-Length 方式第一个字节到达时间与 chunked 方式的对比

chunked 的格式

实测 · 本机执行输出
  响应头里:Transfer-Encoding = chunked
;Content-Length = None
  消息体共 589 字节:1 个数据块,块大小 [577];实际内容 577 字节,框架开销 12 字节(块长度行 + 每块后的 CRLF + 结尾的 "0\r\n\r\n")
  内容开头 b'<!doctype html><html lang=en><head><meta'…结尾 b'ript></body></html>\n';最后一个块之后 b'\r\n0\r\n\r\n' 是结束标记(长度 0 的块 + 空的 trailer + 空行)

每块:十六进制长度行 CRLF + 数据 + CRLF;长度 0 的块结束,可带 trailer。

开销

实测 · 本机执行输出
  Content-Length        :消息体 100000 字节,框架开销 0 字节(0.00%)
  chunked,每块 100 字节     :消息体 106005 字节,框架开销 6005 字节(6.00%)
  chunked,每块 1 KiB      :消息体 100691 字节,框架开销 691 字节(0.69%)
  chunked,每块 16 KiB     :消息体 100060 字节,框架开销 60 字节(0.06%)

块越小开销越高:每块 100 字节 6.00%,16 KiB 0.06%。

流式

实测 · 本机执行输出
  chunked:客户端收到的数据(到达时刻 ms, 字节):(0, 66), (205, 26), (409, 26), (610, 26), (813, 26), (1016, 31)
  Content-Length(服务器要先生成完整内容,算出长度再发):第一个字节在 1001 ms 才到,共 159 字节,全部在同一时刻到达

chunked 允许边生成边发,不必先知道总长度。

close 定界

实测 · 本机执行输出
  100 字节在 0 ms 就到齐了,但客户端无从知道这是全部——一直等到服务器在 2001 ms 关闭连接才能确定。这样的响应不可能复用连接

没有长度的响应要等连接关闭才结束,无法复用连接,也无法发现被截断。

长度不一致

实测 · 本机执行输出
  Content-Length: 1000,实际只发 600 字节就关闭连接 → IncompleteRead:已读 600 字节,还缺 400
  chunked,发了一个块之后没有发结尾的 0 块就关闭连接 → IncompleteRead:已读 256 字节,还缺 None
  没有长度,发 600 字节就关闭连接(close 定界) → 正常读到 600 字节(状态 200)(读起来一切正常,但客户端无法判断是否被截断)
  Content-Length: 500,实际发了 1000 字节 → 正常读到 500 字节(状态 200)(多出的 500 字节被客户端忽略 / 留在连接里)
  响应错位演示:服务器对第 1 个请求声明 Content-Length: 5 却多发了 6 个字节 "WORLD-EXTRA"[:6]…客户端按长度读出第 1 个响应的 b'hello';紧接着的字节是 b'WORLD-EXTRAHTTP/1.1 200 OK\r\nContent-Leng',被当作“第 2 个响应”的开头——不是合法的状态行,连接上的响应从此错位(请求走私 / 响应拆分漏洞的根源)

写小了,多余字节污染后面的响应;截断在有长度的响应里能被发现。

分步演示

分块传输与 Content-Length
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64(Apple 芯片)/ Python 3.14.8 / tcpdump(libpcap)/ scapy 2.8 / dpkt / dnspython / h2 / aioquic / OpenSSL / curl 8.7.1;抓包在 lo0(环回)上做,少数实验用真实网卡 en0(这台机器上 en0 是 Wi-Fi,tcpdump 看到的是系统转成以太网格式的帧,所以文中的“以太网头”都指这个 14 字节的头)或访问公网(会在文章里注明);需要丢包 / 延迟 / 限速时,用一个只作用于 lo0 端口 9400-9499 的 dummynet 辅助脚本(netlab);所有服务端都是本机自己起的进程;时间戳取自 tcpdump 的 -tt,数字每次运行会有小幅抖动。第一节是 example.com:80 的真实响应;第二至五节用本机脚本化的服务器(逐段按延迟发送预设字节)和 Python 的 http.client / 原始套接字当客户端;各框架开销一部分是算出来的(第二节的 100 字节 / 1 KiB / 16 KiB 是实测消息体长度减 100000)
    • 没有验证:chunked 的 chunk extension 与 trailer 的使用(只看到空 trailer);Content-Length 与 Transfer-Encoding 同时出现时各家服务器的取舍(请求走私的实际利用);HTTP/2 里没有分块,用帧划界(见后面的 HTTP/2 篇);压缩与分块组合;浏览器对错误长度的容忍度
    京ICP备2026067502号-1京公网安备11010502063487号