mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
2072 字
6 分钟
为什么 HTTP 是无状态协议
2024-02-25

HTTP(HyperText Transfer Protocol)是现代 Web 的基石。从诞生之初,HTTP 就被设计为一个无状态协议(Stateless Protocol)。这个设计决策深刻影响了整个 Web 技术栈的演进,从 Cookie/Session 机制到 JWT Token,再到微服务架构,无不与这一设计息息相关。

一、什么是无状态协议#

1.1 无状态的定义#

HTTP 的无状态性是指:服务器不会保留与客户端交互的任何状态上下文。每个请求都是完全独立的,服务器无法知道两个请求是否来自同一个客户端,也无法知道它们之间的关联。

sequenceDiagram participant C as 客户端 participant S as 服务器 C->>S: 请求 1: GET /index.html S->>C: 响应 1: 返回页面 Note over C,S: 服务器处理完请求后<br>不保存任何状态 C->>S: 请求 2: GET /profile.html S->>C: 响应 2: 返回页面 Note over C,S: 服务器将请求 2 视为<br>全新的、独立的请求

RFC 7230 对 HTTP 无状态性的定义:

The HTTP protocol is a stateless request/response protocol.

1.2 有状态 vs 无状态的对比#

flowchart TB subgraph 有状态协议 A1[客户端] -->|请求 1| B1[服务器] B1 -->|响应 1 + 状态| A1 A1 -->|请求 2 (带状态引用)| B1 B1 -->|基于状态处理| A1 Note1[服务器维护会话状态] end subgraph 无状态协议 A2[客户端] -->|请求 1| B2[服务器] B2 -->|响应 1| A2 A2 -->|请求 2 (完整信息)| B2 B2 -->|独立处理| A2 Note2[服务器无状态记忆] end

1.3 HTTP 请求的独立性#

每个 HTTP 请求必须包含服务器处理该请求所需的所有信息

GET /api/user/profile HTTP/1.1
Host: example.com
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
Accept: application/json

服务器不需要记住「这个用户是谁」,因为每个请求都会携带认证信息。

二、设计无状态的历史原因#

2.1 简单性原则#

1991 年,Tim Berners-Lee 在 CERN 设计 HTTP 时,目标是创建一个极其简单的协议来传输超文本文档。

timeline title HTTP 协议演进史 1991 : HTTP/0.9 - 单行协议 : 仅支持 GET : 无头部,无状态 1996 : HTTP/1.0 - 标准化 : 添加头部 : 支持多种内容类型 1997 : HTTP/1.1 - 持久连接 : Keep-Alive : 管道化 2015 : HTTP/2 - 多路复用 2022 : HTTP/3 - QUIC 传输

简单性带来的好处:

方面简单设计的优势
实现难度容易实现,降低准入门槛
调试友好请求响应清晰,易于排查问题
互操作性不同实现容易兼容
规范演进容易扩展新特性

2.2 可扩展性考量#

无状态设计天然支持水平扩展:

flowchart LR subgraph 客户端 C1[用户 A] C2[用户 B] C3[用户 C] end subgraph 负载均衡 LB[Load Balancer] end subgraph 服务器集群 S1[Server 1] S2[Server 2] S3[Server 3] end C1 --> LB C2 --> LB C3 --> LB LB --> S1 LB --> S2 LB --> S3 Note over S1,S3: 无状态服务器<br>任意服务器可处理任意请求

如果 HTTP 是有状态的,扩展会变得极其困难:

flowchart LR subgraph 有状态的问题 subgraph 服务器 S1[Server 1<br>用户 A 的会话] S2[Server 2<br>用户 B 的会话] end A[用户 A] -->|请求| LB[负载均衡] LB -->|路由到错误服务器| S2 S2 -->|找不到会话| E[错误] end

2.3 早期 Web 的需求#

1990 年代的 Web 主要是静态文档的获取:

flowchart LR A[浏览器] -->|请求文档| B[HTTP 服务器] B -->|返回 HTML| A Note over A,B: 单向的信息获取<br>无需记住用户状态

当时的典型场景:

  • 浏览论文和文档
  • 获取静态页面
  • 没有登录、购物车等交互需求

无状态设计完全满足这些需求,而且实现最简单。

三、无状态的优点#

3.1 水平扩展能力#

无状态是微服务架构的基石:

