本篇文章会从如下角度介绍 HTTP 协议:
- 原理与工作机制
- 请求方法与状态码
- Header 与 Body
1、原理与工作机制
1.1 HTTP 是什么
HyperText Transfer Protocol,超文本传输协议,"超"表示扩展而非超级,即可以链接到其他文本的扩展型文本。典型代表是 HTML 文档,其中包含标题、段落等结构化元素和超链接功能。最初 HTTP 就是为传输 HTML 而设计,它可以解决资源定位问题,确保客户端能获取特定资源。
1.2 HTTP 的工作方式
基本流程:浏览器地址栏输入 URL → 发送请求到服务器 → 服务器处理请求 → 返回响应 → 浏览器渲染显示。
以上流程涉及到两个核心组件:
- 渲染引擎:负责将 HTML 文本转换为可视内容(如 Chrome 使用 Blink,Safari 用 WebKit,Firefox 用 Gecko)
- 服务器交互:实际工作包含请求 → 响应的完整周期,而非简单的地址栏输入
我们来了解 URL 如何转成 HTTP 报文,以及报文的格式。
URL 通常由协议类型、服务器地址、路径三部分组成:
上述地址可转换成如下精简形式的 HTTP 请求报文(精简请求头的部分内容):
完整的请求报文如下:
GET /search/users?q=google HTTP/1.1
Accept: */*
Accept-Encoding: gzip, deflate, br
User-Agent: PostmanRuntime-ApipostRuntime/1.1.0
Connection: keep-alive
Host: api.github.com
第一行是请求行,有三要素:
- Method:操作类型(GET 获取 / POST 提交,如论坛发帖用 POST)
- Path:资源定位路径(如 /search/users?q=google 定位用户资源)
- HTTP 版本:主流为 1.1,2.0 在 API 服务中逐步普及
第二行到最后一行是请求头 Headers,每个 Header 都是键值对形式,多行之间无需空行。
当然,对于 POST 请求还会有请求体 Body,但是 GET 就没有,这部分后续会详解。
此外,服务器返回的内容称为响应体,它的大致格式如下:
从 Android 开发角度看,开发者通过 API 接口调用(如 Retrofit),底层自动完成报文转换,实际得到的是从完整响应报文中提取的有效载荷(如 JSON),非原始报文。
2、请求方法与状态码
2.1 请求方法
本节来介绍几个 HTTP 常用的请求方法。
GET
GET 是 HTTP/0.9 版本唯一的方法,它的核心功能是用于获取资源。它具备如下特征:
- 绝对不包含请求体(Body)
- 具有幂等特性(多次调用结果相同)
开发时要注意,HTTP 规范要求 GET 不能带 Body,但实际开发中可能遇到不规范设计。Retrofit 等框架会强制校验 GET 是否带 Body 这条规范,如果发送的 GET 请求带有 Body,就会抛出如下异常:
java.lang.IllegalArgumentException:Non-body HTTP method cannot contain @Body.
POST
POST 用于用于增加或修改资源,它具备如下特征:
- 必须包含请求体(Body)
- 不具有幂等性(多次调用可能产生多个资源)
POST 请求报文大致如下:
POST /users HTTP/1.1
Host: api.github.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 21
name=Tony&gender=male
请求头 Headers 与请求体 Body 之间隔一个空行。Body 支持多种格式,如示例中的 x-www-form-urlencoded,具体格式由 Content-Type 头指定。
PUT
PUT 用于修改资源,它具备如下特征:
- 必须包含请求体(Body)
- 具有幂等特性(多次修改结果一致)
与 POST 的区分:
- PUT 专用于修改,POST 还可用于新增
- 实际开发中可互换使用,取决于后端设计
PUT 请求报文大致如下:
PUT /users/1 HTTP/1.1
Host: api.github.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 13
gender=female
DELETE
用于删除资源,特征如下:
- 不需要请求体(Body)
- 通过 URL 路径定位要删除的资源
- 具有幂等特性(首次删除成功,后续操作无效果)
DELETE 请求报文大致如下:
DELETE /users/1 HTTP/1.1
Host: api.github.com
HEAD
HEAD 的功能与 GET 几乎一致,唯一区别是 HEAD 不会返回响应体。
HEAD 主要用于获取信息。比如下载文件之前, 先用 HEAD 发一个请求,可以获得诸如文件大小、是否支持断点续传等信息, 根据这些信息可以做进一步决策,比如是否分段下载等等,最后使用 GET 去进行真正的文件下载。
2.2 状态码
状态码会对 HTTP 响应结果进行类型化描述,如 200 表示获取成功、404 表示内容未找到等。
状态码按首位数字分为 5 大类(1xx - 5xx),每类代表不同响应类型:
- 1xx:临时性消息。如:100 (继续发送)、101(正在切换协议)
- 2xx:成功。最典型的是 200(OK)、201(创建成功)
- 3xx:重定向。如 301(永久移动)、302(暂时移动)、304(内容未改变)
- 4xx:客户端错误。如 400(客户端请求错误)、401(认证失败)、403(被禁止)、404(找不到内容)
- 5xx:服务器错误。如 500(服务器内部错误)
下面稍作解释。
1xx
1xx 是临时性消息,不包含最终响应,仅传递过渡信息。比较重要的是 100 与 101 两个状态码:
- 100 Continue:请求体较大时的分段传输确认。客户端发送大文件时,请求头中会包含
Expect: 100-continue
字段,服务器接收到之后意识到客户端会继续发送数据,此时会先返回 100 告知客户端可以继续发送,直到客户端所有数据发送完,服务器全部处理好之后才返回 200 - 101 Switching Protocols:协议升级(如 HTTP/1.1 → HTTP/2)。客户端发送请求时带有
Upgrade: h2c
字样,表示询问服务器是否支持 HTTP 2.0。如支持,则服务器返回 101,后续客户端会发送 HTTP 2.0 的相关请求; 否则会直接返回一个 200
2xx
所有 2 开头的状态码均表示请求成功处理,200 表示通用的成功状态,比如网页请求成功:
201 表示创建成功,如成功新建了用户资源。
3xx
重定向状态码,核心机制是通过 Location 头字段指定新资源地址。常用于网站改版、HTTPS 升级、临时维护等场景。
比如我们在浏览器中输入 http://www.baidu.com,观察请求结果会发现有两个访问 www.baidu.com 的请求,状态码分别是 307 与 200:
307 状态码对应的是原始请求:
由于状态码是 307 需要根据响应头的 Location 的值进行重定向,因此 http 请求被重定向到 https 请求:
进行 https 请求时状态码为 200 表示请求成功。
除了上面 307 这个内部重定向外,还有:
- 301:永久重定向(SEO 权重转移)
- 302:临时重定向(保留原地址权重)
需要注意的是,重定向是浏览器行为,它自动跳转到新地址,用户可能感知不到原始请求。
4xx
4 开头的状态码表示请求方(浏览器/客户端)导致的错误。由于请求方的错误导致服务器无法返回正常的内容,此时就会返回 4 开头的状态码,根据错误不同分为如下几种常见情况:
- 401:未授权访问,需要登录后才可访问该内容
- 404:请求了服务器不存在的内容,可能是提供了错误的 URL 导致的
5xx
表示服务端处理请求时发生的内部错误,常见原因:数据库连接失败、资源不足、程序异常等,典型代码有 500 表示通用服务器错误,502 表示网关错误。
将客户端与服务器错误分成两类状态是为了方便程序员调试,而不是给用户看的。
3、Header
虽然 Body 才是报文的核心,但是 Body 的内容是配合 Header 来写的,因此我们要先介绍 Header 的内容。
Header 是 HTTP 消息的 metadata(元数据,即数据的数据),用于描述核心数据(Method/Path/Body)的属性。比如指定 Body 的格式、长度、压缩方式等辅助信息,例如 Content-Type 决定 Body 的解析方式。
下面我们来介绍常用的 Header。
3.1 Host
⽬标主机。注意:Host 不是在⽹络上⽤于寻址的,⽽是在⽬标服务器上⽤于定位⼦服务器的。
比如一个 GET 请求的 Header 中,标明了 Host: api.github.com。这个 Host 地址并不用于网络寻址,因为网络寻址是通过 DNS 服务,由 Host 名字查询到对应的 IP 地址,然后通过 IP 地址查找到服务器。
虚拟主机可以在一个主机上运行多个服务器,比如我买一个阿里云服务器,在上面跑了四个服务器,那么 Host 的名字就用于告知,我要联系的是这个 IP 地址下哪一个具体的子服务器。如果这个阿里云服务器上只跑了一个子服务器,那么无需使用 Host 进行区分。
3.2 Content-Type 与 Content-Length
Content-Type 用于指定 Body 的类型,Content-Length 则是 Body 的长度(单位:字节)。比如:
POST /users HTTP/1.1
Host: api.github.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 21
name=Tony&gender=male
Body 内容 name=Tony&gender=male
的长度就是 Content-Length 的 21。由于 Body 内容可以是二进制,为了避免二进制数据恰好与特殊的换行符或结束符,比如 \n
相同而造成数据截断,因此使用 Content-Length 告知 Body 的长度。
至于 Content-Type,主要有五类,逐一来看。
text/html
请求 Web ⻚⾯时返回响应的类型,Body 中返回 html ⽂本,说白了就是 html 页面。格式如下:
HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
Content-Length: 853
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
...
application/x-www-form-urlencoded
Web 页面纯文本表单,也称为普通表单,形如下图:
代码如下,注意 enctype 的值就是 application/x-www-form-urlencoded
:
提交表单发送网络请求时,可以看到请求头中的 Content-Type 的值就是 application/x-www-form-urlencoded
,而提交的表单内容则在请求体中:
POST /users HTTP/1.1
Host: api.github.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 27
name=Tony&nickname=Iron Man
对于 Android 开发而言,使用 Retrofit 发送上述请求时,需要这样配置 Retrofit 接口函数:
@FormUrlEncoded
@POST("/users")
Call<User> addUser(@Field("name") String name, @Field("nickname") String nickname);
使用 @FormUrlEncoded 注解,Retrofit 在拼接请求时才会将 Content-Type 赋值为 application/x-www-form-urlencoded
这种普通表单格式。又由于表单提交是 POST 请求,所以要使用 @POST 注解,配合 @Field 注解将参数转换成键值对形式。
multipart/form-data
Web ⻚⾯含有⼆进制⽂件时的提交⽅式。使用 multipart 可以传递图片,这也是当前大厂传递图片普遍使用的方式。
比如 Web 页面现在可以上传图片文件:
提交时能看到 Content-Type 的值为 multipart/form-data,而且后面还跟了一个 boundary 分隔符:
POST /users HTTP/1.1
Host: test.com
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Length: 2382
------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="name"
Tony
------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="avatar";
filename="avatar.jpg"
Content-Type: image/jpeg
JFIFHHvOwX9jximQrWa......
------WebKitFormBoundary7MA4YWxkTrZu0gW--
含有二进制文件的表单,每个部分的数据需要用 boundary 这个分割线隔开。第一部分数据的 key 是 name,value 是 Tony;第二部分是图片数据,key 是 avatar,类型是 image/jpeg。
boundary 的值之所以这么长,原因是为了避免与发送的数据内容相同造成错误的数据分割。
对应的 Retrofit 代码:
@Multipart
@POST("/users")
Call<User> addUser(@Part("name") RequestBody name, @Part("avatar") RequestBody avatar);
...
RequestBody namePart = RequestBody.create(MediaType.parse("text/plain"), nameStr);
RequestBody avatarPart = RequestBody.create(MediaType.parse("image/jpeg"), avatarFile);
api.addUser(namePart, avatarPart);
multipart 表单请求必须要加 @Multipart 注解,并且每个 part 的数据由 @Part 注解指定。
application/json
JSON 形式,用于 Web Api 的响应或 POST / PUT 请求。这种格式在请求体与响应体中都会用到,比如在请求中提交 JSON:
POST /users HTTP/1.1
Host: test.com
Content-Type: application/json; charset=utf-8
Content-Length: 32
{"name":"Tony","gender":"male"}
对应的 Retrofit 代码:
@POST("/users")
Call<User> addUser(@Body("user") User user);
...
// 需要使⽤ JSON 相关的 Converter,如在配置 Retrofit 时添加 GsonConverterFactory
api.addUser(user);
响应中返回的 JSON 数据:
HTTP/1.1 200 OK
content-type: application/json; charset=utf-8
content-length: 234
[{"login":"mojombo","id":1,"node_id":"MDQ6VXNlcjE=","avatar_url":"https://avatars0.githubusercontent.com/u/1?v=4","gravat......
通常需要配置 Retrofit 添加 GsonConverterFactory 将 JSON 格式的数据反序列化成对象:
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://api.github.com/")
.addConverterFactory(GsonConverterFactory.create())
.build();
其他单文件类型
比如 image/jpeg、application/zip 这些单文件类型,也是用于 Web Api 的响应或 POST / PUT 请求。像网络上的图片,它的 Content-Type 就很有可能是 image/jpeg。
当你要上传一个图片时,除了前面讲过的 multipart 表单方式之外,也可以使用如下方式(当然还是用 multipart 的更多)上传图片:
POST /user/1/avatar HTTP/1.1
Host: test.com
Content-Type: image/jpeg
Content-Length: 1575
JFIFHH9......
对应的 Retrofit 代码:
@POST("users/{id}/avatar")
Call<User> updateAvatar(@Path("id") String id, @Body RequestBody avatar);
...
RequestBody avatarBody =RequestBody.create(MediaType.parse("image/jpeg"), avatarFile);
api.updateAvatar(id, avatarBody)
而服务器返回图片时,大致内容如下(类型是 image/jpeg,长度 1575,响应体是图片的二进制数据):
HTTP/1.1 200 OK
content-type: image/jpeg
content-length: 1575
JFIFHH9......