【计算基础|网络02】HTTP 原理:报文结构、方法语义与状态码
上一篇把"一次请求的全景"走完了,停在了应用层。这一篇就钻进应用层,把 HTTP 这封信本身彻底拆开。
很多人天天调接口,却说不清一个 HTTP 请求到底由几块组成;状态码背了一肚子,真遇到 422、429、504 还是发懵。根子在于:HTTP 不是什么黑盒协议,它就是一段按固定格式组织的文本。你把这封信的信封(请求行/响应行)、信纸抬头(头部)、分页线(空行)、正文(实体)看明白,方法语义和状态码就不是死记硬背,而是顺理成章。
环境:macOS 自带 curl 8.7.1,全程用
curl -v抓真实报文;HTTP 语义以现行 RFC 9110(HTTP 语义)为准,HTTP/1.1 报文格式以 RFC 9112 为准。
一、HTTP 报文就四部分:信封、抬头、分页线、正文
结论先给:无论请求还是响应,报文都 = 起始行 + 头部区 + 空行 + 实体(可选)。请求和响应的差别只在"起始行":请求是"请求行",响应是"状态行"。
1.1 请求报文 vs 响应报文对照
http
# ===== 请求报文 =====
GET /get?name=lonez HTTP/1.1 # ① 请求行:方法 + 路径(含查询串) + 版本
Host: httpbin.org # ② 头部:key: value,每行一个
User-Agent: curl/8.7.1
Accept: */*
# ③ 空行:CRLF,分隔头部与实体,绝不能少
{"name":"lonez"} # ④ 实体(请求体):GET 一般没有,POST/PUT 才有
http
# ===== 响应报文 =====
HTTP/1.1 200 OK # ① 状态行:版本 + 状态码 + 原因短语
Date: Tue, 22 Sep 2026 03:46:24 GMT # ② 头部
Content-Type: application/json
Content-Length: 254
Connection: keep-alive
Server: gunicorn/19.9.0
# ③ 空行
{"args":{}, "headers":{...}} # ④ 实体(响应体):真正的页面/数据
对照着看就清楚了:
| 部分 | 请求报文里叫 | 响应报文里叫 | 内容 |
|---|---|---|---|
| ① 起始行 | 请求行 | 状态行 | 请求:方法 路径 版本;响应:版本 状态码 原因短语 |
| ② 头部区 | 请求头 | 响应头 | 一堆 key: value,描述元信息 |
| ③ 空行 | 空行 | 空行 | 一个 CRLF,标记头部结束 |
| ④ 实体 | 请求体 | 响应体 | 真正要传的数据,GET 通常无请求体 |
🔴 重点 :那个空行是协议的硬规定。头部和实体之间必须有一个单独的 CRLF,服务器靠它判断"头读完了,下面是体"。手搓报文时漏掉空行,服务端会一直等不到头结束、直到超时------这是 socket 手撸 HTTP 最经典的坑。
1.2 用 curl -v 抓一份真实可读的报文
光看结构不够,我们直接抓一份真实的。注意:现在很多站点默认协商成 HTTP/2,报文是二进制帧(05 篇讲),看不清。所以强制 --http1.1,把可读文本报文逼出来:
bash
curl -sv --http1.1 https://httpbin.org/get 2>&1 | grep -E "^[*>]"
> 开头是你发出去的请求,< 开头是收到的响应,真实输出如下:
text
* ALPN: server accepted http/1.1
* using HTTP/1.x
> GET /get HTTP/1.1
> Host: httpbin.org
> User-Agent: curl/8.7.1
> Accept: */*
>
* Request completely sent off
< HTTP/1.1 200 OK
< Date: Tue, 22 Sep 2026 03:46:24 GMT
< Content-Type: application/json
< Content-Length: 254
< Connection: keep-alive
< Server: gunicorn/19.9.0
<
对照 1.1 的四部分:> GET /get HTTP/1.1 是请求行,> Host/User-Agent/Accept 是请求头,> 后面那个空行就是③,< 后面的就是响应。这就是 HTTP 的全部秘密------你写的框架、requests、浏览器,干的事就是按这个格式把文本拼好塞进 TCP。
二、请求方法语义:谁安全、谁幂等
上一篇(03 实战篇)已经用过方法,这里把语义讲透,重点是两个面试高频词:安全(Safe) 和 幂等(Idempotent)。
- 安全:方法执行不会改变服务器资源状态,纯读。GET/HEAD/OPTIONS 安全。
- 幂等:同一请求执行一次和执行 N 次,对服务器资源的影响相同。GET/PUT/DELETE 幂等,POST 不幂等。
2.1 方法语义对照表(RFC 9110)
| 方法 | 语义 | 安全 | 幂等 | 有请求体 | 典型场景 |
|---|---|---|---|---|---|
| GET | 获取资源 | ✅ | ✅ | 否 | 列表、详情、搜索 |
| HEAD | 同 GET,但只要响应头不要体 | ✅ | ✅ | 否 | 探活、看文件大小/更新时间 |
| POST | 新建/提交/触发一个动作 | ❌ | ❌ | 是 | 新建资源、登录、下单 |
| PUT | 用载荷完整替换目标资源 | ❌ | ✅ | 是 | 全量更新(不存在则创建) |
| PATCH | 对资源做部分修改 | ❌ | 通常设计成幂等 | 是 | 只改某几个字段 |
| DELETE | 删除资源 | ❌ | ✅ | 一般无 | 删除 |
| OPTIONS | 查询目标支持哪些方法/选项 | ✅ | ✅ | 否 | CORS 预检(04 篇讲) |
🔴 为什么 GET/PUT/DELETE 幂等、POST 不幂等:
GET /users/100:读一次和读一百次,资源都没变。DELETE /users/100:删第一次用户没了,删第一百次还是"没了"这个状态,资源状态没变(哪怕第二次返回 404)。POST /orders:发一次建一个订单,发十次建十个,资源被改变十次。
这就是为什么下单接口网络抖动时不能让用户无脑重试 ------POST 不幂等,重试会重复扣款;而 GET、DELETE 重发无所谓。线上要给 POST 做"幂等键"(Idempotency-Key),本质就是把不幂等的 POST 人为设计成幂等。
⚠️ PUT vs PATCH 的坑 :PUT 是"用我给你的这份完整表示替换旧的",你得把所有字段带上,没带的字段可能被置空;PATCH 只带要改的字段。很多人全用 POST 图省事,接口可读性就没了。
三、状态码速查:三位数字的第一位是分类
结论先给:状态码三位数字,第一位定大类,后两位是具体原因。 记住五大类,剩下的就是对号入座。
| 大类 | 含义 | 你该怎么理解 |
|---|---|---|
| 1xx | 信息性(临时响应) | 握手过程中的中间状态,业务基本不碰(101 切换协议、103 早期提示) |
| 2xx | 成功 | 请求被正常接收、理解、处理 |
| 3xx | 重定向 | 要进一步操作才能完成,多半是跳转或用缓存 |
| 4xx | 客户端错误 | 你的请求有问题,服务器不处理 |
| 5xx | 服务器错误 | 服务器自己出问题了,不是你的请求格式错 |
🔴 最实用的判断:4xx 怪客户端(前端/调用方改),5xx 怪服务端(后端/运维改)。看到 4xx 别急着找后端,先看自己请求发对没。
3.1 高频状态码逐个说
| 状态码 | 名称 | 什么时候出现 | 怎么处理 |
|---|---|---|---|
| 200 | OK | 请求成功 | 正常 |
| 201 | Created | POST 新建资源成功 | 看 Location 头拿新资源地址 |
| 204 | No Content | 成功但没有响应体(DELETE 常用) | 别去解析 body |
| 301 | Moved Permanently | 永久重定向(http→https、域名迁移) | 浏览器会缓存,自动跳 |
| 302 | Found | 临时重定向(登录后跳回原页) | 临时,不缓存 |
| 304 | Not Modified | 资源没变化,用本地缓存 | 配合 If-Modified-Since/ETag,省流量 |
| 400 | Bad Request | 请求本身格式错(JSON 语法错、缺参) | 检查报文格式 |
| 401 | Unauthorized | 没登录/凭证无效 | 先去拿 token |
| 403 | Forbidden | 已认证但没权限 | 权限问题,别再重试 |
| 404 | Not Found | 路径/资源不存在 | 查 URL 或资源是否被删 |
| 405 | Method Not Allowed | 路径对但方法不对(该用 GET 用了 POST) | 换方法 |
| 415 | Unsupported Media Type | Content-Type 服务器不认 |
核对请求体格式 |
| 422 | Unprocessable Entity | 格式对但语义校验不过(字段类型错、缺必填) | 最常见于表单/JSON 校验失败 |
| 429 | Too Many Requests | 触发限流了 | 退避重试,别狂刷 |
| 500 | Internal Server Error | 服务器内部炸了(代码异常) | 找后端看日志 |
| 502 | Bad Gateway | 网关/代理从上游拿到无效响应 | 后端进程挂了或没起,找运维 |
| 503 | Service Unavailable | 服务暂时不可用(过载、维护) | 稍后重试 |
| 504 | Gateway Timeout | 网关等上游响应超时 | 后端处理太慢或挂了 |
⚠️ 几个最容易混的:
- 401 vs 403:401 是"你是谁我都不知道"(没带/带错凭证);403 是"我知道你是谁,但你就是没权限"。
- 301 vs 302:301 永久,浏览器会记住、下次直接跳;302 临时。SEO 和接口迁移要分清,别把临时跳转配成 301。
- 502 vs 504:502 是网关连上游时拿到了"坏"响应(上游进程崩了);504 是网关等上游等到超时(上游还在跑但太慢)。
3.2 实测几个真实状态码
用 httpbin 直接构造各种状态码,眼见为实:
bash
# -o /dev/null 丢弃 body,-w 只打印状态码
curl -s -o /dev/null -w "200 -> %{http_code}\n" https://httpbin.org/get
curl -s -o /dev/null -w "404 -> %{http_code}\n" https://httpbin.org/status/404
curl -s -o /dev/null -w "401 -> %{http_code}\n" https://httpbin.org/status/401
# http 访问 github 会被 301 到 https
curl -s -o /dev/null -w "301 -> %{http_code} 跳转到 %{redirect_url}\n" http://github.com
真实输出:
text
200 -> 200
404 -> 404
401 -> 401
301 -> 301 跳转到 https://github.com/
注意最后一行:http://github.com 返回 301 ,redirect_url 是 https://github.com/------这就是"HTTP 永久重定向到 HTTPS"的真实样子。
四、HTTP/1.0 vs HTTP/1.1:差在哪
结论先给:HTTP/1.1 相比 1.0 最大的进步是"省连接"和"更灵活"------长连接、Host 头、分块传输、范围请求。 现在说的"HTTP/1.1"就是 1997 年定稿、至今仍是基础版本的那套。
| 特性 | HTTP/1.0 | HTTP/1.1 | 解决了什么问题 |
|---|---|---|---|
| 连接 | 默认短连接,每个请求建一次 TCP | 默认 Keep-Alive 长连接,一个连接跑多个请求 | 省掉反复三次握手的开销 |
| Host 头 | 无(一个 IP 一个站点) | 必须带 Host | 一台服务器(一个 IP)靠 Host 头托管多个域名(虚拟主机) |
| 响应体长度 | 靠连接关闭判断结束 | Chunked 分块传输 (Transfer-Encoding: chunked) |
边生成边发,不用先算好总长度 |
| 范围请求 | 无 | Range 请求 (Range: bytes=0-1023) |
断点续传、视频拖拽、只取文件一段 |
| 管线化 | 无 | 支持 Pipelining(发完一个不等响应就发下一个) | 进一步省往返(但因队头阻塞基本被弃用,05 篇讲) |
| 方法/状态码 | 少 | 更多方法(PUT/PATCH/OPTIONS)、更多状态码 | 语义更丰富 |
🔴 重点理解 Host 头 :没有 Host 头,一个 IP 只能对应一个网站;有了 Host 头,Nginx 在同一个 IP:80 上靠 Host: a.com 和 Host: b.com 区分两个站点。这也是为什么你手动 curl 一个 IP 访问虚拟主机会 404 或拿到默认站------因为没带对 Host。
⚠️ 管线化(Pipelining)为什么没普及 :它要求响应必须按请求顺序返回(队头阻塞),第一个响应慢,后面全堵着。实际实现问题太多,浏览器基本默认关掉,直到 HTTP/2 才真正解决。这正是 05 篇的主线。
4.1 常用请求头/响应头速查
| 头部 | 方向 | 作用 |
|---|---|---|
| Host | 请求 | 目标主机名(+端口),虚拟主机靠它 |
| User-Agent | 请求 | 客户端身份(浏览器/脚本/SDK) |
| Accept | 请求 | 我能接收什么类型的响应(application/json、*/*) |
| Content-Type | 请求/响应 | 实体是什么格式(json/form/multipart) |
| Content-Length | 请求/响应 | 实体字节长度 |
| Connection | 请求/响应 | keep-alive / close,控制连接是否复用 |
| Accept-Encoding | 请求 | 我支持的压缩(gzip/br) |
| Content-Encoding | 响应 | 响应实体实际用的压缩算法 |
| Authorization | 请求 | 鉴权凭证(Bearer/ Basic) |
| Cookie | 请求 | 携带会话 Cookie |
| Server | 响应 | 服务器软件(gunicorn/nginx) |
| Location | 响应 | 重定向去哪(3xx 必带) |
| Set-Cookie | 响应 | 让客户端种下 Cookie |
| Transfer-Encoding | 响应 | chunked,分块传输 |
| Range / Content-Range | 请求/响应 | 范围请求、断点续传 |
| ETag / If-None-Match | 响应/请求 | 缓存校验(配合 304) |
🔴 分清两对 :Accept(我要什么)vs Content-Type(这是什么);Accept-Encoding(我支持什么压缩)vs Content-Encoding(实际用了什么压缩)。方向别搞反。
五、把这些串起来:一次完整报文长什么样
把前面所有点拼起来,一个带 JSON body 的 POST 真实请求/响应报文骨架:
http
POST /users HTTP/1.1 # 请求行
Host: api.example.com # 头部开始
Content-Type: application/json
Content-Length: 38
Authorization: Bearer eyJhbGci...
# 空行
{"name":"lonez","age":25} # 请求体
http
HTTP/1.1 201 Created # 状态行
Content-Type: application/json
Content-Length: 42
Location: /users/1001
Connection: keep-alive
# 空行
{"id":1001,"name":"lonez"} # 响应体
方法语义(POST 新建→201)、状态码(201 Created)、头部(Location 指向新资源)、四部分结构,全对上了。
总结:你真正需要记住的 8 件事
- 报文 = 起始行 + 头部 + 空行 + 实体;请求的起始行叫请求行,响应的叫状态行。
- 空行是硬规定,漏了服务端会一直等头结束直到超时。
- 安全 (只读)和幂等(执行一次和 N 次效果相同)是两个独立维度;GET/PUT/DELETE 幂等,POST 不幂等。
- POST 不幂等,所以下单不能无脑重试,要靠幂等键。
- 状态码第一位定大类:2xx 成功、3xx 重定向、4xx 客户端错、5xx 服务端错。
- 高频码要能反应:401 没登录、403 没权限、422 语义校验失败、429 限流、502 上游挂、504 上游超时。
- HTTP/1.1 相对 1.0:默认 Keep-Alive 长连接、必须带 Host 头、Chunked 分块、Range 范围请求。
- 看真实报文用
curl -v --http1.1(强制文本报文),>是请求、<是响应。
验证清单
- 能默写出请求/响应报文的四部分,并说清空行的作用
- 能用
curl -v --http1.1抓一份真实报文,指出每一行属于哪一部分 - 能说清安全/幂等的区别,以及为什么 POST 不幂等、DELETE 幂等
- 能区分 401/403、301/302、502/504
- 能说出 HTTP/1.1 相对 1.0 的四个关键改进
- 理解为什么必须带 Host 头,以及不带会怎样
- 能说出至少 6 个常用请求头和它们的方向
- 用
curl -o /dev/null -w "%{http_code}"实测过 200/301/404/401
参考资源
- MDN:HTTP 概述 https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Overview
- MDN:HTTP 状态码 https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Status
- MDN:HTTP 请求方法 https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Methods
- HTTP 语义(RFC 9110,现行):https://www.rfc-editor.org/rfc/rfc9110
- HTTP/1.1 报文格式(RFC 9112,现行):https://www.rfc-editor.org/rfc/rfc9112
- curl 手册:https://curl.se/docs/manpage.html
下一篇《网络05:HTTP/1.1 优化、HTTP/2 与 HTTP/3》:接着这篇讲"文本报文太啰嗦、长连接队头阻塞"是怎么一步步逼出 HTTP/2 的二进制分帧、多路复用、HPACK,又怎么因为 TCP 层队头阻塞最终走向 HTTP/3 与基于 UDP 的 QUIC,并实测用 curl --http2 验证真实协商结果。