flowchart TB subgraph 无状态架构 LB[负载均衡] --> S1[实例 1] LB --> S2[实例 2] LB --> S3[实例 3] S1 --> DB[(共享存储)] S2 --> DB S3 --> DB end Note over LB,DB: 任意实例可处理任意请求<br>弹性伸缩成为可能

有状态架构的扩展困境

问题有状态架构无状态架构
添加服务器需要迁移会话或同步状态直接加入,无需配置
服务器故障会话丢失,用户体验中断请求被路由到其他
负载均衡策略需要会话粘滞(Sticky)任意路由
滚动更新复杂的会话迁移直接替换

3.2 缓存友好#

无状态使得缓存策略简单有效:

sequenceDiagram participant C as 客户端 participant CD as CDN 缓存 participant S as 源服务器 C->>CD: GET /api/articles CD->>CD: 缓存命中? alt 缓存命中 CD->>C: 返回缓存内容 else 缓存未命中 CD->>S: 转发请求 S->>CD: 返回响应 CD->>CD: 缓存响应 CD->>C: 返回内容 end Note over C,S: 无状态请求可被任意节点缓存

HTTP 缓存机制依赖于无状态性:

HTTP/1.1 200 OK
Cache-Control: max-age=3600, public
ETag: "abc123"
Last-Modified: Sun, 22 Mar 2026 15:00:00 GMT

3.3 可靠性提升#

无状态设计提高了系统的容错能力:

stateDiagram-v2 [*] --> 请求到达 请求到达 --> 服务器A: 路由到 A 请求到达 --> 服务器B: 路由到 B 服务器A --> 处理成功: A 可用 服务器A --> 服务器B: A 故障,重试 服务器B --> 处理成功: B 可用 处理成功 --> [*] note right of 服务器A: 服务器故障时<br>请求可被重新路由

3.4 调试简单#

无状态请求天然可重现:

# 无状态请求,任何时间、任何地点都能重现
curl -X GET "https://api.example.com/users/123" \
-H "Authorization: Bearer token123"
# 有状态请求需要重现整个会话上下文(非常困难)

四、无状态的缺点#

4.1 会话管理的复杂性#

Web 应用需要记住用户状态,但 HTTP 不提供这种能力。开发者必须在应用层实现会话管理:

flowchart TB subgraph 应用层需要解决的问题 A[用户身份识别] B[会话状态存储] C[会话安全保护] D[会话过期处理] end subgraph 实现方案 A --> A1[Cookie + Session] A --> A2[JWT Token] B --> B1[内存存储] B --> B2[Redis 集群] B --> B3[数据库] C --> C1[HTTPS] C --> C2[HttpOnly Cookie] C --> C3[CSRF Token] D --> D1[过期时间] D --> D2[滑动过期] end

4.2 每次请求携带完整信息#

无状态意味着每个请求都要携带认证信息:

sequenceDiagram participant C as 客户端 participant S as 服务器 Note over C: 每次请求都要携带 Token C->>S: GET /api/user<br>Authorization: Bearer xxx... S->>C: 200 OK C->>S: GET /api/orders<br>Authorization: Bearer xxx... S->>C: 200 OK C->>S: GET /api/cart<br>Authorization: Bearer xxx... S->>C: 200 OK Note over C,S: Token 可能有 500-1000 字节<br>累积起来带宽开销可观

4.3 服务器端状态存储压力#

虽然协议无状态,但业务需要状态,压力转移到服务器:

flowchart LR subgraph 客户端 C1[浏览器 1] C2[浏览器 2] C3[浏览器 N] end subgraph 服务器 S[Web 服务器] R[(Redis 会话存储)] end C1 -->|Cookie: SessionID| S C2 -->|Cookie: SessionID| S C3 -->|Cookie: SessionID| S S -->|查询会话| R Note over R: 需要存储所有用户的会话<br>内存压力大,需要分布式方案

五、Cookie/Session 的诞生#

1994 年,Netscape 的 Lou Montulli 发明了 Cookie,解决了 HTTP 无状态带来的用户识别问题:

sequenceDiagram participant C as 客户端 participant S as 服务器 Note over C,S: 首次访问 C->>S: GET /login S->>C: 200 OK<br>Set-Cookie: session_id=abc123 Note over C,S: 后续请求 C->>S: GET /profile<br>Cookie: session_id=abc123 S->>S: 查找 session_id 对应的用户 S->>C: 200 OK (用户数据)

5.2 Session 机制#

Session 将状态存储在服务器,Cookie 只存储标识符:

