普通表单和 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 位置的对应关系
关键点:
@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 表示服务器是否支持按范围请求资源。
如果服务器支持,客户端可以用 Range 请求资源的一部分:
这表示只请求第 0 到第 30000 个字节。
常见用途:
当服务器返回部分内容时,响应里可能包含:
Content-Range: bytes 0-30000/60058
它表示本次返回的是总资源中的哪一段,以及总大小是多少。
Cookie 和 Authorization
视频中提到 Cookie、Set-Cookie 和 Authorization 也很重要,但这一节没有展开,因为它们会放到登录和授权章节里专门讲。
简单理解:
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 是缓冲,通常存在于一个工作流中,有上游和下游。
典型场景:
所以:
本节最重要的知识点
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 的总长度。
Location Header 在重定向中起什么作用?
答:Location 告诉客户端下一次应该请求哪个 URL。客户端收到 301、302 等重定向状态码后,会读取 Location,再向这个新地址发起请求。
断点续传为什么需要 Range / Accept-Ranges?
答:Accept-Ranges 表示服务器支持按范围返回资源,Range 表示客户端想请求哪一段字节。下载中断后,客户端可以从已下载位置继续请求剩余字节,因此可以实现断点续传。
Cache 和 Buffer 的区别是什么?
答:Cache 是缓存,重点是“以后可能还会用,所以先保存起来复用”,例如图片缓存、HTTP 缓存。Buffer 是缓冲,重点是“上下游处理速度不一致,所以先临时存放”,例如上游生产太快、下游暂时处理不过来时用 Buffer 垫一下。