3. HTTP 请求报文结构:从一行到一包数据
HTTP 请求报文,说白了就是客户端发给服务器的一封信。这封信有固定的格式,服务器拿到手就知道你想干嘛。我刚开始学 HTTP 的时候,总觉得报文结构很抽象,直到自己抓包看了几次,才真正理解——原来每一行都有它的道理。
一个完整的 HTTP 请求报文,由四部分组成:请求行、请求头、空行、请求体。缺一不可,顺序也不能乱。
核心要点:HTTP 请求报文 = 请求行 + 请求头 + 空行 + 请求体。空行是分隔符,告诉服务器“头部结束了,下面是正文”。
3.1 请求行:请求的第一句话
请求行只有一行,但信息量巨大。它包含三个要素:方法、URL、协议版本。格式如下:
GET /index.html HTTP/1.1
POST /api/login HTTP/2.0
PUT /user/profile HTTP/1.1
这三个部分用空格分隔,最后以回车换行(CRLF)结束。我见过不少新手在写自定义 HTTP 客户端时,忘了加空格或者用了 Tab,结果服务器直接返回 400 Bad Request。嗯,这个坑我踩过。
方法(Method)
方法就是你想对资源做什么操作。常用的有:
- GET:获取资源,最常用。不修改服务器数据。
- POST:提交数据,比如表单登录、上传文件。
- PUT:更新资源,幂等的(多次调用结果一样)。
- DELETE:删除资源。
- PATCH:部分更新资源。
- HEAD:只获取响应头,不获取响应体。常用于检查资源是否存在。
- OPTIONS:询问服务器支持哪些方法。跨域请求时会用到。
我的经验:在实际项目中,很多人把 GET 和 POST 用混了。比如用 GET 提交登录表单,密码直接暴露在 URL 里,浏览器历史记录里一查就看到了。记住:GET 用于读取,POST 用于写入,这是基本原则。
URL(统一资源定位符)
URL 就是你要访问的资源地址。在请求行里,通常只写路径部分,不写协议和域名。比如:
GET /api/users?page=1&size=20 HTTP/1.1
这里 /api/users?page=1&size=20 就是 URL 路径加查询参数。完整的 URL 应该是 https://example.com/api/users?page=1&size=20,但协议和域名会放在请求头的 Host 字段里。
为什么会这样?因为 HTTP/1.1 支持虚拟主机,一台服务器可以托管多个域名。服务器需要根据 Host 字段来判断你要访问哪个网站。
协议版本
协议版本告诉服务器你用的是哪个 HTTP 版本。常见的有:
| 版本 | 特点 | 现状 |
|---|---|---|
| HTTP/0.9 | 只有 GET 方法,没有请求头 | 已废弃 |
| HTTP/1.0 | 支持多种方法,短连接 | 基本淘汰 |
| HTTP/1.1 | 持久连接、管道化、Host 头 | 目前最常用 |
| HTTP/2.0 | 多路复用、二进制分帧、头部压缩 | 逐渐普及 |
| HTTP/3.0 | 基于 QUIC,使用 UDP | 新兴标准 |
我个人习惯在开发新项目时优先使用 HTTP/2.0,性能提升很明显。但如果是维护老系统,HTTP/1.1 也完全够用。
3.2 请求头:告诉服务器你的“底细”
请求头是键值对,每行一个,格式是 字段名: 字段值。请求头提供了请求的元数据,比如客户端信息、内容类型、缓存策略等。
常见的请求头字段:
| 字段名 | 作用 | 示例 |
|---|---|---|
| Host | 指定目标服务器的主机名和端口 | Host: www.example.com:8080 |
| User-Agent | 标识客户端软件信息 | User-Agent: Mozilla/5.0 ... |
| Accept | 告诉服务器客户端能接受的内容类型 | Accept: text/html, application/json |
| Content-Type | 请求体的媒体类型(POST/PUT 时使用) | Content-Type: application/json |
| Content-Length | 请求体的字节长度 | Content-Length: 348 |
| Authorization | 身份认证信息 | Authorization: Bearer <token> |
| Cookie | 发送之前服务器设置的 Cookie | Cookie: sessionId=abc123 |
| Referer | 当前请求的来源页面 URL | Referer: https://www.google.com/ |
注意:我曾经在项目中遇到过一个诡异的问题——用户反馈某些页面加载不出来。排查了半天,发现是 User-Agent 字段被某个中间件修改了,导致后端无法正确识别客户端类型。所以,不要随意修改请求头,除非你清楚后果。
Host 字段:虚拟主机的关键
HTTP/1.1 强制要求必须携带 Host 字段。没有它,服务器不知道你要访问哪个网站。比如一台服务器上托管了 a.com 和 b.com,请求行里只有 GET /index.html,服务器怎么区分?靠的就是 Host 头。
Content-Type:告诉服务器你发的是什么
当你有请求体时(比如 POST 请求),Content-Type 是必须的。常见的值有:
application/json:JSON 数据application/x-www-form-urlencoded:表单数据(键值对)multipart/form-data:文件上传text/plain:纯文本application/xml:XML 数据
3.3 空行:不起眼但必不可少
空行就是一行空白,只有回车换行(CRLF)。它的作用很简单:分隔请求头和请求体。
服务器解析请求时,读到空行就知道头部结束了,接下来是请求体。如果没有这个空行,服务器会把请求体的内容也当成请求头来解析,结果就是解析失败。
你想想看,如果一个人写信,正文和落款之间没有空行,是不是容易看混?HTTP 也是这个道理。
3.4 请求体:GET 与 POST 的本质区别
请求体是可选部分,不是所有请求都有。GET 请求没有请求体,POST 请求有请求体。这是两者最直观的区别,但远不止于此。
GET 请求
- 没有请求体,参数放在 URL 的查询字符串中。
- 幂等的:多次请求结果一样,不会改变服务器状态。
- 可缓存:浏览器和代理服务器可以缓存 GET 请求的结果。
- 长度限制:URL 长度有限制(不同浏览器不同,通常 2048 字符左右)。
- 安全性低:参数暴露在 URL 中,会被浏览器历史记录、服务器日志记录。
POST 请求
- 有请求体,参数放在请求体中。
- 非幂等的:多次请求可能产生不同结果(比如重复提交订单)。
- 不可缓存:默认不缓存,除非响应头明确允许。
- 无长度限制:理论上请求体可以很大(实际受服务器配置限制)。
- 安全性高:参数不在 URL 中,不会出现在浏览器历史记录里。
| 对比维度 | GET | POST |
|---|---|---|
| 请求体 | 无 | 有 |
| 参数位置 | URL 查询字符串 | 请求体 |
| 幂等性 | 是 | 否 |
| 可缓存 | 是 | 否(默认) |
| 长度限制 | 有(URL 长度限制) | 无(理论上) |
| 安全性 | 低(参数暴露) | 高(参数在体内) |
| 典型用途 | 获取数据、搜索、分页 | 提交表单、上传文件、创建资源 |
重要原则:GET 用于读取,POST 用于写入。这是 RESTful API 设计的基本规范。违反这个原则,不仅会让 API 难以理解,还可能带来安全问题。
3.5 完整请求报文示例
下面是一个完整的 POST 请求报文,我把它拆开来看:
POST /api/login HTTP/1.1
Host: example.com
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36
Accept: application/json, text/plain, */*
Content-Type: application/json
Content-Length: 45
{"username":"admin","password":"123456"}
注意看:
- 第一行是请求行:方法 POST,路径 /api/login,版本 HTTP/1.1
- 第 2-6 行是请求头:Host、User-Agent、Accept、Content-Type、Content-Length
- 第 7 行是空行(这里看不到,但实际存在)
- 第 8 行是请求体:JSON 格式的登录信息
如果是 GET 请求,报文会更短:
GET /api/users?page=1 HTTP/1.1
Host: example.com
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36
Accept: application/json
没有请求体,也没有 Content-Type 和 Content-Length。因为 GET 不需要这些。
3.6 知识体系总览
下面这张图把 HTTP 请求报文的结构和核心概念串起来了。你可以把它当作一个快速参考:
3.7 避坑指南
最后,分享几个我在项目中踩过的坑:
- 忘记空行:写自定义 HTTP 客户端时,忘了在请求头后面加空行,结果服务器一直等待请求体,超时返回 408。排查了半小时才发现少了一个
\r\n。 - Content-Length 不匹配:请求体实际长度和 Content-Length 不一致,服务器要么截断数据,要么一直等待。我建议用框架自动计算,不要手动写。
- GET 请求带请求体:虽然 HTTP 规范没有明确禁止,但很多服务器和代理会忽略 GET 的请求体。所以,别这么干。
- Host 头缺失:HTTP/1.1 强制要求 Host 头,没有的话服务器可能返回 400。我见过有人用 HTTP/1.0 的客户端访问 HTTP/1.1 的服务器,结果请求失败。
我的建议:如果你刚开始学习 HTTP,最好的方法是用抓包工具(比如 Wireshark 或 Chrome 开发者工具)看真实的请求报文。看一次,比读十遍书都管用。