flowchart TB subgraph 客户端 Cookie[Cookie: session_id=xxx] end subgraph 服务器 Web[Web 服务器] Store[(Session 存储)] end Cookie -->|请求携带| Web Web -->|根据 session_id 查询| Store Store -->|返回会话数据| Web subgraph Session 数据 D1[用户 ID] D2[登录时间] D3[购物车内容] D4[用户偏好] end

现代 Cookie 机制提供了多层安全保护:

Set-Cookie: session_id=abc123;
HttpOnly; # JavaScript 无法访问,防 XSS
Secure; # 仅 HTTPS 传输
SameSite=Strict; # 防 CSRF 攻击
Path=/; # 作用路径
Domain=example.com; # 作用域
Max-Age=3600 # 过期时间

5.4 Session 存储的演进#

timeline title Session 存储演进 1995-2000 : 单机内存存储 : 简单快速 : 重启丢失 2000-2010 : 数据库存储 : 持久化 : 性能瓶颈 2010-至今 : Redis/Memcached : 高性能 : 分布式支持

六、Token(JWT)的设计理念#

6.1 JWT 的结构#

JSON Web Token 将状态编码在 Token 本身,实现了「无状态认证」:

JWT 结构: Header.Payload.Signature
Header: {"alg": "HS256", "typ": "JWT"}
Payload: {"sub": "user123", "name": "张三", "exp": 1711123200}
Signature: HMACSHA256(base64(Header) + "." + base64(Payload), secret)

6.2 JWT 的工作流程#

sequenceDiagram participant C as 客户端 participant A as 认证服务 participant R as 资源服务 C->>A: POST /login (用户名/密码) A->>A: 验证凭据 A->>A: 生成 JWT (包含用户信息) A->>C: 返回 JWT Token Note over C: 存储 Token C->>R: GET /api/resource<br>Authorization: Bearer JWT R->>R: 验证签名 (无需查询数据库) R->>R: 解析 Payload 获取用户信息 R->>C: 返回资源

6.3 Session vs JWT 对比#

flowchart TB subgraph Session 方案 S1[客户端存储: Session ID] S2[服务器存储: 完整会话数据] S3[验证: 查询存储] S1 --> S2 --> S3 end subgraph JWT 方案 J1[客户端存储: 完整 Token] J2[服务器存储: 无 (或黑名单)] J3[验证: 签名验证] J1 --> J2 --> J3 end
方面SessionJWT
状态存储位置服务器客户端 (Token 内)
扩展性需要共享存储天然分布式
注销难易容易 (删除 Session)困难 (需要黑名单)
安全性较高 (服务器控制)依赖客户端保护
跨域支持复杂简单
适合场景单体应用微服务、移动端

6.4 JWT 的问题#

JWT 并非完美解决方案:

flowchart LR subgraph JWT 的挑战 A[无法即时注销] B[Token 较大] C[敏感信息暴露] D[续期机制复杂] end A --> A1[需要维护黑名单<br>违背无状态初衷] B --> B1[每次请求携带<br>带宽开销] C --> C1[Payload 可被解码<br>不能存敏感数据] D --> D1[Refresh Token 机制<br>增加复杂度]

七、有状态协议的对比#

7.1 FTP 协议#

FTP 是典型的有状态协议,使用独立的控制和数据连接:

sequenceDiagram participant C as FTP 客户端 participant S as FTP 服务器 Note over C,S: 控制连接 (21 端口) C->>S: USER username S->>C: 331 Password required C->>S: PASS password S->>C: 230 Login successful Note over C,S: 服务器维护登录状态 C->>S: PWD (当前目录) S->>C: 257 "/home/user" C->>S: PASV (请求被动模式) S->>C: 227 Entering Passive Mode (ip,port) Note over C,S: 数据连接 (临时端口) C->>S: RETR file.txt S->>C: 数据传输

FTP 的有状态特性:

状态信息说明
认证状态用户是否已登录
当前目录用户所在的目录位置
传输模式ASCII/Binary
被动/主动数据连接建立方式

7.2 SMTP 协议#

SMTP 同样维护会话状态:

sequenceDiagram participant C as 邮件客户端 participant S as SMTP 服务器 C->>S: EHLO client.example.com S->>C: 250 Hello C->>S: MAIL FROM:<sender@example.com> S->>C: 250 OK Note over S: 记住发件人 C->>S: RCPT TO:<recipient@example.com> S->>C: 250 OK Note over S: 记住收件人 C->>S: DATA S->>C: 354 End with <CR><LF>.<CR><LF> C->>S: 邮件内容... S->>C: 250 OK: queued Note over S: 整个邮件事务完成

