HTTP协议详解(二):深入理解Header与Body

前言

现在,我们就来看看 HTTP 的 Header 和 Body。主要看 Header,因为 Body 的具体格式通常是由 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 有两个作用:

  1. 在请求发起前,通过 DNS 查找到目标主机的 IP 地址。

  2. 在请求到达后,作为 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 页面,用于给浏览器渲染界面。

    H 复制代码
    HTTP/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)

    上面代码构建的请求报文,会是下面这样的:

    H 复制代码
    POST /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: 这也是一种表单,叫做多部分表单。它相较于普通表单,除了可以提交文字外,还可以提交文件等二进制数据。

    H 复制代码
    POST /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 内容当做一个文件进行传输。

    平时下载图片,收到的响应几乎都是这种格式。

    H 复制代码
    HTTP/1.1 200 OK
    content-type: image/jpeg
    content-length: 13204
    
    hkSfsD89...

    而上传单个文件也可以使用这种格式。

    H 复制代码
    POST /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(缓冲),缓冲用于协调上下游速度不一致的问题,例如:

  1. 上游生产数据太快,下游消费不动,可以将数据先放到缓冲里。

  2. 下游马上要消费大量数据,为此,上游可以提前生产一些到缓冲中。

在 HTTP 中,一般会通过 Cache-Control Header 来控制缓存,策略主要有:

  • 强缓存: 服务器让客户端,在一段时间内都直接使用本地的旧数据,不用来询问我。

  • 协商缓存 : 本地的缓存可能过期了,客户端会去询问服务器:当前的数据标识为 ETage,是否需要更新?如果服务器发现数据未改变,就会返回一个未改变的标志(状态码 304),让客户端接着使用旧数据。这样避免了内容的传输重复,提高了效率。

相关推荐
CANI_PLUS4 小时前
ESP32将DHT11温湿度传感器采集的数据上传到XAMPP的MySQL数据库
android·数据库·mysql
来来走走5 小时前
Flutter SharedPreferences存储数据基本使用
android·flutter
安卓开发者6 小时前
Android模块化架构深度解析:从设计到实践
android·架构
阿豪元代码7 小时前
深入理解 SurfaceFlinger —— 如何调试 SurfaceFlinger
android
阿豪元代码7 小时前
深入理解 SurfaceFlinger —— 概述
android
稚辉君.MCA_P8_Java8 小时前
常见通信协议详解:TCP、UDP、HTTP/HTTPS、WebSocket 与 GRPC
服务器·tcp/ip·http·https·udp
CV资深专家8 小时前
Launcher3启动
android
stevenzqzq9 小时前
glide缓存策略和缓存命中
android·缓存·glide
雅雅姐9 小时前
Android 16 的用户和用户组定义
android