HTTP 原理与报文 — 报文结构、方法、状态码与版本演进

HTTP 请求/响应报文格式、方法与状态码全表、关键首部、持久连接与 HTTP/2 分帧 / 应用层 / TCP 80 / RFC 9110

先建立直觉

HTTP 说白了就是一问一答:你说清楚"我想对哪个东西做什么",对方回一句"办成了 / 没办成,原因是这个",顺便把东西给你。

它之所以能撑起整个 Web,靠的是把"提问"和"回答"都标准化成了固定套路——谁都能读懂,谁都能转发,中间还能随手加个缓存、加个代理。而且它记性不好,办完一件事就把你忘了,所以你每次来都得自报家门;这个"缺点"恰恰让服务器能随便加机器扛流量。

它解决什么问题

设想一个没有 HTTP 的世界:想下载文件学一套协议,想看新闻学另一套,想查目录再学第三套。每加一种服务,客户端就得多学一门"外语"。

HTTP 做的事情是把千奇百怪的需求压缩成一套通用问句

  • **“东西在哪”**统一用一个地址串表达,不管它是网页、图片还是接口。
  • **“你想干什么”**统一收敛成几个动词——取、提交、替换、删除,服务端一看就懂。
  • **“结果怎样”**统一用一个三位数字回答,客户端不用理解业务也知道该重试、该跳转还是该报错。
  • “还有什么要交代的”(我能接受什么语言、这份内容能存多久、我是谁)全塞进可自由扩展的"备注栏"里,以后要加新能力也不用改协议本身。

所以后来的 CORS、HSTS、压缩、断点续传,全都是往备注栏里加字段加出来的——协议核心二十年基本没动。

工作流程(简化版)

一次完整的 HTTP/1.1 交互经历五步:

  1. 拆地址:把你输入的网址拆成"哪台机器、哪个端口、要哪个路径、带什么参数"。
  2. 找到人并接上线:先问 DNS 要 IP,再和对方建立 TCP 连接(HTTPS 还要多做一次 TLS 握手)。
  3. 把问题写清楚发过去:按固定格式写好请求行、备注栏和正文,一股脑写进连接里。
  4. 等对方回话:服务器处理完,回一个状态行 + 备注栏 + 正文。
  5. 线先别挂:HTTP/1.1 默认保持连接,下一个请求接着用同一条线;除非对方说要关或者闲太久了。

展开细节(原理原文)

  1. 解析 URLhttps://api.example.com:443/v1/users?id=7 拆成 scheme、host、port、path、query。
  2. DNS 解析 + 建立连接:域名 → IP → TCP 三次握手(HTTPS 再加 TLS 握手)。
  3. 发送请求报文:客户端把方法、目标、首部、正文按 ASCII 文本格式串行写入 TCP 流。
  4. 服务器处理并回送响应报文:状态行 + 首部 + 正文。
  5. 连接复用或关闭:HTTP/1.1 默认 Connection: keep-alive,同一 TCP 连接可继续承载后续请求;收到 Connection: close 或超时则关闭。

核心约束是 HTTP/1.1 在一条连接上必须"一问一答、严格有序"。虽然规范允许流水线(Pipelining),但因为响应必须按请求顺序返回,一个慢响应会堵住后面所有响应——这就是 HTTP 层的队头阻塞。浏览器的应对是对同一域名开 6–8 条并行 TCP 连接,HTTP/2 的应对是二进制分帧多路复用。

报文 / 头部长什么样

HTTP/1.1 报文通用格式

看这段格式前先记住:HTTP 报文只有四个部分,靠一个空行分隔上下——空行以上是"信封和备注",空行以下是"包裹内容"。所有 HTTP/1.1 报文,无论请求还是响应,都是这个结构。

1
2
3
4
5
起始行(请求行 或 状态行)\r\n
首部字段名: 值\r\n
首部字段名: 值\r\n
\r\n ← 空行,标志首部结束
[消息正文 Body]

请求示例

