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),让客户端接着使用旧数据。这样避免了内容的传输重复,提高了效率。

相关推荐
小南知更鸟12 小时前
前端静态项目快速启动:python -m http.server 4173 与 npx serve . 全解析
前端·python·http
quanyechacsdn12 小时前
Android Studio创建库文件用jitpack构建后使用implementation方式引用
android·ide·kotlin·android studio·implementation·android 库文件·使用jitpack
程序员陆业聪13 小时前
聊聊2026年Android开发会是什么样
android
编程大师哥13 小时前
Android分层
android
Andy工程师14 小时前
网络响应码(HTTP 状态码)和解析方法
网络·网络协议·http
极客小云15 小时前
【深入理解 Android 中的 build.gradle 文件】
android·安卓·安全架构·安全性测试
Juskey iii15 小时前
Android Studio Electric Eel | 2022.1.1 Patch 2 版本下载
android·ide·android studio
Android技术之家15 小时前
2025年度Android行业总结:AI驱动生态重构,跨端融合开启新篇
android·人工智能·重构
浅陌sss15 小时前
使用Unity从IIS搭建的文件服务器下载资源时出现HTTP/1.1 404 Not Found
运维·服务器·http
洞见前行15 小时前
Android第二代加固技术原理详解(附源码)
android