7.3 为什么这些协议需要状态?#

flowchart TB subgraph FTP 需要状态 F1[多步骤操作] F2[断点续传] F3[目录导航] end subgraph SMTP 需要状态 M1[多命令事务] M2[发件人/收件人关联] M3[邮件完整性] end subgraph HTTP 无状态 H1[独立资源获取] H2[无多步骤事务] H3[简单性优先] end

7.4 有状态协议的代价#

方面有状态协议 (FTP/SMTP)无状态协议 (HTTP)
服务器资源需维护连接状态请求结束即释放
扩展性困难容易
容错性状态丢失需重建请求可重试
负载均衡需要会话粘滞任意路由
协议复杂度

八、RESTful 架构与无状态#

8.1 REST 的无状态约束#

REST 架构风格明确要求无状态(Stateless)约束:

flowchart LR subgraph REST 约束 C1[客户端-服务器分离] C2[无状态] C3[可缓存] C4[统一接口] C5[分层系统] C6[按需代码] end C2 --> S[每个请求包含<br>所有必要信息]

Roy Fielding 在论文中强调:

Each request from client to server must contain all of the information necessary to understand the request.

8.2 RESTful API 的无状态设计#

sequenceDiagram participant C as 客户端 participant S as REST API 服务器 Note over C: 获取用户信息 C->>S: GET /users/123<br>Authorization: Bearer token S->>C: 200 OK (用户数据) Note over C: 更新用户信息 C->>S: PUT /users/123<br>Authorization: Bearer token<br>{"name": "新名字"} S->>C: 200 OK Note over C: 删除用户 C->>S: DELETE /users/123<br>Authorization: Bearer token S->>C: 204 No Content Note over S: 每个请求独立<br>无需记住之前发生了什么

8.3 违反无状态的设计#

常见的反模式:

flowchart TB subgraph 反模式: 服务器存储请求上下文 A[请求 1: 开始事务] B[请求 2: 添加操作] C[请求 3: 提交事务] A --> D[(服务器事务状态)] B --> D C --> D end Note over D: 如果服务器重启<br>事务状态丢失

正确的 RESTful 设计应该让客户端维护状态:

flowchart TB subgraph 正确: 客户端维护状态 E[客户端构建完整请求] F[一次性提交] E --> G[POST /orders<br>完整订单数据] F --> G end

8.4 HATEOAS 与无状态#

HATEOAS(Hypermedia as the Engine of Application State)是 REST 的高级约束:

{
"id": 123,
"status": "pending",
"items": [...],
"_links": {
"self": { "href": "/orders/123" },
"cancel": { "href": "/orders/123/cancel", "method": "POST" },
"pay": { "href": "/orders/123/payment", "method": "POST" }
}
}

服务器通过响应告诉客户端可以做什么,而不是客户端需要记住状态。

九、HTTP/2、HTTP/3 的状态管理改进#

9.1 HTTP/2 的连接复用#

HTTP/2 引入了多路复用,但协议层面仍是无状态的

flowchart LR subgraph HTTP/1.1 A1[请求 1] --> B1[TCP 连接 1] A2[请求 2] --> B2[TCP 连接 2] A3[请求 3] --> B3[TCP 连接 3] end subgraph HTTP/2 C1[请求 1] --> D[单一 TCP 连接] C2[请求 2] --> D C3[请求 3] --> D D --> E[Stream 1] D --> F[Stream 2] D --> G[Stream 3] end Note over D: 多路复用<br>但每个请求仍独立

9.2 HTTP/2 的状态特性#

虽然 HTTP/2 引入了连接级别的状态(如流量控制窗口),但这些是传输层的优化,不影响应用层的无状态语义:

flowchart TB subgraph HTTP/2 新特性 A[多路复用 Stream] B[头部压缩 HPACK] C[服务器推送] D[流量控制] end subgraph 无状态语义保持 E[请求独立性] F[响应自描述] G[无会话概念] end A --> E B --> F C --> E D --> G

9.3 HTTP/3 与 QUIC#

HTTP/3 使用 QUIC 传输协议,同样保持无状态语义:

