写一个导出接口,数据量大,后端同事说「我先查完数据库再一起返回」。结果用户点导出,等了 30 秒页面没动静,以为卡死了。其实后端在查数据,只是 HTTP body 要等全部准备好才能发——因为请求里带了 Content-Length,必须一次发完。

这个体验问题的根源,是对 body 的边界机制理解不够。body 不是「想发就发的数据」,它得先告诉对方「我要发多少、什么时候发完」。这一篇讲清楚 body 怎么定边界、怎么编码,以及不同编码方式的取舍。

这是 URL 与报文阶段的最后一篇,把报文层讲透后,下一篇就要进连接与传输了。

body 的边界:三种告诉对方「发完了」的方式

一个 body 最基本的问题:接收方怎么知道它结束了?HTTP 有三种方式,对应三种场景:

Content-Length 已知长度 vs chunked 流式传输

  • 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 里的数据怎么组织」。常见的有四种,各有适用场景:

四种 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 里塞多段不同类型的数据」:

multipart/form-data 的 boundary 分隔机制

它靠一个叫 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 这条线更新。

十三Tech公众号二维码