HTTP 的 Header 和 Body
核心结论
这一节表面上讲 Header 和 Body,实际重点是 Header。原因是:Body 才是 HTTP 报文承载业务数据的主体,但 Body 应该如何解释、如何读取、是什么格式,往往都由 Header 决定。
例如:
Content-Length告诉接收方 Body 有多少字节。Content-Type告诉接收方 Body 应该按什么格式解析。Transfer-Encoding: chunked告诉接收方 Body 会分块发送。Host告诉目标主机应该把请求转给哪一个虚拟主机。
所以理解 HTTP 的 Body,不能只看 Body 自己,还要看与它配套的 Header。
Header 是什么
Header 是 HTTP 消息的元数据,也就是“关于数据的数据”。
在 HTTP 报文中,真正的核心数据包括:
- 请求方法,例如
GET、POST。 - 路径,例如
/users/1。 - Body,例如表单、JSON、图片、压缩包等。
而 Header 用来描述、辅助和修饰这些核心数据,例如:
- Body 有多长。
- Body 是什么格式。
- 数据有没有压缩。
- 请求来自什么客户端。
- 是否需要重定向。
- 是否支持缓存、断点续传等。
Host
Host 表示目标主机名,例如:
GET /users/1 HTTP/1.1
Host: api.example.com
容易误解的一点是:Host 不是用来查找 IP 地址的。
域名到 IP 的查找发生在请求真正发送之前,这个过程叫 DNS 查询。浏览器或客户端会先把 api.example.com 这样的域名解析成 IP 地址,然后再向这个 IP 地址发送 HTTP 请求。
那 Host 的作用是什么?
它主要用于虚拟主机场景。一个物理主机或云主机上可能运行多个站点,例如:
a.example.comb.example.comc.example.com
这些域名可能最终解析到同一个 IP。请求到达这台主机后,主机需要根据 Host 判断请求到底应该交给哪个站点或服务处理。
所以域名有两个阶段的作用:
- 请求发送前:通过 DNS 查询目标 IP。
- 请求到达后:放进
HostHeader,帮助目标主机定位具体虚拟主机。
Content-Length
Content-Length 表示 Body 的字节长度。
POST /users HTTP/1.1
Content-Length: 27
name=rengwuxian&gender=male
它的作用不是“让接收方提前知道大小”这么简单,而是解决 Body 结束位置的问题。
HTTP 的 Body 不只可以是文本,也可以是图片、文件等二进制数据。二进制数据里任何字节值都可能出现,因此不能随便规定某个字符作为“结束符”。如果用换行符作为结束符,Body 内部正好出现换行符时就会被误判截断。
因此 HTTP 选择提前告诉接收方长度:接收方按字节数读取,读够 Content-Length 指定的长度,就知道 Body 到这里结束。
Content-Type
Content-Type 表示 Body 的类型,也就是接收方应该用什么方式解析 Body。
text/html
网页响应常见类型:
Content-Type: text/html
Body 是 HTML 文本,浏览器会按 HTML 页面解析和渲染。
浏览器并不是天然知道所有响应都是网页。用户访问的也可能是 JSON API、图片、压缩包等,所以服务器需要通过 Content-Type 告诉浏览器响应内容的类型。
application/x-www-form-urlencoded
这是普通表单,也就是纯文本表单。
POST /users HTTP/1.1
Content-Type: application/x-www-form-urlencoded
Content-Length: 32
firstName=kai&lastName=zhu
它用 URL 查询参数类似的格式存储键值对:
- 键和值用
=连接。 - 多个键值对用
&分隔。
在 Retrofit 中,对应关系通常是:
@FormUrlEncoded
@POST("users")
fun updateUser(
@Field("firstName") firstName: String,
@Field("lastName") lastName: String
): Call<ResponseBody>
@FormUrlEncoded 的作用是告诉 Retrofit:这个请求的 Body 要按普通表单格式组装。@Field 表示表单里的每个字段。
multipart/form-data
multipart/form-data 也是表单,但它适合包含文件的表单,例如上传头像时同时提交文字和图片。
Content-Type: multipart/form-data; boundary=----abc123
boundary 是分隔符,用来把 Body 里的多个 part 分开。每个 part 可以有自己的字段名、文件名、内容类型和内容。
普通表单可以用 & 分隔字段,因为它只传文本;但 multipart 可能传二进制文件,不能保证文件内容里不会出现某个普通分隔符,所以需要一个更长、更特殊、极小概率重复的 boundary。
在 Retrofit 中,对应关系通常是:
@Multipart
@POST("users/avatar")
fun uploadAvatar(
@Part avatar: MultipartBody.Part,
@Part("description") description: RequestBody
): Call<ResponseBody>
注意:
@FormUrlEncoded和@Multipart不能同时使用。@Field属于普通表单。@Part属于 multipart。- 二者是两种不同的 Body 格式。
application/json
JSON 既可以用于响应,也可以用于请求。
响应示例:
HTTP/1.1 200 OK
Content-Type: application/json
{"id":1,"name":"Kai"}
请求示例:
POST /users HTTP/1.1
Content-Type: application/json
{"firstName":"Kai","lastName":"Zhu"}
普通表单和 JSON 都能提交文本数据。实际开发中到底用哪种,不由移动端单方面决定,而是由后端接口格式决定。后端要求普通表单,就用 @FormUrlEncoded + @Field;后端要求 JSON,就用 JSON。
在 Retrofit 中,JSON 请求通常用 @Body:
@POST("users")
fun createUser(
@Body user: User
): Call<ResponseBody>
同时需要给 Retrofit 配置转换器,例如 Gson、Moshi、kotlinx.serialization 等,把对象转换成 JSON 字符串,也把 JSON 响应转换成对象。
单文件类型
常见响应类型:
Content-Type: image/jpeg
Content-Type: application/zip
这类 Content-Type 表示 Body 本身就是一个文件,例如图片、压缩包等。
上传单个文件时,也可以直接把文件作为整个 Body,而不一定非要使用 multipart。例如上传头像:
@PUT("users/{id}/avatar")
fun updateAvatar(
@Path("id") id: String,
@Body avatar: RequestBody
): Call<ResponseBody>
调用时构造带 Content-Type 的 RequestBody:
val body = imageFile.asRequestBody("image/jpeg".toMediaType())
api.updateAvatar(userId, body)
这种方式比 multipart 更简单、更直接,适合“整个 Body 就是一个文件”的接口。不过实际公司里,上传图片更常见的仍然是 multipart,因为它来自传统网页表单上传的历史习惯。
Retrofit 参数和 HTTP 位置的对应关系
| Retrofit 写法 | 对应位置 | 适用场景 |
|---|---|---|
@Path | URL 路径 | /users/{id} |
@Query | URL 查询参数 | /users?gender=male |
@Field | 普通表单 Body | application/x-www-form-urlencoded |
@Part | multipart 的某个 part | multipart/form-data |
@Body | 整个 Body | JSON、单文件、自定义 Body |
关键点:
@Query不属于 Body,它在 URL 上。@Field必须配合@FormUrlEncoded。@Part必须配合@Multipart。@Body表示把参数作为整个 Body,常用于 JSON 或单文件上传。
Transfer-Encoding: chunked
Transfer-Encoding: chunked 表示分块传输。
Transfer-Encoding: chunked
有时服务端不能立刻准备完整响应,只能先准备一部分。如果等全部数据都准备好再发送,用户会多等一段时间。分块传输可以让服务端先发已经准备好的部分,后续内容准备好后再继续发送。
使用 chunked 时,服务端不知道完整 Body 长度,因此不能写 Content-Length。
分块格式大致是:
4
Chun
9
ked Trans
12
fer Encoding
0
每个块由两部分组成:
- 块长度。
- 块内容。
最后用长度 0 表示传输结束。
Location
Location 用于重定向,告诉客户端下一次应该请求哪个 URL。
HTTP/1.1 301 Moved Permanently
Location: https://example.com
例如访问 http://example.com 时,服务器可能返回 301,并在 Location 里告诉客户端跳到 https://example.com。
浏览器收到 301/302 等重定向状态码后,会读取 Location,再自动发起第二次请求。OkHttp 默认也会自动处理重定向,所以移动端代码里看到的往往已经是第二次请求后的最终响应。
User-Agent
User-Agent 表示发起请求的客户端是什么。
User-Agent: Mozilla/5.0 ...
它可以告诉服务器:
- 客户端是浏览器还是 App。
- 是桌面设备还是手机。
- 操作系统是什么。
- 浏览器内核或版本是什么。
服务端可以据此返回不同内容。例如同一个网页,桌面浏览器返回桌面版,手机浏览器返回手机版。
视频里还提到一个历史原因:很多浏览器的 User-Agent 都以 Mozilla 开头,这是早期浏览器兼容大战留下的结果。为了让网站认为自己兼容 Netscape/Mozilla,各浏览器都在 User-Agent 中带上了类似标识,后来逐渐变成惯例。
Range 和 Accept-Ranges
Accept-Ranges 表示服务器是否支持按范围请求资源。
Accept-Ranges: bytes
如果服务器支持,客户端可以用 Range 请求资源的一部分:
Range: bytes=0-30000
这表示只请求第 0 到第 30000 个字节。
常见用途:
- 断点续传:下载中断后,从已下载位置继续请求。
- 多线程下载:多个连接分别下载不同字节范围,再合并。
当服务器返回部分内容时,响应里可能包含:
Content-Range: bytes 0-30000/60058
它表示本次返回的是总资源中的哪一段,以及总大小是多少。
Cookie 和 Authorization
视频中提到 Cookie、Set-Cookie 和 Authorization 也很重要,但这一节没有展开,因为它们会放到登录和授权章节里专门讲。
简单理解:
Cookie/Set-Cookie常用于保存和传递会话状态。Authorization常用于携带认证凭证,例如 token。
Accept、压缩和编码相关 Header
视频里还简要提到了一些 Header:
Accept:客户端能接受什么类型的响应,例如application/json。Accept-Encoding:客户端能接受什么压缩编码,例如gzip。Content-Encoding:服务器实际使用了什么压缩编码。
这些 Header 对移动端日常业务开发通常不如 Content-Type、Content-Length、Host 常用,但在和后端沟通接口、压缩、编码、性能优化时很重要。
Cache 和 Buffer 的区别
这一节最后用 Cache 引出了 Cache 和 Buffer 的区别。
Cache
Cache 是缓存:这次用完的数据,后面可能还会用,所以先保存起来,避免重复请求或重复计算。
例如图片缓存、HTTP 缓存等。
HTTP 缓存大致有两类方式:
- 服务器直接告诉客户端资源什么时候过期,过期前直接用本地缓存。
- 服务器给资源一个标签或指纹,例如
ETag。下次客户端带着这个标签询问服务器资源是否变化。如果没变化,服务器可以返回304 Not Modified,客户端继续用本地缓存。
Buffer
Buffer 是缓冲,通常存在于一个工作流中,有上游和下游。
典型场景:
- 上游生产太快,下游处理不过来,先把数据放到 Buffer。
- 下游稍后会集中消费,上游提前准备一批数据放到 Buffer。
所以:
Cache关注“以后可能还用,所以先存着”。Buffer关注“上下游速度不匹配,所以先垫一下”。
本节最重要的知识点
Header是 HTTP 消息的元数据,Body 的很多解释方式由 Header 决定。Host不是用来 DNS 查询的,而是请求到达目标主机后,用来定位具体虚拟主机。Content-Length用字节数标记 Body 长度,避免二进制数据无法可靠使用结束符的问题。Content-Type决定 Body 格式,是理解 Retrofit@Field、@Part、@Body的关键。application/x-www-form-urlencoded是普通文本表单。multipart/form-data用boundary切分多个 part,常用于文件上传。application/json可以用于请求和响应,Retrofit 中通常配合@Body和 converter。- 单文件上传可以把整个文件作为 Body,而不是一定要 multipart。
Transfer-Encoding: chunked用于不知道完整长度时的分块传输。Location配合 301/302 等状态码完成重定向。Range和Accept-Ranges支持断点续传和多线程下载。Cache和Buffer都可译作“缓存/缓冲”,但前者是复用数据,后者是协调上下游速度。
复习问题
-
为什么说 Body 的格式是由 Header 决定的?
答:因为 Body 只是承载具体数据,而数据应该怎么读取、怎么解析、是什么格式,通常要看 Header。例如
Content-Length决定读取多少字节,Content-Type决定按 HTML、JSON、表单、文件等哪种格式解析。 -
Host和 DNS 查询分别发生在什么时候?各自解决什么问题?答:DNS 查询发生在请求发送前,用来把域名解析成目标 IP 地址。
Host在 HTTP 请求 Header 中随请求一起发送,用来让目标主机在收到请求后判断应该交给哪个虚拟主机或站点处理。 -
为什么 Body 不能简单用换行符作为结束标记?
答:因为 Body 可能是二进制数据,而二进制数据中任何字节值都可能出现,包括换行符。如果把换行符当结束标记,Body 里的普通数据可能被误判为结束,导致数据被截断。
-
Content-Length的单位是什么?答:单位是字节。它表示 Body 一共有多少个字节,而不是多少个字符。
-
普通表单和 multipart 表单有什么区别?
答:普通表单的类型是
application/x-www-form-urlencoded,主要提交纯文本键值对,用=连接键和值,用&分隔多个字段。multipart 表单的类型是multipart/form-data,可以提交多个 part,常用于包含文件的上传场景,用boundary分隔不同 part。 -
Retrofit 中
@Query、@Field、@Part、@Body分别对应 HTTP 报文的哪里?答:
@Query对应 URL 查询参数;@Field对应普通表单 Body 里的字段;@Part对应 multipart Body 里的某个 part;@Body对应整个 Body,常用于 JSON 请求或单文件上传。 -
为什么
@FormUrlEncoded和@Multipart不能同时使用?答:因为它们代表两种不同且不兼容的 Body 格式。
@FormUrlEncoded会把 Body 组装成普通表单格式,@Multipart会把 Body 组装成 multipart 格式,一个请求的 Body 不能同时按两种格式编码。 -
JSON 请求为什么需要 converter?
答:因为 Retrofit 需要把 Kotlin/Java 对象转换成 JSON 字符串放进请求 Body,也需要把响应里的 JSON 字符串转换成对象。Gson、Moshi、kotlinx.serialization 等 converter 就负责这个转换过程。
-
Transfer-Encoding: chunked为什么可以不写Content-Length?答:因为 chunked 是分块传输,每个块都会先声明自己的长度,最后用长度为
0的块表示传输结束。接收方可以根据每块长度和结束块判断 Body 边界,所以不需要提前知道完整 Body 的总长度。 -
LocationHeader 在重定向中起什么作用?
答:Location 告诉客户端下一次应该请求哪个 URL。客户端收到 301、302 等重定向状态码后,会读取 Location,再向这个新地址发起请求。
- 断点续传为什么需要
Range/Accept-Ranges?
答:Accept-Ranges 表示服务器支持按范围返回资源,Range 表示客户端想请求哪一段字节。下载中断后,客户端可以从已下载位置继续请求剩余字节,因此可以实现断点续传。
- Cache 和 Buffer 的区别是什么?
答:Cache 是缓存,重点是“以后可能还会用,所以先保存起来复用”,例如图片缓存、HTTP 缓存。Buffer 是缓冲,重点是“上下游处理速度不一致,所以先临时存放”,例如上游生产太快、下游暂时处理不过来时用 Buffer 垫一下。