HTTP 请求报文结构详解:请求行、请求头、请求体各自的作用
一、请求报文的整体结构
一个 HTTP 请求报文由四部分组成:
请求行
请求头
空行
请求体
用文本表示:
POST /api/user HTTP/1.1 ← 请求行
Host: localhost:8080 ← 请求头开始
Content-Type: application/json
Content-Length: 42
Authorization: Bearer xxx
← 空行
{"username":"jack","age":25} ← 请求体
空行是请求头和请求体的分隔符,没有请求体时,空行之后直接结束。
三部分不是随意拆分的,而是按"做什么、怎么描述、带什么数据"三层职责划分。
二、请求行:说明"对谁做什么"
1. 结构
GET /api/user/1?status=active HTTP/1.1
| 部分 | 示例 | 作用 |
|---|---|---|
| 请求方法 | GET |
表达操作意图:查、建、改、删 |
| 请求目标 | /api/user/1?status=active |
表达操作对象:路径 + 查询参数 |
| 协议版本 | HTTP/1.1 |
表达通信规则 |
2. 作用
请求行是服务器的第一层路由依据。
服务器拿到请求后,第一件事就是读请求行:
- 用什么方法?决定交给哪个处理器。
- 请求哪个路径?决定匹配哪个接口。
- 什么协议版本?决定用哪套解析规则。
Spring MVC 的 DispatcherServlet 也是先解析请求行,再通过 HandlerMapping 按 方法 + 路径 查找对应的 Controller 方法。
3. 为什么方法放在请求行
方法不是数据,是动作。它和路径一起构成"操作意图",必须在服务器读取任何数据之前就确定。
如果把方法放在请求体里,服务器就必须先读完整个请求体才能知道要做什么,这对路由、鉴权、限流都不利。
三、请求头:说明"这个请求有什么特征和约束"
1. 结构
请求头是一组键值对,每行一个:
Host: localhost:8080
Content-Type: application/json
Content-Length: 42
Accept: application/json
Authorization: Bearer xxx
User-Agent: Apipost/1.0
Cookie: sessionId=abc123
2. 作用
请求头负责描述请求的元信息,常见类别:
| 类别 | 请求头 | 作用 |
|---|---|---|
| 目标信息 | Host |
请求哪个主机 |
| 内容描述 | Content-Type、Content-Length |
请求体是什么格式、有多长 |
| 内容协商 | Accept、Accept-Language |
客户端希望收到什么格式、什么语言 |
| 身份认证 | Authorization、Cookie |
是谁在请求 |
| 客户端信息 | User-Agent |
用什么客户端 |
| 缓存控制 | If-None-Match、Cache-Control |
缓存策略 |
| 链路追踪 | X-Request-Id、Traceparent |
请求链路标识 |
3. 为什么需要请求头
请求头解决的是"在不读请求体的情况下,就能知道请求的关键属性"。
举例:
- 服务器看到
Content-Type: application/json,才知道该用 Jackson 解析请求体。如果这个信息放在请求体里,服务器必须先解析才能知道怎么解析,形成死循环。 - 服务器看到
Authorization,可以先做鉴权,不通过直接返回 401,不需要读取请求体,节省资源。 - 代理服务器看到
Host,可以决定把请求转发到哪台后端机器,不需要理解请求体内容。
请求头是分层处理的基础:网关、代理、缓存、鉴权组件都只读请求头,不动请求体。
四、请求体:携带"真正的业务数据"
1. 结构
请求体的格式由 Content-Type 决定,常见三种:
表单:
Content-Type: application/x-www-form-urlencoded
username=jack&password=123456
JSON:
Content-Type: application/json
{"username":"jack","password":"123456"}
文件:
Content-Type: multipart/form-data; boundary=----Boundary
------Boundary
Content-Disposition: form-data; name="file"; filename="a.txt"
Content-Type: text/plain
文件内容
------Boundary--
2. 作用
请求体携带客户端提交的业务数据:
- 创建资源时的字段值。
- 更新资源时的修改内容。
- 上传的文件内容。
- 批量操作的 ID 列表。
3. 为什么请求体单独存在
请求体可能很大,也可能为空。
- GET 请求通常没有请求体,数据在 URL 查询参数里。
- POST 上传文件时,请求体可能是几十 MB。
- 服务器不应该在路由和鉴权阶段就读取请求体,否则会浪费带宽和内存。
把业务数据隔离在请求体里,服务器可以先做路由、鉴权、限流,再决定是否读取请求体。
五、为什么分成三部分
1. 职责分离
| 部分 | 回答的问题 | 谁关心 |
|---|---|---|
| 请求行 | 对谁做什么 | 路由、DispatcherServlet |
| 请求头 | 这个请求有什么特征和约束 | 鉴权、内容协商、缓存、代理 |
| 请求体 | 具体带什么数据 | 业务处理方法 |
每一层只关心自己需要的信息,不需要理解其他层的内容。
2. 分层处理
一个请求可能经过多个组件:
客户端 → 网关 → 代理 → 鉴权服务 → 业务服务
- 网关只读请求行和
Host,做路由。 - 代理只读请求头,做转发。
- 鉴权服务只读
Authorization,做身份校验。 - 业务服务最后才读请求体,处理业务。
如果所有信息混在一起,每个组件都必须解析全部内容,效率低且耦合高。
3. 大小与性能
请求头通常很小,可以被缓存、压缩、快速解析。请求体可能很大,不应该在不需要的时候读取。
分开之后,服务器可以先基于请求行和请求头做出决策,决定是否继续读取请求体。
4. 协议扩展性
请求头是键值对,可以随时新增自定义头,比如 X-Request-Id、X-Client-Version,不需要改变协议结构。
请求体格式由 Content-Type 决定,可以自由切换 JSON、XML、表单、二进制,不需要改变请求行和请求头。
六、和 Spring MVC 注解的对应关系
| 请求部分 | 内容 | Spring MVC 获取方式 |
|---|---|---|
| 请求行 | 方法 | @GetMapping、@PostMapping、HttpMethod |
| 请求行 | 路径变量 | @PathVariable |
| 请求行 | 查询参数 | @RequestParam |
| 请求头 | 普通头 | @RequestHeader |
| 请求头 | Cookie | @CookieValue |
| 请求体 | JSON | @RequestBody |
| 请求体 | 表单 | @RequestParam 或对象绑定 |
| 请求体 | 文件 | MultipartFile |
| 全部 | 方法 + URL + 头 + 体 | RequestEntity<T> |
这张表能解释很多"为什么":
- 为什么
@PathVariable和@RequestParam不需要读请求体?因为它们在请求行里。 - 为什么
@RequestHeader能拿到鉴权信息?因为请求头在请求体之前。 - 为什么
@RequestBody只能读一次?因为请求体是流,读完就没了。
七、记忆方法
1. 寄快递类比
| HTTP 请求 | 快递类比 | 作用 |
|---|---|---|
| 请求行 | 快递单上的"寄到哪、什么方式" | 路由到正确的地方 |
| 请求头 | 面单上的"易碎、保价、寄件人" | 描述包裹特征和约束 |
| 请求体 | 包裹里真正的物品 | 业务数据 |
快递员先看快递单决定送哪,再看面单决定怎么处理,最后才拆包裹。服务器处理请求也是这个顺序。
2. 三句话口诀
请求行说"对谁做什么",
请求头说"有什么特征和约束",
请求体说"具体带了什么数据"。
3. 处理顺序口诀
先看行做路由,
再看头做鉴权,
最后才读体做业务。
4. 为什么分开记
行和头很小,可以先读;
体可能很大,按需再读;
分层处理,各管各的。
5. 和注解对应口诀
路径参数在行里,@PathVariable;
查询参数在行里,@RequestParam;
认证信息在头里,@RequestHeader;
业务数据在体里,@RequestBody。
八、总结
| 部分 | 核心作用 | 关键特征 | Spring MVC 对应 |
|---|---|---|---|
| 请求行 | 说明对谁做什么 | 方法 + 路径 + 协议版本 | @GetMapping、@PathVariable、@RequestParam |
| 请求头 | 说明请求的特征和约束 | 键值对,体积小,先于请求体 | @RequestHeader、@CookieValue |
| 请求体 | 携带业务数据 | 格式由 Content-Type 决定,可能很大 |
@RequestBody、MultipartFile |
三部分分开的本质是职责分离和分层处理:请求行负责路由,请求头负责描述和约束,请求体负责承载数据。这样网关、代理、鉴权、业务服务可以各读各的部分,不需要解析全部内容,既提高了效率,也让协议具备扩展性。记住"行定方向、头定条件、体装数据",再记住"先读行、再读头、最后读体"的处理顺序,HTTP 请求结构就不会混乱。