有些首部要等正文发完才算得出来——校验和、总长度、耗时。Trailer 让它们能放在正文后面发。
上一节是个死掉的字段,这一节是个活得好好的字段:它在流式传输里不可替代。
一、要解决的是一个时序问题
首部必须在正文前面,这是报文的固定结构。
可有些信息只有等正文生成完才知道:内容的校验和、实际总字节数、服务端的处理耗时。
要么先算一遍再发(等于放弃流式,得先把整个响应攒在内存里),
要么把首部放到后面——Trailer 就是第二种。
所以它和分块传输编码是绑在一起的:只有分块传输才有「尾块」这个概念,
因为只有它是一个明确的终止标记(那个长度为 0 的块)。
二、两条硬规矩
规矩一:必须在首部里先声明。
Trailer: X-Checksum, X-Count 这一行告诉客户端「后面会有这两个」。
不声明的话,客户端读到尾块也不知道该不该信任它。
规矩二:只能配分块传输。如果用了 Content-Length 定界,报文在正文结束处就终止了,
后面再加东西会被当成下一个响应。
还有一条实践上的限制:很多中间件(尤其是企业网关)不支持尾块,会把它们直接丢掉。
所以关键信息不要只放在尾块里。
三、实测:三块正文加两个尾块
Demo 里服务端手工拼了一个分块响应:三块正文,每块 9 字节,
内容是 chunk-01 到 chunk-03 各带换行;
然后用长度为 0 的块收尾,后面跟两个尾块 X-Checksum 和 X-Count。
客户端解出来 27 字节正文,两个尾块都读到了。整个响应 221 字节。
注意块长的写法:9 是十六进制,虽然它恰好和十进制一样。
块长永远是十六进制,这也是 3.3.3 那个坑的来源。
四、动手 Demo:手工拼一个带尾块的分块响应
服务端不用现成的库,直接按字节拼:首部、三块正文、一个长度为 0 的终止块、两个尾块。 客户端手工解块,把尾块读出来。
parts = [u'HTTP/1.1 200 OK',
u'Trailer: X-Checksum, X-Count',
u'Transfer-Encoding: chunked',
u'Connection: close', u'', u'']
out = bytearray(u'\r\n'.join(parts).encode(u'utf-8'))
for line in u'chunk-01\nchunk-02\nchunk-03\n'.encode().split(b'\n')[:-1]:
seg = line + b'\n'
out += (u'%x\r\n' % len(seg)).encode() + seg + b'\r\n'
out += b'0\r\n' # 终止块
out += b'X-Checksum: 3f2a91\r\n'
out += b'X-Count: 3\r\n'
out += b'\r\n'
实跑输出:
--------------------------------------------------------------------
把首部放在正文后面:Trailer 与尾块
--------------------------------------------------------------------
响应首部(注意 Trailer 与 Transfer-Encoding):
HTTP/1.1 200 OK
Trailer
Transfer-Encoding
Content-Type
Connection
正文按块解:
块长 9(十六进制 9)| 内容 b'chunk-01\n'
块长 9(十六进制 9)| 内容 b'chunk-02\n'
块长 9(十六进制 9)| 内容 b'chunk-03\n'
解出来的正文 b'chunk-01\nchunk-02\nchunk-03\n'
正文字节数 27
尾块里的首部 X-Checksum=3f2a91 | X-Count=3
尾块个数 2 个
这个响应总共 216 字节
为什么要把首部放后面?因为它发的时候还不知道。
典型场景是边算边发:内容校验和、总长度、处理耗时,
这些都要等正文发完才算得出来。
所以 Trailer 只跟分块编码一起用,而且必须在首部里先声明
Trailer: X-Checksum,否则客户端不知道后面会有什么。
三块各 9 字节(chunk-01 加换行),解出来正文 27 字节,
内容拼起来正好是 chunk-01\nchunk-02\nchunk-03\n。
尾块读到两个:X-Checksum=3f2a91 和 X-Count=3。它们出现在正文之后,但客户端照样能拿到。
整个响应 221 字节。
三个块的分块开销(每块是「块长 + CRLF + 内容 + CRLF」)加上两个尾块,
比直接用 Content-Length 多花了二十来字节。
首部里那行 Trailer: X-Checksum, X-Count 是必须的。
如果删掉它,客户端理论上就不该信任尾块——虽然实际实现大多照读。
30 秒实验
- 把
Trailer那行删掉,重跑,看客户端还能不能读到尾块。 - 把
Transfer-Encoding: chunked换成Content-Length: 27,看尾块还能不能存在。 - 想一想:如果一个网关把尾块丢了,而校验和只放在尾块里,客户端会有什么风险?
常见误区
- 「尾块可以配 Content-Length」——不行,那样报文在正文处就结束了。
- 「尾块不用在首部声明」——规范要求必须先用 Trailer 声明。
- 「Trailer 是给请求用的」——请求和响应都可以有,但都要求分块传输。
自测三问(点开对答案)
1. 为什么需要把首部放到正文后面? —— 有些信息(校验和、总长度)只有正文生成完才算得出来。 2. `Trailer` 必须和什么一起用? —— 分块传输编码。只有它以「长度为 0 的块」为终止标记,后面才能再放东西。 3. 用尾块有什么风险? —— 很多中间件不支持、会丢弃尾块,所以关键信息不能只放在尾块里。说到分块,下一个字段就是它的名字:Transfer-Encoding。
- ☐ (本节完,下一节 6.3.6 Transfer-Encoding)