1
2
3
4
5
6
7
8
9
POST /v1/users?verbose=1 HTTP/1.1
Host: api.example.com
User-Agent: curl/8.4.0
Accept: application/json
Content-Type: application/json
Content-Length: 27
Cookie: session=abc123; theme=dark

{"name":"alice","age":30}

响应示例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
HTTP/1.1 201 Created
Date: Mon, 03 Aug 2026 02:15:00 GMT
Server: nginx/1.24.0
Content-Type: application/json; charset=utf-8
Content-Length: 45
Location: /v1/users/1024
Cache-Control: no-store
Set-Cookie: session=xyz789; HttpOnly; Secure; SameSite=Lax

{"id":1024,"name":"alice","created":true}

起始行字段

看这张表前先记住:起始行就是报文的第一行,也是唯一一行"不带冒号"的内容。请求的第一行说"我要干什么",响应的第一行说"办得怎么样"。

报文类型格式说明
请求行方法 请求目标 HTTP/版本请求目标常见为 origin-form(/path?query);CONNECT 用 authority-form(host:port);代理请求用 absolute-form(完整 URL);OPTIONS 可用 asterisk-form(*
状态行HTTP/版本 状态码 原因短语原因短语仅供人读,客户端不得依赖其内容

请求方法(RFC 9110 §9)

看这张表前先记住:只需盯住"安全"和"幂等"两列。安全=只读不改;幂等=重复执行结果一样,这是客户端敢不敢自动重试的唯一依据。方法名(Method)即"操作动词"。

方法语义安全幂等可缓存常带请求体
GET(获取)获取资源的当前表示
HEAD(只取头部)同 GET 但只返回首部,不返回正文
POST(提交)提交数据,由服务器决定如何处理(创建/执行)条件性
PUT(整体替换)用请求体整体替换目标资源
DELETE(删除)删除目标资源
PATCH(局部修改,RFC 5789)对资源做局部修改
OPTIONS(查询选项)查询目标资源支持的通信选项(CORS 预检用)
CONNECT(建立隧道)建立到目标的隧道(HTTPS 正向代理必用)
TRACE(回显诊断)回显收到的请求,用于诊断(有 XST 风险,通常禁用)
  • 安全(Safe):不改变服务器状态,只读。
  • 幂等(Idempotent):执行 1 次与执行 N 次,服务器状态的最终效果相同。这是客户端敢于自动重试的前提。

状态码分类(RFC 9110 §15)

看这张表前先记住:首位数字定性质,后两位才是细节。1 开头=还没完,2=成了,3=去别处,4=你的问题,5=我的问题。哪怕遇到一个没见过的状态码,看首位也能立刻决定下一步怎么办。

类别含义高频状态码
1xx 信息请求已收到,处理中100 Continue(配合 Expect: 100-continue 大文件预检)、101 Switching Protocols(WebSocket 升级)、103 Early Hints
2xx 成功请求被成功接收理解200 OK、201 Created(配 Location)、202 Accepted(异步已受理)、204 No Content(成功但无正文)、206 Partial Content(范围请求/断点续传)
3xx 重定向需进一步动作才能完成301 永久重定向(会改写书签,SEO 传权)、302 Found(临时,历史上会把 POST 改成 GET)、303 See Other(强制转 GET)、304 Not Modified(缓存命中,无正文)、307 临时重定向(保持方法与正文)、308 永久重定向(保持方法)
4xx 客户端错误请求有语法错误或无法被满足400 Bad Request、401 Unauthorized(未认证,须带 WWW-Authenticate)、403 Forbidden(已认证但无权限)、404 Not Found、405 Method Not Allowed(须带 Allow)、408 Request Timeout、409 Conflict、413 Content Too Large、415 Unsupported Media Type、429 Too Many Requests(限流,配 Retry-After
5xx 服务端错误服务器未能完成合法请求500 Internal Server Error、501 Not Implemented502 Bad Gateway(上游返回无效响应)、503 Service Unavailable(过载/维护,配 Retry-After)、504 Gateway Timeout(上游超时)、505 HTTP Version Not Supported

记牢三组易混:301 vs 308(后者保留方法与请求体)、401 vs 403(前者"你是谁",后者"你不能")、502 vs 504(前者上游回了坏响应,后者上游压根没回)。

关键首部字段

看这张表前先记住:首部就是报文的"备注栏",按用途分成十来族。不必背下每一个,但要知道遇到某类问题该去翻哪一族——缓存问题看缓存族,跨域问题看 CORS 族,拿不到真实客户端 IP 就看代理链路族。

分类字段作用
必需HostHTTP/1.1 强制,虚拟主机分流依据;缺失服务器必须回 400
正文描述Content-Type / Content-Length / Content-Encoding / Transfer-Encoding: chunked正文的类型、长度、压缩方式、分块传输
内容协商Accept / Accept-Language / Accept-EncodingVary客户端偏好 ↔ 服务端声明"响应随哪些请求头变化"(缓存键的关键)
缓存Cache-Control / Age / Expires / ETag / Last-Modified强缓存与协商缓存
条件请求If-None-Match / If-Modified-Since / If-Match / If-Unmodified-Since分别用于缓存校验与并发写冲突检测(乐观锁)
连接管理Connection / Keep-Alive / Upgrade连接复用与协议升级
认证Authorization / WWW-Authenticate / Proxy-AuthorizationBasic / Bearer / Digest 认证
状态Cookie / Set-Cookie会话状态承载
范围Range / Accept-Ranges / Content-Range断点续传、视频拖拽
代理链路Via / X-Forwarded-For / Forwarded(RFC 7239)记录经过的中间节点与真实客户端 IP
安全Strict-Transport-Security / Content-Security-Policy / X-Content-Type-OptionsHSTS、CSP、禁止 MIME 嗅探
CORSOriginAccess-Control-Allow-Origin / -Methods / -Headers / -Credentials跨域资源共享

HTTP/2 帧头(RFC 9113,固定 9 字节)

看这张表前先记住:HTTP/2 不再是一行行文本,而是一个个定长 9 字节头的"小包裹"。最关键的是最后那个 Stream Identifier(流 ID)——正是靠它,多个请求的碎片才能在同一条连接上交错传输、到了对面再各归各位。

字段长度含义
Length(负载长度)24 bit帧负载长度(默认上限 16384,可由 SETTINGS 调整)
Type(帧类型)8 bit帧类型:0x0DATA、0x1HEADERS、0x2PRIORITY、0x3RST_STREAM、0x4SETTINGS、0x5PUSH_PROMISE、0x6PING、0x7GOAWAY、0x8WINDOW_UPDATE、0x9CONTINUATION
Flags(标志位)8 bit标志位,如 END_STREAMEND_HEADERSACK
R(保留位)1 bit保留位,必须为 0
Stream Identifier(流标识符)31 bit流 ID。0 表示连接级控制流;客户端发起用奇数,服务端发起用偶数
Frame Payload(帧负载)可变帧负载

HTTP/2 连接以固定前导 PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n 开始,随后必须发一个 SETTINGS 帧。

交互时序

一句话看懂这张图:从查 DNS 到握手、到第一个请求、到复用同一条连接发第二个请求、到十分钟后靠 304 省流量、再到登录后的重定向带 Cookie——这就是你每天打开一个网页背后完整发生的事。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
sequenceDiagram
    participant B as 浏览器
    participant D as DNS
    participant S as Web 服务器

    B->>D: 查询 A 记录 example.com
    D-->>B: 93.184.216.34
    Note over B,S: TCP 三次握手(80 端口)
    B->>S: SYN
    S->>B: SYN+ACK
    B->>S: ACK

    Note over B,S: ── 第 1 个请求(连接复用开始)──
    B->>S: GET /index.html HTTP/1.1<br/>Host: example.com<br/>Connection: keep-alive
    S->>B: HTTP/1.1 200 OK<br/>Content-Type: text/html<br/>ETag: "v1"<br/>Cache-Control: max-age=600<br/><br/>[HTML 正文]

    Note over B,S: ── 同连接复用发第 2 个请求 ──
    B->>S: GET /app.css HTTP/1.1
    S->>B: HTTP/1.1 200 OK [CSS 正文]

    Note over B,S: ── 10 分钟后再访问:协商缓存 ──
    B->>S: GET /index.html HTTP/1.1<br/>If-None-Match: "v1"
    S->>B: HTTP/1.1 304 Not Modified<br/>(无正文,直接用本地缓存)

    Note over B,S: ── POST 提交与重定向 ──
    B->>S: POST /login HTTP/1.1<br/>Content-Type: application/x-www-form-urlencoded<br/><br/>user=alice&pass=***
    S->>B: HTTP/1.1 303 See Other<br/>Location: /dashboard<br/>Set-Cookie: sid=xyz; HttpOnly; Secure
    B->>S: GET /dashboard HTTP/1.1<br/>Cookie: sid=xyz
    S->>B: HTTP/1.1 200 OK [页面]

    Note over B,S: 空闲超时或 Connection: close
    B->>S: FIN
    S->>B: FIN+ACK

关键机制 / 变体

这是干什么的:服务器记性不好,于是把"你是谁"这张小纸条交给浏览器保管,每次来访自动带上——这就是 Cookie。

HTTP 本身不记忆客户端。Cookie(RFC 6265)通过 Set-Cookie 下发、Cookie 回带,把状态"寄存"在客户端。安全属性必须掌握:

属性作用
HttpOnly禁止 JavaScript 读取,缓解 XSS 窃取会话
Secure仅通过 HTTPS 发送
SameSite=Strict/Lax/None控制跨站请求是否携带,缓解 CSRF;None 必须同时带 Secure
Domain / Path作用域,决定哪些请求会带上
Max-Age / Expires有效期;都不设即为会话 Cookie,关浏览器即失效

2. 缓存机制(RFC 9111)

这是干什么的:能不发请求就不发(强缓存),非发不可也尽量别传正文(协商缓存),核心目标是省 RTT 和省带宽。

  • 强缓存Cache-Control: max-age=3600Expires。命中则完全不发请求,浏览器直接读本地(DevTools 显示 from disk cache)。
  • 协商缓存:强缓存过期后,带 If-None-Match: <ETag>If-Modified-Since: <日期> 询问服务器。未变更返回 304 Not Modified(无正文,省带宽),变更则返回 200 + 新内容。
  • Cache-Control 关键指令no-cache(可缓存但每次必须校验)、no-store(禁止任何存储,敏感数据用)、private(仅浏览器可存,CDN 不可)、publicmust-revalidateimmutables-maxage(仅对共享缓存生效)。
  • Vary:告诉缓存"响应内容随这些请求头变化",如 Vary: Accept-Encoding, Accept-Language。漏配会导致给英文用户返回中文缓存。

易错点:no-cache 不是“不缓存”,而是"缓存但每次都要问";真正的不缓存是 no-store

3. 持久连接与分块传输

这是干什么的:一是别每次都重新拨号(持久连接),二是让"还没写完就能先发"成为可能(分块传输)。

  • 持久连接(Persistent Connection):HTTP/1.1 默认开启,避免每个资源都重做 TCP 三次握手与慢启动。
  • 分块传输编码(Chunked Transfer Encoding):当响应长度事先未知(如流式生成),用 Transfer-Encoding: chunked 替代 Content-Length,正文形如 1a\r\n<26字节数据>\r\n0\r\n\r\n,以长度为 0 的块结尾。
  • 请求走私(Request Smuggling)风险:当 Content-LengthTransfer-Encoding 同时存在且前后端解析优先级不同,攻击者可构造出被"劈开"的请求。RFC 9112 明确要求:两者共存时必须以 Transfer-Encoding 为准并视为可疑,代理应拒绝该请求。

4. HTTP/2 的多路复用

这是干什么的:把"一条路一次只能过一辆车"改成"一条路划出很多车道,多辆车同时跑",从而干掉应用层的排队。

HTTP/2 把报文拆成二进制帧,同一 TCP 连接上并行承载多个流(Stream),帧头的 Stream ID 标识归属,接收端按 ID 重组。这解决了 HTTP 层的队头阻塞。此外:

  • HPACK 首部压缩(RFC 7541):静态表 + 动态表 + 霍夫曼编码,把重复的 User-AgentCookie 压到几个字节。
  • 流量控制:连接级与流级各有一个窗口,通过 WINDOW_UPDATE 帧调整。
  • 优先级:原 PRIORITY 帧机制复杂且效果不佳,RFC 9218 提出了更简单的 Priority 首部方案。

但 TCP 层的队头阻塞依然存在:一个 TCP 段丢失,内核必须等它重传才能把后续数据交给上层,此时所有流一起卡住——这正是 HTTP/3 改用 QUIC 的根本原因。QUIC 在 UDP 之上为每个流独立维护可靠性,一个流丢包不影响其他流。

5. 内容协商与压缩

这是干什么的:让同一个网址能按客户端偏好返回不同"版本"(语言、编码、压缩格式),同时告诉缓存该按哪些条件区分。

客户端 Accept-Encoding: gzip, br, zstd → 服务端选一种并回 Content-Encoding: br + Vary: Accept-Encoding。注意 Content-Length 指的是压缩后的字节数

常见误区

  • 误区一:no-cache 就是不缓存。 它的意思是"可以缓存,但每次用前必须向服务器校验";真正禁止存储的是 no-store
  • 误区二:GET 不能带请求体。 规范允许带,只是语义未定义,多数实现会忽略。GET 与 POST 的真正区别在语义(安全、幂等)和参数位置,而不是"能不能带 body"。
  • 误区三:301/302 重定向会原样保留方法和请求体。 301/302 在历史实现中会把 POST 改写为 GET;只有 307/308 严格保留原方法与请求体。API 场景应优先 307/308。
  • 误区四:HTTP/2 彻底解决了队头阻塞。 它只解决了 HTTP 层的。TCP 层的队头阻塞仍在——一个 TCP 段丢失,所有流一起等重传。彻底解决要靠 HTTP/3 的 QUIC。
  • 误区五:状态行里的"原因短语"有实际作用。 原因短语仅供人读,客户端不得依赖其内容,真正起作用的是三位数字状态码。

速记口诀

一行起始两行头,空行之下才是肉;请求说"做什么",响应答"怎么样"。

一信息二成功三跳转,四是你错五是我错。 易混三对记牢:301/308 看方法,401/403 分"是谁"和"能不能",502/504 辨"坏响应"和"没响应"。

no-cache 要问一句,no-store 一字不留;强缓存省来回,协商缓存省流量。

知识框架

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
mindmap
  root((HTTP))
    报文结构
      起始行 请求行 状态行
      首部字段
      空行 CRLF
      消息正文
    请求方法
      安全方法 GET HEAD OPTIONS TRACE
      幂等方法 GET HEAD PUT DELETE OPTIONS TRACE
      非幂等 POST PATCH
      隧道 CONNECT
    状态码
      1xx 信息 100 101 103
      2xx 成功 200 201 204 206
      3xx 重定向 301 302 303 304 307 308
      4xx 客户端 400 401 403 404 405 409 429
      5xx 服务端 500 501 502 503 504
    核心首部
      Host 虚拟主机
      Content-Type Length Encoding
      Cache-Control ETag Vary
      Cookie Set-Cookie
      Authorization
      Range Accept-Ranges
      CORS 系列
    连接管理
      持久连接 keep-alive
      分块传输 chunked
      协议升级 Upgrade
      请求走私风险
    缓存体系
      强缓存 max-age Expires
      协商缓存 ETag Last-Modified
      304 Not Modified
      共享缓存 s-maxage private
    版本演进
      HTTP0.9 单行GET
      HTTP1.0 首部与状态码
      HTTP1.1 RFC9112 持久连接Host
      HTTP2 RFC9113 二进制分帧HPACK
      HTTP3 RFC9114 QUIC QPACK
    无状态与会话
      Cookie RFC6265
      HttpOnly Secure SameSite
      Token 与 Session