HTTP 响应 Chunked 格式分析

背景与原理

当服务器无法事先确定 HTTP 响应内容的具体大小(例如动态生成的内容)时,无法在响应头中预先写入 Content-Length。此时,服务器通常采用 Chunked 编码(分块传输编码)来实时生成消息长度。

在进行 Chunked 编码传输时,响应消息头中会包含 Transfer-Encoding 字段并定义为 chunked,表示内容将通过 Chunked 编码进行传输。

数据结构与格式

Chunked 编码由若干个 Chunk(数据块)串联而成,并由一个长度为 0 的 Chunk 标示结束。每个 Chunk 分为头部和正文两部分:

  1. 头部:指定下一段正文的字符总数(十六进制数字)和数量单位(通常省略)。
  2. 正文:指定长度的实际内容。
  3. 分隔:头部与正文之间用回车换行(CRLF,\r\n)隔开。
  4. 结束:最后一个长度为 0 的 Chunk 之后可能包含称为 Footer(尾部)的内容,这是一些附加的 Header 信息(通常可以直接忽略)。

模拟数据结构如下:

[Chunk 大小][CRLF][Chunk 数据体][CRLF][Chunk 大小][CRLF][Chunk 数据体][CRLF][0][CRLF][Footer 内容(可选)][CRLF]

注意

  • chunk-size 是以十六进制的 ASCII 码表示的。例如 25(十六进制)对应的十进制长度为 37,表示随后的数据体长度为 37 字节。
  • 在跟踪 www.yahoo.com 的返回数据时曾发现,chunk-size 字段中有时会包含多余空格。这可能是为了固定长度(如 7 字节),不满则以空格(ASCII 0x20)补足。但这属于特定服务器的实现细节,并非标准强制要求。

解码流程

对 Chunked 编码进行解码的目的是将分块的 chunk-data 整合恢复成完整的报文体,同时计算此块体的总长度。

参考 RFC2616 附带的解码流程(伪代码):

length := 0                          // 长度计数器置 0
read chunk-size, chunk-extension     // 读取 chunk-size, chunk-extension
read CRLF                            // 和 CRLF

while (chunk-size > 0) {             // 表明不是 last-chunk
    read chunk-data and CRLF         // 读 chunk-size 大小的 chunk-data, skip CRLF
    append chunk-data to entity-body // 将此块 chunk-data 追加到 entity-body 后
    read chunk-size and CRLF         // 读取新 chunk 的 chunk-size 和 CRLF
}

read entity-header                   // entity-header 格式为 name:value CRLF,如果为空即只有 CRLF
while (entity-header not empty) {    // 即,不是只有 CRLF 的空行
    append entity-header to existing header fields
    read entity-header
}

Content-Length := length             // 将解码流程结束后计算得到的新报文体 length
                                     // 作为 Content-Length 域的值写入报文中
Remove "chunked" from Transfer-Encoding // 同时从 Transfer-Encoding 域值中去除 chunked 标记

示例分析

1. 编码后的响应 (Encoded Response)

HTTP/1.1 200 OK
Content-Type: text/plain
Transfer-Encoding: chunked

25
This is the data in the first chunk

1A
and this is the second one
0

2. 原始字节十六进制视图 (Raw Bytes in Hex)

0000-000F   48 54 54 50 2f 31 2e 31 20 32 30 30 20 4f 4b 0d   HTTP/1.1 200 OK.
0010-001F   0a 43 6f 6e 74 65 6e 74 2d 54 79 70 65 3a 20 74   .Content-Type: t
0020-002F   65 78 74 2f 70 6c 61 69 6e 0d 0a 54 72 61 6e 73   ext/plain..Trans
0030-003F   66 65 72 2d 45 6e 63 6f 64 69 6e 67 3a 20 63 68   fer-Encoding: ch
0040-004F   75 6e 6b 65 64 0d 0a 0d 0a 32 35 0d 0a 54 68 69   unked....25..Thi
0050-005F   73 20 69 73 20 74 68 65 20 64 61 74 61 20 69 6e   s is the data in
0060-006F   20 74 68 65 20 66 69 72 73 74 20 63 68 75 6e 6b    the first chunk
0070-007F   0d 0a 0d 0a 31 41 0d 0a 61 6e 64 20 74 68 69 73   ....1A..and this
0080-008F   20 69 73 20 74 68 65 20 73 65 63 6f 6e 64 20 6f    is the second o
0090-009F   6e 65 0d 0a 30 0d 0a 0d 0a                        ne..0....

3. Java 代码模拟 (Java Code Simulation)

public static final byte[] CHUNKED_RESPONSE;
static {         
    StringBuilder sb = new StringBuilder();
    sb.append("HTTP/1.1 200 OK\r\n");
    sb.append("Content-Type: text/plain\r\n");
    sb.append("Transfer-Encoding: chunked\r\n\r\n");
    sb.append("25\r\n");        
    sb.append("This is the data in the first chunk\r\n"); // 37 bytes of payload
            // (conveniently consisting of ASCII characters only)
    sb.append("\r\n1A\r\n");
    sb.append("and this is the second one"); // 26 bytes of payload
            // (conveniently consisting of ASCII characters only)
    sb.append("\r\n0\r\n\r\n");
    CHUNKED_RESPONSE = sb.toString().getBytes(java.nio.charset.Charset.forName("US-ASCII"));
}

4. 解码后的数据 (Decoded Data)

This is the data in the first chunk
and this is the second one

说明

  1. 本文引用的解码流程基于 RFC2616,该协议已被 RFC7230 及后续的 RFC9112 取代,但 Chunked 编码的核心机制保持一致。
  2. 文中关于 Yahoo 服务器固定长度补空格的观察属于特定历史案例,实际开发中应以 RFC 标准为准,不应依赖此类非标准行为。
  3. 以上就是 HTTP 响应中 Chunked 编码方式的基本分析。