sequenceDiagram participant C as 客户端 participant S as 服务器 Note over C,S: QUIC 0-RTT 连接 C->>S: 0-RTT 请求 (携带之前的数据) S->>C: 响应 Note over C,S: 请求仍需携带完整信息 C->>S: GET /api/user<br>Authorization: Bearer token S->>C: 200 OK

QUIC 的 0-RTT 特性减少了连接建立延迟,但不会改变 HTTP 的无状态本质。

9.4 HTTP/2 头部压缩的状态性#

HPACK 是 HTTP/2 中唯一引入状态的部分:

flowchart LR subgraph HPACK 状态 A[动态表] B[静态表] end C[请求 1] -->|存储常用头部| A D[请求 2] -->|引用动态表索引| A Note over A: 编码器和解码器<br>维护同步的动态表

但这是传输优化,应用层逻辑仍然是无状态的。

十、现代应用的状态管理策略#

10.1 状态分层架构#

现代 Web 应用采用分层的状态管理策略:

flowchart TB subgraph 客户端状态 A1[UI 状态] A2[本地缓存] A3[临时数据] end subgraph 会话状态 B1[JWT Token] B2[Refresh Token] end subgraph 服务器状态 C1[Session 存储] C2[数据库] C3[缓存层] end A1 --> B1 B1 --> C1 B2 --> C1 A2 --> C3

10.2 分布式会话方案#

flowchart TB subgraph 客户端 C[浏览器] end subgraph 应用服务器 S1[Server 1] S2[Server 2] S3[Server N] end subgraph 共享存储 R[(Redis 集群)] end C -->|Session ID| LB[负载均衡] LB --> S1 LB --> S2 LB --> S3 S1 --> R S2 --> R S3 --> R

10.3 无状态服务的设计模式#

flowchart LR subgraph 无状态服务原则 A[请求自包含] B[不依赖本地存储] C[幂等设计] D[外部化状态] end A --> E[所有信息在请求中] B --> F[使用外部存储] C --> G[重复请求结果相同] D --> H[Redis/DB 存储会话]

10.4 实际架构示例#

一个典型的现代 Web 应用状态管理:

flowchart TB subgraph 前端 F[React/Vue 应用] F1[Zustand/Redux<br>客户端状态] F2[LocalStorage<br>持久化数据] end subgraph API 网关 G[Kong/Nginx] end subgraph 微服务 A[认证服务] B[用户服务] C[订单服务] end subgraph 数据层 R[(Redis<br>会话缓存)] D[(PostgreSQL<br>持久化)] end F -->|JWT Token| G G -->|验证 Token| A G --> B G --> C A --> R B --> D C --> D F1 --> F2

十一、总结与权衡#

11.1 无状态设计的本质#

HTTP 的无状态设计是一个深思熟虑的工程决策

flowchart TB A[无状态设计] --> B[简单性] A --> C[可扩展性] A --> D[可靠性] A --> E[可缓存性] B --> F[易于实现和理解] C --> G[水平扩展能力] D --> H[容错和恢复] E --> I[CDN/代理缓存] J[代价: 会话管理复杂性] --> A

11.2 设计权衡总结#

方面选择无状态的原因付出的代价
简单性协议实现简单应用层需实现会话
扩展性天然支持水平扩展需要共享存储
可靠性请求独立,容错性强每次请求携带完整信息
可缓存性响应可被任意节点缓存缓存失效策略复杂
安全性无状态减少攻击面Token 管理复杂

11.3 最佳实践#

flowchart LR subgraph 选择 Session S1[单体应用] S2[安全要求高] S3[需要即时注销] end subgraph 选择 JWT J1[微服务架构] J2[移动应用] J3[跨域需求] end subgraph 混合方案 H1[JWT 短期访问] H2[Refresh Token + Session] H3[敏感操作二次验证] end

11.4 最终思考#

HTTP 的无状态设计体现了互联网架构的核心理念:简单性优于完备性。正是因为无状态,HTTP 才能成为支撑全球规模 Web 应用的协议。理解无状态的权衡,才能更好地设计现代 Web 应用。

「无状态」不是缺陷,而是特性。关键在于如何在应用层妥善处理状态管理。

参考引用#

支持与分享

如果这篇文章对你有帮助,欢迎支持作者或分享给更多人

为什么 HTTP 是无状态协议
https://blog.souloss.com/posts/why-the-design/why-http-is-stateless/
作者
Souloss
发布于
2024-02-25
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时