网络-12 HTTP 报文、方法与状态码
前置阅读:网络-07 TCP 连接管理 本文讲 HTTP 协议本身的构成,版本演进(0.9→1.1→2)见 网络-15。
HTTP(HyperText Transfer Protocol,超文本传输协议)是一个应用层、无状态、基于请求-响应的协议。它本身不关心底层用什么传输——HTTP/1.1 和 HTTP/2 跑在 TCP 上,HTTP/3 跑在 QUIC 上,但报文的语义始终一致。
1. URL 的构成
https://user:pass@www.example.com:443/path/to/page?id=1&sort=asc#section2
└─┬─┘ └───┬───┘ └──────┬───────┘ └┬┘└─────┬────┘ └──────┬─────┘ └───┬──┘
scheme userinfo host port path query fragment
| 部分 | 说明 |
|---|---|
| scheme | 协议,如 http、https、ws |
| host | 域名或 IP |
| port | 端口,http 默认 80、https 默认 443,默认端口通常省略 |
| path | 资源路径 |
| query | 查询参数,? 开始,& 分隔 |
| fragment | 锚点,只在浏览器本地生效,不会发送给服务器 |
最后一点值得注意:#section2 这部分永远不会出现在 HTTP 请求里,它由浏览器自己处理(定位页面位置)。这也是早期单页应用用 # 做路由的原因——改变它不会触发页面请求。
2. 报文结构
HTTP 报文是纯文本的(HTTP/2 起改为二进制,但语义不变),由四部分组成:
起始行
头部字段
空行 ← 关键:这个空行标记头部结束
消息体(可选)
请求报文:
POST /api/login HTTP/1.1 ← 请求行:方法 + 路径 + 版本
Host: www.example.com ← 头部开始
Content-Type: application/json
Content-Length: 43
User-Agent: curl/8.4.0
← 空行
{"username":"admin","password":"123"} ← 消息体
响应报文:
HTTP/1.1 200 OK ← 状态行:版本 + 状态码 + 原因短语
Content-Type: application/json
Content-Length: 27
Date: Wed, 29 Jul 2026 05:00:00 GMT
← 空行
{"code":0,"msg":"success"} ← 消息体
那个空行至关重要:HTTP 头部长度不固定,接收方靠 \r\n\r\n(连续两个换行)判断"头部到这里结束了"。这也解释了 网络-08 里提到的消息边界问题——HTTP 就是用"分隔符 + Content-Length"这套组合,在面向字节流的 TCP 上划出消息边界的。
消息体长度怎么确定? 两种方式:
Content-Length: 43—— 明确告知字节数,接收方读够就停;Transfer-Encoding: chunked—— 分块传输,长度未知时用(如动态生成的内容、流式输出)。每块前面标注自己的长度,以一个长度为0的块表示结束:
HTTP/1.1 200 OK
Transfer-Encoding: chunked
7\r\n
Mozilla\r\n
9\r\n
Developer\r\n
0\r\n ← 长度 0,传输结束
\r\n
两者不能同时使用。服务端在生成内容前就知道大小时用 Content-Length,否则用 chunked——大文件流式下载、SSE 流式输出都属于后者。
3. 请求方法
HTTP 定义了若干方法,核心区别在三个属性:安全性、幂等性、可缓存性。
| 方法 | 安全 | 幂等 | 可缓存 | 用途 |
|---|---|---|---|---|
| GET | ✓ | ✓ | ✓ | 获取资源 |
| HEAD | ✓ | ✓ | ✓ | 只要响应头,不要 body |
| OPTIONS | ✓ | ✓ | ✗ | 查询服务器支持的方法/CORS 预检 |
| POST | ✗ | ✗ | ✗ | 提交数据、创建资源 |
| PUT | ✗ | ✓ | ✗ | 完整替换资源 |
| PATCH | ✗ | ✗ | ✗ | 局部更新资源 |
| DELETE | ✗ | ✓ | ✗ | 删除资源 |
三个概念必须分清,这是面试必考:
- 安全(Safe):不会修改服务器状态,纯读取。注意"安全"在这里和加密、防攻击毫无关系。
- 幂等(Idempotent):执行一次和执行 N 次的效果相同。
DELETE /user/1删一次和删十次,最终状态都是"用户 1 不存在",所以幂等;POST /user调用十次会创建十个用户,所以不幂等。 - 可缓存:响应能否被缓存复用。
为什么 PATCH 不幂等? 因为它的语义是"局部更新",而更新操作可能依赖当前值。比如 PATCH {"op":"increment","field":"count"} 每次执行都会让 count 加一。如果 PATCH 的内容是"把 name 设为 xxx"这种绝对赋值,那实际上是幂等的——但协议不保证,所以规范上归为不幂等。
幂等性的工程意义:它直接决定能不能安全重试。网络超时后,客户端不知道请求到底有没有到达服务端。GET/PUT/DELETE 可以放心重发,POST 不行——这就是为什么支付、下单这类接口需要幂等键(Idempotency-Key):客户端生成一个唯一 ID,服务端据此去重,把不幂等的 POST 人为变成幂等的。
这也和 网络-17 的 0-RTT 重放风险呼应:TLS 1.3 的 early data 只允许用于幂等请求,原因完全一样。
4. GET 和 POST 的区别
这是最高频的面试题,也是错误说法最多的一题。先说流传很广但不准确的几种:
| 常见说法 | 实际情况 |
|---|---|
HTTP 协议对两者都没有长度规定。 GET 的限制来自浏览器和服务器实现(通常 2KB~8KB,Nginx 由 large_client_header_buffers 控制)。POST 的限制也来自服务端配置(client_max_body_size)。 |
|
| 两者都是明文传输,同样不安全。 POST 的参数在 body 里,只是不出现在 URL 和浏览器历史里,抓包一样看得见。真正的安全靠 HTTPS。 | |
| 非 ASCII 字符需要 URL 编码(percent-encoding),但不代表传不了。 | |
这是把 Expect: 100-continue 机制当成了通例。绝大多数实现(包括浏览器和 curl 默认行为)不会这样。 |
真正的核心区别是语义,以及由语义派生出的行为差异:
| GET | POST | |
|---|---|---|
| 语义 | 获取资源(安全 + 幂等) | 提交数据(不安全 + 不幂等) |
| 参数位置 | URL query | 消息体 |
| 可缓存 | ✓ 浏览器/CDN 会缓存 | ✗ 默认不缓存 |
| 可收藏为书签 | ✓ | ✗ |
| 留在浏览器历史 | ✓(含参数) | ✗ |
| 刷新页面 | 直接重发 | 弹窗提示"确认重新提交" |
| 前进/后退 | 无副作用 | 可能重复提交 |
| 能否重试 | ✓ 安全 | ✗ 可能产生重复数据 |
所以该用哪个的判断标准是语义,不是数据量或安全性:只要这个请求会改变服务器状态,就该用 POST(或 PUT/PATCH/DELETE),哪怕参数只有一个字节;只要是纯读取,就该用 GET,哪怕参数很长(真的超限了,可以改用 POST 传查询条件,这是工程妥协而非语义正确)。
一个经典的反面案例:把删除操作写成 GET /deleteUser?id=1。后果是——搜索引擎爬虫爬一遍你的页面,用户数据就被删光了。因为爬虫认为 GET 是安全的,可以随便访问。
5. 状态码
状态码由三位数字组成,首位决定类别:
| 类别 | 含义 |
|---|---|
| 1xx | 信息性——请求已收到,继续处理 |
| 2xx | 成功 |
| 3xx | 重定向——需要进一步操作 |
| 4xx | 客户端错误 |
| 5xx | 服务端错误 |
记住 4xx 和 5xx 的分界(谁的锅)在排障时最实用。
5.1 常考的具体状态码
2xx 成功
| 码 | 含义 | 说明 |
|---|---|---|
| 200 | OK | 最常见 |
| 201 | Created | 资源创建成功,RESTful 里 POST/PUT 的规范响应 |
| 204 | No Content | 成功但无返回内容,DELETE 的常见响应 |
| 206 | Partial Content | 分块/断点续传,配合 Range 请求头 |
3xx 重定向 —— 这组最容易混
| 码 | 含义 | 是否保留请求方法 | 用途 |
|---|---|---|---|
| 301 | 永久重定向 | ✗(历史实现会把 POST 变 GET) | 域名更换、URL 永久调整,会被缓存,SEO 权重转移 |
| 302 | 临时重定向 | ✗(同上) | 临时跳转,不缓存 |
| 303 | See Other | ✗(强制改为 GET) | POST 提交后跳转到结果页(避免刷新重复提交) |
| 304 | Not Modified | — | 协商缓存命中,见 网络-13 |
| 307 | 临时重定向 | ✓ 严格保留 | 302 的规范化版本 |
| 308 | 永久重定向 | ✓ 严格保留 | 301 的规范化版本 |
301/302 与 307/308 的区别是重点:历史上很多浏览器在处理 301/302 时,会把原本的 POST 请求改成 GET 并丢弃 body(虽然违反规范,但已成事实标准)。这在重定向一个表单提交时会导致数据丢失。307/308 就是为了修正这个歧义而定义的——它们严格要求保留原方法和 body。
304 严格来说不是"重定向",它属于缓存机制,只是被归在 3xx 里。它的响应没有 body,告诉浏览器"你缓存的版本还能用"。
4xx 客户端错误
| 码 | 含义 | 关键区别 |
|---|---|---|
| 400 | Bad Request | 请求格式错误(参数缺失、JSON 解析失败) |
| 401 | Unauthorized | 未认证——你是谁我不知道,请先登录 |
| 403 | Forbidden | 已认证但无权限——我知道你是谁,但你不能访问 |
| 404 | Not Found | 资源不存在 |
| 405 | Method Not Allowed | 路径存在但方法不对(如对只读接口发 POST) |
| 409 | Conflict | 状态冲突,如并发修改、资源已存在 |
| 413 | Payload Too Large | 请求体过大(Nginx 的 client_max_body_size 超限) |
| 415 | Unsupported Media Type | Content-Type 不支持 |
| 429 | Too Many Requests | 被限流,通常配合 Retry-After 响应头 |
401 vs 403 是高频考点:401 是"没带身份"或"身份无效"(该返回 WWW-Authenticate 头引导认证),403 是"身份有效但权限不够"。搞反了会让客户端做出错误的重试决策——遇到 401 应该去刷新 token 重试,遇到 403 重试多少次都没用。
5xx 服务端错误
| 码 | 含义 | 典型原因 |
|---|---|---|
| 500 | Internal Server Error | 代码抛异常、未捕获的 panic |
| 501 | Not Implemented | 服务端不支持该方法 |
| 502 | Bad Gateway | 网关拿到了上游的无效响应——后端进程挂了、崩了、返回了garbage |
| 503 | Service Unavailable | 服务暂时不可用——过载、正在维护,通常配 Retry-After |
| 504 | Gateway Timeout | 网关等上游超时——后端还活着但太慢 |
502 / 503 / 504 的区分是运维排障基本功:
- 502 → 后端没有正常应答(进程崩溃、端口没监听、返回了非法响应)。查后端进程是否存活、Nginx 的
upstream配置是否指对。 - 504 → 后端活着但没在超时时间内答完。查慢查询、慢接口,或调
proxy_read_timeout。 - 503 → 通常是服务自己主动拒绝(限流、熔断、健康检查失败、优雅关闭中)。
一个记忆方式:502 是"上游给了我垃圾",504 是"上游让我等太久",503 是"我自己现在不干活"。
6. 常见头部字段
头部字段按作用可分四类:
通用头(请求响应都可用)
| 字段 | 说明 |
|---|---|
Connection |
keep-alive / close,控制长连接 |
Date |
报文产生时间 |
Cache-Control |
缓存策略,见 网络-13 |
请求头
| 字段 | 说明 |
|---|---|
Host |
HTTP/1.1 强制要求,虚拟主机靠它区分同一 IP 上的多个站点 |
User-Agent |
客户端标识 |
Accept |
能接受的 MIME 类型 |
Accept-Encoding |
能接受的压缩方式,如 gzip, br |
Accept-Language |
语言偏好 |
Referer |
从哪个页面跳来的(注意这个词拼写少了一个 r,是历史遗留的拼写错误) |
Range |
请求部分内容,断点续传用 |
Authorization |
认证凭证,如 Bearer <token> |
Cookie |
见 网络-14 |
Origin |
跨域请求的来源,CORS 用 |
响应头
| 字段 | 说明 |
|---|---|
Content-Type |
内容类型 + 字符集,如 text/html; charset=utf-8 |
Content-Length |
内容字节数 |
Content-Encoding |
实际使用的压缩方式 |
Location |
3xx 重定向的目标地址 |
Set-Cookie |
下发 Cookie |
Server |
服务端软件标识(生产环境建议隐藏,减少信息泄露) |
Access-Control-Allow-Origin |
CORS 放行来源 |
Retry-After |
配合 429/503,告知多久后重试 |
Host 头为什么是 HTTP/1.1 的重大改进? 因为 TCP 连接只知道 IP 和端口,一台服务器上如果用同一个 IP 托管多个域名,服务端无法判断请求要访问哪个站点。有了 Host 头,虚拟主机(Virtual Host)才成为可能——这是共享主机和现代反向代理的基础。HTTP/1.0 没有它,所以一个 IP 只能放一个站点。
7. 用 curl 观察
# -v 看完整的请求和响应头(> 是发出的,< 是收到的)
curl -v https://example.com
# -I 只发 HEAD 请求,只看响应头
curl -I https://example.com
# 看重定向链路:-L 跟随跳转,观察每一跳的状态码
curl -sIL http://github.com | grep -E "^HTTP|^location"
# 自定义方法和头部
curl -X PATCH -H "Content-Type: application/json" \
-d '{"name":"new"}' https://api.example.com/user/1
# 只输出状态码,适合写健康检查脚本
curl -s -o /dev/null -w "%{http_code}\n" https://example.com
# 看各阶段耗时,排查慢请求到底慢在哪
curl -s -o /dev/null -w "DNS:%{time_namelookup}s 连接:%{time_connect}s TLS:%{time_appconnect}s 首字节:%{time_starttransfer}s 总计:%{time_total}s\n" https://example.com
最后那条在排查性能问题时特别有用——它能直接告诉你时间花在 DNS、TCP 握手、TLS 握手还是服务端处理上。
8. 本章面试题
GET 和 POST 的区别?(注意别答错)
核心区别是语义:GET 是安全 + 幂等的读取操作,POST 是不安全 + 不幂等的提交操作。
由此派生的行为差异:GET 可缓存、可收藏、留历史记录、刷新直接重发、可安全重试;POST 不可缓存、刷新会提示重新提交、不能随意重试。
要避开的错误说法:协议本身没有规定 GET 的长度限制(是浏览器/服务器实现限制);POST 并不比 GET 安全(都是明文,安全靠 HTTPS)。
详见上文「GET 和 POST 的区别」。
什么是幂等?哪些方法是幂等的?为什么重要?
幂等 = 执行一次和执行 N 次对服务器状态的影响相同。
幂等:GET、HEAD、OPTIONS、PUT、DELETE。不幂等:POST、PATCH。
重要性在于决定能否安全重试。超时后客户端不知道请求是否已到达,幂等方法可以放心重发。POST 需要靠**幂等键(Idempotency-Key)**由服务端去重,才能安全重试——支付、下单接口必须这么做。
301 和 302 的区别?307、308 又是什么?
301 永久、302 临时。301 会被浏览器缓存并转移 SEO 权重,302 不缓存。
关键补充:历史上浏览器处理 301/302 时会把 POST 改成 GET 并丢弃 body(违反规范但成了事实标准)。307/308 是修正版本,严格保留原方法和 body —— 307 对应 302,308 对应 301。
重定向表单提交时必须用 307/308,否则数据会丢。
401 和 403 的区别?
- 401 Unauthorized:未认证——没带凭证或凭证无效。语义是"你是谁我不知道,请先登录",应返回
WWW-Authenticate头。客户端应刷新 token 后重试。 - 403 Forbidden:已认证但无权限——“我知道你是谁,但你不够格”。客户端重试多少次都没用。
搞反会导致客户端做出错误的重试决策。
502、503、504 有什么区别?线上遇到怎么排查?
- 502 Bad Gateway:网关从上游拿到无效响应——后端进程崩了、端口没监听。查后端存活状态和 upstream 配置。
- 504 Gateway Timeout:网关等上游超时——后端活着但太慢。查慢查询/慢接口,或调
proxy_read_timeout。 - 503 Service Unavailable:服务主动拒绝——限流、熔断、健康检查失败、优雅关闭中。通常带
Retry-After。
记忆:502「上游给了垃圾」、504「上游太慢」、503「我自己不干活」。
HTTP 报文如何确定消息体的长度?
两种方式,不能同时使用:
Content-Length: N—— 明确字节数,生成前已知大小时用;Transfer-Encoding: chunked—— 分块传输,每块自带长度,以长度 0 的块结束。用于流式/动态内容(大文件下载、SSE)。
头部与消息体之间靠**空行(\r\n\r\n)**分隔——这就是 HTTP 在 TCP 字节流上划分消息边界的方式(见 网络-08)。
为什么 HTTP/1.1 强制要求 Host 头?
因为 TCP 连接只携带 IP 和端口信息。一台服务器用同一 IP 托管多个域名时,服务端无法判断请求的目标站点。
Host 头让**虚拟主机(Virtual Host)**成为可能,是共享主机和现代反向代理的基础。HTTP/1.0 没有它,一个 IP 只能服务一个站点。
URL 里的 # 后面的内容会发给服务器吗?
不会。 fragment(锚点)只在浏览器本地生效,用于定位页面位置,永远不会出现在 HTTP 请求中。
这也是早期单页应用用 # 做前端路由的原因——改变它不触发页面请求。
xingliuhua