HTTP 原理与报文 — 报文结构、方法、状态码与版本演进
HTTP 请求/响应报文格式、方法与状态码全表、关键首部、持久连接与 HTTP/2 分帧 / 应用层 / TCP 80 / RFC 9110
先建立直觉
HTTP 说白了就是一问一答:你说清楚"我想对哪个东西做什么",对方回一句"办成了 / 没办成,原因是这个",顺便把东西给你。
它之所以能撑起整个 Web,靠的是把"提问"和"回答"都标准化成了固定套路——谁都能读懂,谁都能转发,中间还能随手加个缓存、加个代理。而且它记性不好,办完一件事就把你忘了,所以你每次来都得自报家门;这个"缺点"恰恰让服务器能随便加机器扛流量。
它解决什么问题
设想一个没有 HTTP 的世界:想下载文件学一套协议,想看新闻学另一套,想查目录再学第三套。每加一种服务,客户端就得多学一门"外语"。
HTTP 做的事情是把千奇百怪的需求压缩成一套通用问句:
- **“东西在哪”**统一用一个地址串表达,不管它是网页、图片还是接口。
- **“你想干什么”**统一收敛成几个动词——取、提交、替换、删除,服务端一看就懂。
- **“结果怎样”**统一用一个三位数字回答,客户端不用理解业务也知道该重试、该跳转还是该报错。
- “还有什么要交代的”(我能接受什么语言、这份内容能存多久、我是谁)全塞进可自由扩展的"备注栏"里,以后要加新能力也不用改协议本身。
所以后来的 CORS、HSTS、压缩、断点续传,全都是往备注栏里加字段加出来的——协议核心二十年基本没动。
工作流程(简化版)
一次完整的 HTTP/1.1 交互经历五步:
- 拆地址:把你输入的网址拆成"哪台机器、哪个端口、要哪个路径、带什么参数"。
- 找到人并接上线:先问 DNS 要 IP,再和对方建立 TCP 连接(HTTPS 还要多做一次 TLS 握手)。
- 把问题写清楚发过去:按固定格式写好请求行、备注栏和正文,一股脑写进连接里。
- 等对方回话:服务器处理完,回一个状态行 + 备注栏 + 正文。
- 线先别挂:HTTP/1.1 默认保持连接,下一个请求接着用同一条线;除非对方说要关或者闲太久了。
展开细节(原理原文)
- 解析 URL:
https://api.example.com:443/v1/users?id=7拆成 scheme、host、port、path、query。 - DNS 解析 + 建立连接:域名 → IP → TCP 三次握手(HTTPS 再加 TLS 握手)。
- 发送请求报文:客户端把方法、目标、首部、正文按 ASCII 文本格式串行写入 TCP 流。
- 服务器处理并回送响应报文:状态行 + 首部 + 正文。
- 连接复用或关闭: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 报文,无论请求还是响应,都是这个结构。
请求示例
响应示例
| |
起始行字段
看这张表前先记住:起始行就是报文的第一行,也是唯一一行"不带冒号"的内容。请求的第一行说"我要干什么",响应的第一行说"办得怎么样"。
| 报文类型 | 格式 | 说明 |
|---|---|---|
| 请求行 | 方法 请求目标 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 Implemented、502 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 就看代理链路族。
| 分类 | 字段 | 作用 |
|---|---|---|
| 必需 | Host | HTTP/1.1 强制,虚拟主机分流依据;缺失服务器必须回 400 |
| 正文描述 | Content-Type / Content-Length / Content-Encoding / Transfer-Encoding: chunked | 正文的类型、长度、压缩方式、分块传输 |
| 内容协商 | Accept / Accept-Language / Accept-Encoding ↔ Vary | 客户端偏好 ↔ 服务端声明"响应随哪些请求头变化"(缓存键的关键) |
| 缓存 | 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-Authorization | Basic / 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-Options | HSTS、CSP、禁止 MIME 嗅探 |
| CORS | Origin ↔ Access-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_STREAM、END_HEADERS、ACK |
| 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. 无状态与 Cookie
这是干什么的:服务器记性不好,于是把"你是谁"这张小纸条交给浏览器保管,每次来访自动带上——这就是 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=3600或Expires。命中则完全不发请求,浏览器直接读本地(DevTools 显示from disk cache)。 - 协商缓存:强缓存过期后,带
If-None-Match: <ETag>或If-Modified-Since: <日期>询问服务器。未变更返回 304 Not Modified(无正文,省带宽),变更则返回 200 + 新内容。 Cache-Control关键指令:no-cache(可缓存但每次必须校验)、no-store(禁止任何存储,敏感数据用)、private(仅浏览器可存,CDN 不可)、public、must-revalidate、immutable、s-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-Length与Transfer-Encoding同时存在且前后端解析优先级不同,攻击者可构造出被"劈开"的请求。RFC 9112 明确要求:两者共存时必须以Transfer-Encoding为准并视为可疑,代理应拒绝该请求。
4. HTTP/2 的多路复用
这是干什么的:把"一条路一次只能过一辆车"改成"一条路划出很多车道,多辆车同时跑",从而干掉应用层的排队。
HTTP/2 把报文拆成二进制帧,同一 TCP 连接上并行承载多个流(Stream),帧头的 Stream ID 标识归属,接收端按 ID 重组。这解决了 HTTP 层的队头阻塞。此外:
- HPACK 首部压缩(RFC 7541):静态表 + 动态表 + 霍夫曼编码,把重复的
User-Agent、Cookie压到几个字节。 - 流量控制:连接级与流级各有一个窗口,通过 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 一字不留;强缓存省来回,协商缓存省流量。
知识框架
| |