http
09. HTTP/1.1、HTTP/2、HTTP/3
0. 本章先解决什么问题
TCP 提供的是字节流。它不知道这些字节是在请求一个页面、提交一个表单、下载一个文件,还是调用某个接口。
HTTP 在应用层定义了:
请求怎么表达
响应怎么表达
资源如何定位
方法和状态码如何表示语义
头部如何传递控制信息
缓存、压缩、鉴权、内容类型如何协商
本章要解决:
- HTTP 请求和响应到底由哪些部分组成。
- 方法、状态码、Header、Body 分别承担什么语义。
- HTTP 为什么说是无状态协议,以及状态如何被应用补上。
- HTTP/1.1、HTTP/2、HTTP/3 的核心差异是什么。
- 排障时怎样从状态码、头部、连接阶段判断问题位置。
这张图怎么读
这张图提醒你:HTTP 不只是请求到了服务器再返回。浏览器、代理、CDN、源站都可能参与缓存判断;客户端和服务端还可能复用已有连接。排障时要同时看状态码、Header、缓存命中和连接阶段。
1. HTTP 的核心模型:请求和响应
HTTP 的基本交互是:
Client — Request —> Server
Client <— Response — Server
请求不是单纯一段字符串,它包含:
| 部分 | 作用 |
|---|---|
| 方法 | 表示想对资源做什么 |
| URL/路径 | 表示目标资源在哪里 |
| Header | 控制信息和元数据 |
| Body | 请求携带的数据 |
响应包含:
| 部分 | 作用 |
|---|---|
| 状态码 | 表示处理结果类别 |
| Header | 响应元数据和控制信息 |
| Body | 返回的数据 |
HTTP 不是只为网页服务。它是一种通用应用协议模型,可以承载文档、图片、接口数据、文件下载、流式响应等。
2. HTTP/1.1 请求长什么样
一个简化的 HTTP/1.1 请求:
代码块收起展开
GET /users/42 HTTP/1.1
Host: api.example.com
Accept: application/json
如果有请求体,body 会跟在空行之后:
代码块收起展开
POST /orders HTTP/1.1
Host: api.example.com
Content-Type: application/json
Content-Length: 31
{"item":"book","quantity":1}结构可以理解为:
请求行
Header 列表
空行
Body
空行很重要。它表示 Header 结束,后面开始进入 Body。
3. HTTP/1.1 响应长什么样
一个响应:
代码块收起展开
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 27
{"id":42,"name":"example"}结构是:
状态行
Header 列表
空行
Body
响应体长度可以通过 Content-Length 指定,也可以用 chunked 等方式分块传输。
如果 Body 长度处理错,可能出现:
| 问题 | 结果 |
|---|---|
| Content-Length 比实际大 | 客户端一直等更多数据,直到超时 |
| Content-Length 比实际小 | 客户端截断 body,剩余字节污染后续解析 |
| 分块格式错误 | 客户端解析失败 |
这就是为什么 HTTP 虽然看起来是文本协议,底层仍然要严肃处理字节边界。
字节边界:HTTP 报文不是“按行读到结束”
HTTP Header 可以按行解析,但 Body 必须按协议边界解析。常见边界来源:
| 方式 | 如何知道 Body 结束 |
|---|---|
Content-Length | 读固定字节数 |
Transfer-Encoding: chunked | 按 chunk 长度读取直到 0 chunk |
| 无 Body 的状态/方法 | 根据语义不读 body |
| 连接关闭表示结束 | 老式或特定场景,复用连接时不适合依赖 |
如果使用连接复用,边界错误会污染下一次响应:
响应 A 的多余字节
-> 被客户端当成响应 B 的开头
所以 HTTP 实现最底层仍然是严谨的字节流解析,而不是“看起来像文本就随便 split”。
4. 方法语义:安全性和幂等性
常见方法:
| 方法 | 常见语义 |
|---|---|
| GET | 获取资源 |
| POST | 提交数据、创建资源或触发处理 |
| PUT | 整体替换资源 |
| PATCH | 部分更新资源 |
| DELETE | 删除资源 |
| HEAD | 只获取响应头 |
| OPTIONS | 查询服务能力,常见于跨域预检 |
两个重要概念:
| 概念 | 含义 |
|---|---|
| 安全 | 请求不应该改变资源状态 |
| 幂等 | 同一个请求执行一次和多次,最终效果应相同 |
通常:
| 方法 | 安全 | 幂等 |
|---|---|---|
| GET | 是 | 是 |
| HEAD | 是 | 是 |
| PUT | 否 | 是 |
| DELETE | 否 | 通常设计为是 |
| POST | 否 | 通常不是 |
| PATCH | 否 | 不一定 |
这不是语法强制,而是协议语义约定。服务实现如果违反语义,会让缓存、重试、代理、客户端行为变得危险。
例如:如果 GET 请求会删除数据,那么浏览器预取、缓存刷新、爬虫访问都可能造成破坏。
反例:方法语义错了,会把中间层变成风险源
HTTP 的方法语义会影响浏览器、代理、缓存、重试库和网关策略。几个常见反例:
| 设计 | 为什么危险 |
|---|---|
| 用 GET 触发删除或扣费 | 预取、爬虫、缓存刷新都可能触发副作用 |
| 用 POST 做可安全重试的查询却不加幂等键 | 超时重试可能创建重复资源 |
| DELETE 每次调用都报错而不是保持最终删除状态 | 客户端重试和状态收敛会变复杂 |
| PUT 只做部分更新但不说明语义 | 客户端以为是整体替换,数据可能被覆盖 |
设计 HTTP 接口时要把“网络会超时、客户端会重试、中间层会缓存或预取”当作现实前提。方法语义不是为了好看,它决定失败后能不能安全恢复。
5. 状态码是排障入口
状态码分组:
| 范围 | 含义 | 常见例子 |
|---|---|---|
| 1xx | 信息性响应 | 100 Continue |
| 2xx | 成功 | 200、201、204 |
| 3xx | 重定向或缓存相关 | 301、302、304 |
| 4xx | 客户端侧请求问题 | 400、401、403、404、409、429 |
| 5xx | 服务侧或上游问题 | 500、502、503、504 |
更具体的排障直觉:
| 状态码 | 常见含义 |
|---|---|
| 400 | 请求格式或参数不符合要求 |
| 401 | 未认证或认证信息无效 |
| 403 | 已识别身份,但没有权限 |
| 404 | 资源或路由不存在 |
| 409 | 状态冲突 |
| 429 | 请求过多,被限流 |
| 500 | 服务内部错误 |
| 502 | 网关连接上游失败或上游响应异常 |
| 503 | 服务不可用,可能维护、过载、实例不足 |
| 504 | 网关等待上游超时 |
状态码不是结论,但它能帮你缩小范围。
6. Header:HTTP 的控制平面
Header 不承载主体数据,却控制了大量行为。
| Header | 作用 |
|---|---|
| Host | 同一个 IP 上区分不同站点或服务 |
| Content-Type | Body 的格式 |
| Content-Length | Body 的字节长度 |
| Accept | 客户端希望接收的格式 |
| Authorization | 认证信息 |
| Cookie | 浏览器携带的状态数据 |
| Cache-Control | 缓存策略 |
| ETag | 资源版本标识 |
| Location | 重定向目标 |
| User-Agent | 客户端信息 |
| X-Forwarded-For | 代理链路中记录原始来源的常见头 |
Header 的关键点是:它们经常影响中间设备行为。
缓存看 Cache-Control / ETag
代理看 Host / 转发头
浏览器看 Cookie / CORS 相关头
客户端看 Content-Type / Content-Length
7. HTTP 是无状态协议
HTTP 本身不记住上一次请求。
请求 1: GET /profile
请求 2: POST /orders
协议层面这两次请求是独立的。所谓登录状态、购物车、会话上下文,通常由应用用额外机制补上:
| 机制 | 思路 |
|---|---|
| Cookie + Session | 客户端带会话标识,服务侧查会话数据 |
| Token | 客户端每次带可验证凭证 |
| URL 参数 | 少量场景把状态放在链接里 |
| 服务侧存储 | 状态保存在服务侧或共享存储中 |
无状态的好处是请求可以被独立转发、扩展和缓存。代价是应用必须明确设计状态保存、过期、刷新、撤销和安全边界。
8. 缓存:HTTP 不只是传输,还会影响是否访问源站
HTTP 缓存能减少延迟和带宽消耗,但也会导致“为什么我改了内容还是旧的”。
常见机制:
| 机制 | 含义 |
|---|---|
| Cache-Control | 控制是否缓存、缓存多久、是否必须重新验证 |
| ETag | 服务给资源的版本标识 |
| Last-Modified | 资源最后修改时间 |
| 304 Not Modified | 客户端缓存仍可用,不返回完整 body |
简化流程:
第一次请求:
客户端 -> 服务端
服务端 -> body + ETag
客户端缓存
再次请求:
客户端 -> 带 ETag 询问是否变化
服务端 -> 304
客户端使用本地缓存
缓存问题要同时看浏览器、代理、缓存节点和源服务响应头。
9. 连接复用与 Keep-Alive
每次 HTTP 请求都重新建立 TCP/TLS 连接成本很高。
Keep-Alive 允许一条连接承载多个请求:
TCP/TLS 连接建立
请求 1 -> 响应 1
请求 2 -> 响应 2
请求 3 -> 响应 3
连接关闭或回收
好处:
| 好处 | 解释 |
|---|---|
| 减少握手成本 | 不必每次重新 TCP/TLS 握手 |
| 降低延迟 | 后续请求可以更快发出 |
| 减少资源抖动 | 避免大量短连接创建和关闭 |
代价:
| 代价 | 解释 |
|---|---|
| 连接池管理复杂 | 空闲连接、最大连接数、失效连接都要处理 |
| 中间设备可能关闭空闲连接 | 应用复用时可能遇到已断连接 |
| 长连接占资源 | 服务端和客户端都要维护状态 |
连接复用是性能优化,也是资源管理问题。
10. HTTP/1.1、HTTP/2、HTTP/3
三个版本的核心差异:
| 版本 | 传输基础 | 核心特点 | 主要解决 |
|---|---|---|---|
| HTTP/1.1 | TCP | 文本消息、持久连接 | 基本请求响应与连接复用 |
| HTTP/2 | TCP | 二进制帧、多路复用、头部压缩 | 多请求并发和头部开销 |
| HTTP/3 | QUIC/UDP | 基于 QUIC,多路复用,连接迁移 | 减少 TCP 层队头阻塞和握手成本 |
10.1 HTTP/1.1 的限制
HTTP/1.1 可以复用连接,但一个连接上请求/响应的处理仍容易受到顺序影响。实际系统通常会开多条连接并发请求。
10.2 HTTP/2 的多路复用
HTTP/2 把数据拆成帧,在一条连接上承载多个流:
TCP 连接
stream 1: request/response
stream 2: request/response
stream 3: request/response
这样可以减少连接数量,提高并发效率。但它仍然运行在 TCP 上,如果 TCP 层丢包,整条 TCP 字节流的有序交付仍可能影响多个流。
10.3 HTTP/3 与 QUIC
HTTP/3 基于 QUIC,QUIC 运行在 UDP 之上,在传输层内部实现加密、可靠传输、多路复用等能力。
它的重要目标是减少 TCP 层队头阻塞,并改善连接建立、网络切换等场景的体验。
初学时要先吃透 HTTP 的语义,再理解版本优化。版本变化主要优化传输方式,不改变“请求/响应、方法、状态码、头部、资源语义”的基本思维。
11. 联系实际:一次 HTTP 请求慢,应该拆成哪些阶段
一个 URL 访问慢,可能慢在很多地方:
DNS 解析
TCP 连接
TLS 握手
请求上传
服务处理
等待首字节
响应下载
缓存命中/未命中
重定向
代理转发
每个阶段对应不同证据:
| 阶段 | 常见证据 |
|---|---|
| DNS 慢 | 解析时间长、不同解析器结果不同 |
| TCP 慢 | connect time 长、SYN 重传 |
| TLS 慢 | 证书验证、握手失败、协议协商异常 |
| 服务处理慢 | 首字节等待长,服务日志耗时高 |
| 下载慢 | 响应体大、带宽不足、丢包、窗口限制 |
| 缓存异常 | 304/Cache-Control/ETag 行为不符合预期 |
| 网关异常 | 502/503/504、上游连接失败 |
所以排查 HTTP 不只是看状态码,还要看时间分解和链路位置。
排障卡:改了内容还是旧的,先看缓存证据
“HTTP 返回旧内容”通常不是一句“缓存问题”就结束。按层拆:
| 证据 | 说明 |
|---|---|
Cache-Control | 是否允许缓存、缓存多久、是否必须重新验证 |
ETag / Last-Modified | 客户端是否能做条件请求 |
304 Not Modified | 服务认为缓存仍有效,未返回完整 body |
| CDN/代理命中 Header | 是否被中间缓存直接返回 |
| 浏览器缓存状态 | 是否没有真正发起网络请求 |
| 源站日志 | 请求有没有到达源站 |
再看连接复用:
请求失败发生在复用旧连接后的第一次写入
-> 可能是中间设备或服务端回收了空闲连接
HTTP/2 多个流一起变慢
-> 可能是底层 TCP 丢包、连接级窗口或上游处理阻塞
HTTP 排障的基本动作是把“协议语义”和“传输连接”分开:状态码/方法/Header 解释语义,连接复用/握手/下载解释传输成本。
手推:Content-Length 错了会让 HTTP 边界整体错位
HTTP/1.1 在同一连接上复用多个请求/响应时,接收方必须知道当前消息 body 到哪里结束。
假设响应头写:
Content-Length: 5
body 实际发送:
HelloWorld
接收方会把前 5 byte 当作当前响应体:
Hello
剩下的:
World
可能被误认为下一个响应的开头,整个连接上的消息边界都会错位。
反过来,如果 Content-Length 写得比实际 body 长,接收方会继续等待不存在的字节,表现为卡住或超时。
所以 HTTP 不是“读到空行后随便读一点 body”。body 边界必须由协议规则决定,例如 Content-Length、分块传输或连接关闭。
边界条件:状态码是入口,不是完整结论
同一个状态码可能来自不同层:
| 状态码 | 可能来源 |
|---|---|
| 404 | 源站没有资源、网关路由不到、缓存里保存了旧 404 |
| 502 | 代理无法连接上游、上游返回非法响应、协议转换失败 |
| 503 | 上游过载、维护、限流、负载均衡没有健康节点 |
| 504 | 网关等上游超时,不代表客户端到网关不通 |
排障时不能只记录:
返回 502
要继续问:
这个状态码是谁生成的?
请求有没有到源站?
上游连接是否建立?
响应是否被代理改写?
缓存是否命中?
HTTP 的控制面在 header 和中间层里。状态码是入口,证据链才是结论。
12. 学完本章你能解决什么问题
学完本章,你应该能:
- 手写并解释一个 HTTP/1.1 请求和响应的结构。
- 区分方法语义、安全性、幂等性,并理解它们对重试和缓存的影响。
- 根据 4xx、5xx、502、503、504 等状态码判断初步排障方向。
- 解释 Header 如何影响内容类型、缓存、认证、代理转发和连接管理。
- 理解 HTTP 无状态,以及应用如何用 Cookie、Session、Token 等机制补状态。
- 解释 Keep-Alive、连接复用的性能收益和资源风险。
- 概括 HTTP/1.1、HTTP/2、HTTP/3 的核心差异。
- 把一次慢请求拆成 DNS、连接、握手、服务处理、下载、缓存、网关等阶段定位。