前言
现在,我们就来看看 HTTP 的 Header 和 Body。主要看 Header,因为 Body 的具体格式通常是由 Header 来决定的。
Header
Header 是 HTTP 请求和响应消息中,用于传递附加信息的部分(也就是"元数据",即描述数据的数据),比如 Content-Type(内容类型)、Content-Length(内容大小)、Content-Encoding(指定的压缩算法)。
我们来看看具体的例子。
Host
Host 用于指定目标服务器的主机地址。比如有这么一个请求报文:
H
GET /article/1 HTTP/1.1
Host: example.com
其中 example.com
指向了目标主机。
这里需要注意,在请求发起之前,浏览器会先从 URL(如 https://example.com
)中提取出域名 example.com
,然后通过 DNS(Domain Name System)查询它对应的 IP 地址。得到 IP 地址后,浏览器才会往这个 IP 地址发送 HTTP 请求,而这个请求的 Header 中就包含了 Host
。
所以 Host
Header 并不是用来查找 IP 地址的,那么它的作用是什么?
其实 Host
是用于让服务器进行判断的。因为一台服务器上可能会运行多个网站(虚拟主机),服务器需要通过 Host
知道当前请求要访问哪个网站。
简单来说,域名 example.com
有两个作用:
-
在请求发起前,通过 DNS 查找到目标主机的 IP 地址。
-
在请求到达后,作为
Host
Header 的值,让目标主机能够定位到具体的子网站。
Content-Type/Content-Length
Content-Type/Content-Length 这两个 Header 分别表示 Body 的类型和长度。
Content-Length
Content-Length 就是 Body 内容的字节数。例如下面这个报文:
H
POST /users/1 HTTP/1.1
Host: example.com
Content-Length: 21
name=rain&gender=male
其 Body 部分(name=rain&gender=male
)共有 21 个字节,所以 Content-Length
的值就是 21。
为什么需要提前指定长度,不能让接收方自己计算吗?
因为 Body 部分可能是二进制数据(如图片、音频)。而二进制数据的内容中,可能包含任意的字节值。这时,我们无法指定一个固定的符号来作为 Body 的结束符,因为任何结束符都可能会在二进制数据中提前出现。所以为了避免数据被意外截断,最简单的解决方法就是在发送前,通过 Content-Length
指定总长度。
Content-Type
Content-Type 分好几种,常用的有:
-
text/html
: HTML 页面,用于给浏览器渲染界面。HHTTP/1.1 200 OK Content-Type: text/html; charset=utf-8 Content-Length: 130488 <!doctype html> <html> <head> <title>这是标题</title> <meta charset="utf-8"> ...
-
application/x-www-form-urlencoded
: 普通表单,它的格式和 URL 参数的格式相同,以key=value
的形式来存储纯文本。只有纯文本的表单才叫普通表单,最常见的就是登录界面:
点击登录时,请求的
Content-Type
就是application/x-www-form-urlencoded
。服务器从 Body 中取出的信息就是username=...&password=...
,中间使用&
进行分隔。如果想要在 Android 中用 Retrofit 提交普通表单,你可以这样写:
kotlin@FormUrlEncoded @POST("user/login") suspend fun login(@Field("username") username: String, @Field("password") password: String)
上面代码构建的请求报文,会是下面这样的:
HPOST /user/login HTTP/1.1 Host: example.com Content-Type: application/x-www-form-urlencoded Content-Length: 30 username=admin&password=123456
所以 Retrofit 中的
@FormUrlEncoded
和@Field
注解是用于发送这种普通表单请求的。 -
multipart/form-data
: 这也是一种表单,叫做多部分表单。它相较于普通表单,除了可以提交文字外,还可以提交文件等二进制数据。HPOST /users HTTP/1.1 Host: example.com Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW Content-Length: 149056 ----WebKitFormBoundary7MA4YWxkTrZu0gW Content-Disposition:form-data; name="name" jack ----WebKitFormBoundary7MA4YWxkTrZu0gW Content-Disposition:form-data; name="avatar"; filename="my_avatar.jpg" Content-Type: image/jpeg 4AAQSkZJRgABAQEASABIAAD... ----WebKitFormBoundary7MA4YWxkTrZu0gW
可以看到,它多出了一个
boundary
,用来分隔 Body 中的不同部分。为什么需要专门分隔呢?
其实普通表单也需要分隔,只不过分隔符为
&
。但如果要提交二进制文件,就不能使用简单、固定的符号来分隔了,因为文件的内容中可能包含这个符号,这样会导致解析错误。所以,需要一个独特的、不容易重复的随机字符串来做分隔符,也就是上述的boundary
。当然,文件的二进制内容中也可能会出现 boundary,只不过这个概率非常低,可以忽略。就算真遇到了,也只需重发一次请求就行。
如果想要使用 Retrofit 提交这种表单,你可以这样写:
kotlin@Multipart @PUT("user/avatar") suspend fun updateUserAvatar(@Part("avatar") avatar: RequestBody): User
用这种方式来上传图片等文件,是目前最主流的做法。
-
application/json
: 用 JSON 格式传递数据,在现在 Api 的请求或响应中非常常见。H// 请求报文 POST /users HTTP/1.1 Host: example.com Content-Type: application/json; charset=utf-8 Content-Length: 26 {"name":"jack","age":"21"}
H// 响应报文 HTTP/1.1 200 OK content-type: application/json; charset=utf-8 content-length: 157 {"name": "Simon", "age": 2, ...}
它和
application/x-www-form-urlencoded
都可以提交文本数据,实际开发中具体用哪个,应该看后端接口的要求。用 Retrofit 提交 JSON 数据,代码可以这样写:
kotlin@POST("user/new") suspend fun createUser(@Body user: User): User
-
image/jpeg/application/zip ...
: 单文件类型,直接将 Body 内容当做一个文件进行传输。平时下载图片,收到的响应几乎都是这种格式。
HHTTP/1.1 200 OK content-type: image/jpeg content-length: 13204 hkSfsD89...
而上传单个文件也可以使用这种格式。
HPOST /user/1/avatar HTTP/1.1 Host: example.com Content-Type: image/jpeg Content-Length: 13204 hkSfsD89...
有了这个,对于上传单个文件的情况,我们就可以使用这种方式了,这种方式比
multipart/form-data
更直接。使用 Retrofit,代码可以这样写:
kotlin@POST("user/{id}/avatar") suspend fun updateAvatar(@Path("id") id: String,@Body avatar: RequestBody): User // 调用该方法前,创建一个 RequestBody 对象 val avatarBody = avatarFile.asRequestBody("image/jpeg".toMediaTypeOrNull())
Chunked Transfer Encoding
再来看看 Chunked Transfer Encoding
(分块传输编码),它用于将内容分成多个块进行传输。比如当服务器需要耗费很长时间来准备完整的响应数据时,就可以将已经准备好的一部分数据发给客户端,后续的数据等到准备好后再补发。
这时有个问题:因为数据并未完全准备好,服务器不知道内容的总长度(Content-Length
)是多少。为此,就出现了分块传输。
当响应头中出现 Transfer-Encoding: chunked
时,表示当前为分块传输,此时不再有 Content-Length
。
客户端会按照一定格式来接收数据,收到长度为 0 的块时,表示此次传输结束。
H
// Body
<length 1>
<data 1>
<length 2>
<data 2>
...
0 // 0 表示内容结束
这样做可以提高服务器的响应速度。
Location
Location
用来做重定向的,指定一个要跳转的目标 URL。
比如访问 http://example.com
会自动跳转到 https://example.com
,这就是 Location
的作用。
这个过程是:当访问 http://example.com
时,服务器返回的状态码是 3xx
。这时,浏览器会向响应报文中 Location
Header 所指向的 URL(如 https://example.com
)再次发起请求。
这个过程,对于浏览器来说是自动的。我们在代码中向 http://example.com
网址发起请求,也会发生这个过程。这是因为 OkHttp 会自动处理重定向,所以我们通常能够直接拿到最终的网页数据。
User-Agent
User-Agent
是用户代理,就是用于表明客户端的身份的,比如使用的浏览器、使用的操作系统、使用的应用。
例如:
H
user-agent
Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36
服务器可以通过 User-Agent
来判断客户端的类型,然后返回不同的内容,比如给手机返回移动端网页。
Range/Accept Range
Range/Accept Range
用于指定请求内容的范围。
如果响应报文的 Header 中存在 Accept-Ranges: bytes
,说明它支持分段下载。这时,我们就可以在请求头中加上 Range: bytes=0-1024
,表示此次请求只需获取第 0 到 1024 个字节的内容。如果请求的是一张图片,那么这种图片就只会一部分。
分段加载是实现断点续传 和多线程下载的核心技术。
另外,在使用分段加载时,响应头通常会带有 Content-Range: bytes 0-1024/4096
,用于提示客户端当前返回的分段以及内容的总大小。
其他 Header
-
Accept
: 告诉服务器,客户端能接受的数据类型,如application/json
。 -
Accept-Charset
: 告诉服务器,客户端能接受的字符集,如utf-8
。 -
Accept-Encoding
: 告诉服务器,客户端支持的压缩类型,如gzip
。 -
Content-Encoding
: 服务器告诉客户端,实际使用的压缩类型,如gzip
。
Cache
Cache
就是缓存,将使用过的数据存起来,之后可能还会用到。
和它容易搞混的一个概念是 Buffer
(缓冲),缓冲用于协调上下游速度不一致的问题,例如:
-
上游生产数据太快,下游消费不动,可以将数据先放到缓冲里。
-
下游马上要消费大量数据,为此,上游可以提前生产一些到缓冲中。
在 HTTP 中,一般会通过 Cache-Control
Header 来控制缓存,策略主要有:
-
强缓存: 服务器让客户端,在一段时间内都直接使用本地的旧数据,不用来询问我。
-
协商缓存 : 本地的缓存可能过期了,客户端会去询问服务器:当前的数据标识为
ETage
,是否需要更新?如果服务器发现数据未改变,就会返回一个未改变的标志(状态码304
),让客户端接着使用旧数据。这样避免了内容的传输重复,提高了效率。