目录

网络-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 的区别

这是最高频的面试题,也是错误说法最多的一题。先说流传很广但不准确的几种:

常见说法 实际情况
“GET 有长度限制,POST 没有” HTTP 协议对两者都没有长度规定。 GET 的限制来自浏览器和服务器实现(通常 2KB~8KB,Nginx 由 large_client_header_buffers 控制)。POST 的限制也来自服务端配置(client_max_body_size)。
“POST 比 GET 安全” 两者都是明文传输,同样不安全。 POST 的参数在 body 里,只是不出现在 URL 和浏览器历史里,抓包一样看得见。真正的安全靠 HTTPS。
“GET 只能传 ASCII” 非 ASCII 字符需要 URL 编码(percent-encoding),但不代表传不了。
“POST 会发两个 TCP 包” 这是把 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 报文如何确定消息体的长度?

两种方式,不能同时使用:

  1. Content-Length: N —— 明确字节数,生成前已知大小时用;
  2. 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 请求中。

这也是早期单页应用用 # 做前端路由的原因——改变它不触发页面请求。