写一个导出接口,数据量大,后端同事说「我先查完数据库再一起返回」。结果用户点导出,等了 30 秒页面没动静,以为卡死了。其实后端在查数据,只是 HTTP body 要等全部准备好才能发——因为请求里带了 Content-Length,必须一次发完。
这个体验问题的根源,是对 body 的边界机制理解不够。body 不是「想发就发的数据」,它得先告诉对方「我要发多少、什么时候发完」。这一篇讲清楚 body 怎么定边界、怎么编码,以及不同编码方式的取舍。
这是 URL 与报文阶段的最后一篇,把报文层讲透后,下一篇就要进连接与传输了。
body 的边界:三种告诉对方「发完了」的方式
一个 body 最基本的问题:接收方怎么知道它结束了?HTTP 有三种方式,对应三种场景:
- Content-Length:最常见。请求/响应头里写
Content-Length: 128,接收方读到 128 字节就知道 body 结束了。前提是发送前就知道总长度——静态文件、已知大小的 JSON 都用它。 - Transfer-Encoding: chunked:发送前不知道总长度时用。把 body 切成一块一块,每块前面标明这块多长,最后用一个 0 长度的块表示结束。动态生成的内容、流式响应(SSE)都靠它。
- 读到连接关闭:HTTP/1.0 的方式,靠关闭 TCP 连接表示「发完了」。HTTP/1.1 不推荐,因为没法复用连接。现在基本只在响应里偶尔见到(没有 Content-Length 也没有 chunked 时)。
第一种 Content-Length 的限制最直观:必须先把整个 body 准备好才能发。这就是开头那个导出接口慢的原因——后端要等数据全部查完,算出总长度,才能开始发。如果用 chunked,后端可以查一批发一批,用户立刻看到「正在导出」的进度,体验完全不同。
chunked 的格式长这样,每块是「长度行 + 数据 + \r\n」:
1a\r\n
(这块 26 字节的数据)\r\n
0\r\n
\r\n
最后那个 0\r\n\r\n 是「长度为 0 的块 + 空行」,表示 body 结束。这种结构让接收方可以边读边处理,不用等全部到位。
body 的四种常见编码格式
边界解决了「怎么知道发完」,编码格式解决「body 里的数据怎么组织」。常见的有四种,各有适用场景:
| 格式 | Content-Type | 典型场景 | 特点 |
|---|---|---|---|
| URL 编码 | application/x-www-form-urlencoded |
传统表单、简单键值对 | key=value&key2=value2,特殊字符转义 |
| JSON | application/json |
API 接口 | 结构化,前后端通用 |
| multipart | multipart/form-data |
上传文件 | 用 boundary 分隔多段,支持二进制 |
| SSE | text/event-stream |
服务端推送 | 长连接,服务器单向推多条消息 |
前两种简单,重点说后两个。
multipart/form-data 是上传文件必须用的格式。为什么不能用 JSON 传文件?因为 JSON 是文本格式,文件是二进制,直接塞 JSON 要 base64 编码(膨胀 33%),而且 JSON 没法在一条消息里同时带「文件 + 普通字段」。multipart 的设计就是「一条 body 里塞多段不同类型的数据」:
它靠一个叫 boundary 的随机字符串分隔每段。请求头里声明 Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxk,body 里每段用 --boundary 开头,最后用 --boundary-- 结尾。每段有自己的小 header(比如 Content-Disposition: form-data; name="file"; filename="a.png"),告诉接收方这段是文件还是普通字段。
这个设计的好处是:一段 body 可以混合文本字段和二进制文件,文件不用编码直接传原字节。代价是格式比 JSON 啰嗦,而且 boundary 必须选一个「在 body 内容里不会出现」的字符串——如果选不好,body 里恰好有和 boundary 一样的字节序列,解析就会错位。
SSE(Server-Sent Events) 用 text/event-stream,是服务器向客户端的单向流式推送。它和 chunked 配合:服务器保持连接开着,用 chunked 一条一条推 data: {...}\n\n 格式的消息。ChatGPT 那种「打字机效果」就是 SSE——答案一边生成一边推,不用等全部生成完。SSE 是单向的(只能服务器推客户端),如果需要双向,得用 WebSocket。
取舍与边界
body 这套机制有几个值得注意的边界:
- Content-Length 和 chunked 不能同时用:一个 body 只能用一种边界方式。如果同时出现,规范以 chunked 为准(但实际实现可能不一致,别这么干)。
- GET 请求带 body 是灰色地带:RFC 没禁止 GET 带 body,但也没定义它的语义。很多代理、服务器会直接忽略 GET 的 body。Elasticsearch 早期用 GET 带 body 查询,结果一路踩坑,后来改成 POST。结论:需要带 body 用 POST,别用 GET。
- body 大小没有协议上限,但处处有实践上限:服务器有
max_body_size(Nginx 默认 1MB),网关有超时,客户端有内存限制。传大文件别用一条 body 硬塞,用分片上传或范围请求。 - Trailer 头:chunked 模式下,body 结束后还能跟几个 header(叫 trailer),用于发送「整个 body 处理完才知道」的信息,比如整个流的校验和。但浏览器对 trailer 支持很差,实战少见。
收束:报文层讲透了,该进连接层了
到这里,URL、header、method、status、body 这五个报文层的核心都讲完了。一条 HTTP 报文怎么构造、怎么表达意图、怎么携带数据,你已经有了完整的图。
但这套报文层是建立在一个假设上的:底下有一条可靠的传输通道。报文能发出去、能收到、顺序不乱——这都靠下一层的连接保证。下一篇开始进连接与传输,先讲这条传输通道怎么建立:TCP 的三次握手。
关于十三Tech
我是十三,All in AI Agent 方向的架构师,专注 AI 工程实践。
我相信 AI 是程序员的最佳搭档,也希望帮助每一位开发者更好地驾驭 AI。
如果你想继续跟完这套「图解 HTTP」,欢迎关注公众号 「十三Tech」。后续会按 URL 与报文、连接与传输、缓存与协商、安全与边界、HTTP/2 与 HTTP/3 这条线更新。

