目录

网络-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 协议,如 httphttpsws
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 请求中。

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