HTTP 协议
HTTP 协议
一次 curl -v 看到的协议细节
执行 curl -v https://www.example.com:
* Trying 93.184.216.34:443...
* Connected to www.example.com (93.184.216.34) port 443
* TLS 1.3 handshake (client) ...
* Server certificate: subject=...
* using HTTP/2
* h2h3 [:method: GET]
> GET / HTTP/2
> Host: www.example.com
> User-Agent: curl/8.0.0
> Accept: */*
>
< HTTP/2 200
< server: ECS (sec/9740)
< content-type: text/html; charset=UTF-8
< content-length: 1256
< date: Sun, 29 Jun 2026 ...
<
<!doctype html>
<html>...</html>短短几行输出,背后是 TCP 三次握手、TLS 1.3 握手、HTTP/2 请求、服务器响应、连接关闭的全过程。HTTP 协议定义的就是这些 > < 开头的文本行。
HTTP 是 Web 的根基。1991 年 Tim Berners-Lee 写 HTTP/0.9 时,Web 只是 CERN 内部的超文本分享工具。33 年后,HTTP 承载了全球 80% 的互联网流量。这一章用抓包和 curl 把 HTTP 协议讲透。
问题一:HTTP 是怎么演化的
| 版本 | 年份 | 关键特性 |
|---|---|---|
| HTTP/0.9 | 1991 | 单行 GET,纯 HTML,无 header |
| HTTP/1.0 | 1996 (RFC 1945) | 增加 header、状态码、POST 方法 |
| HTTP/1.1 | 1997 (RFC 2068, 2616, 9110) | Keep-Alive、chunked、虚拟主机 |
| HTTP/2 | 2015 (RFC 7540) | 二进制分帧、多路复用、HPACK 压缩 |
| HTTP/3 | 2022 (RFC 9114) | 基于 QUIC,连接迁移、0-RTT |
HTTP/0.9 的全部协议:
GET /index.html
<html>...就这一行。客户端发 GET /index.html\n,服务器回 HTML 文本(无状态行、无 header、连接立即关闭)。CERN 内部用用够了,没考虑过互联网规模。
HTTP/1.0 革命:1996 年发布的 RFC 1945 引入三件事:
- 状态行(
HTTP/1.0 200 OK) - 请求/响应 header(User-Agent, Content-Type, ...)
- POST、HEAD 方法
浏览器和服务器之间开始能传元数据,Web 终于像个"协议"了。
HTTP/1.1 持续连接:HTTP/1.0 每请求一个资源都建一个新 TCP 连接。访问一个有 50 个图片的网页要建 50 个 TCP 连接,慢且浪费。HTTP/1.1 默认 Keep-Alive,一个 TCP 连接可以串行处理多个请求。这个改进看似小,实际把网页加载速度提升 2-3 倍。
HTTP/2 革命:2015 年 Google 主导的 HTTP/2 把文本协议改成二进制,所有请求/响应在同一个 TCP 连接上多路复用。一个网页的 50 个资源请求同时发,不再排队。
HTTP/3 革命:2022 年基于 QUIC(UDP 之上的可靠协议)的 HTTP/3 解决了 HTTP/2 的"队头阻塞"问题——HTTP/2 多路复用是应用层多路,但 TCP 丢一个包要重传后面的包(队头阻塞)。QUIC 是 UDP 之上的可靠多路,单个包丢失不影响其他流。
问题二:HTTP 请求和响应的真实结构
抓一次 HTTP/1.1 请求:
curl -v --http1.1 http://www.example.com 2>&1 | grep -E "^[><]"输出:
> GET / HTTP/1.1
> Host: www.example.com
> User-Agent: curl/8.0.0
> Accept: */*
>
< HTTP/1.1 200 OK
< Content-Type: text/html; charset=UTF-8
< Content-Length: 1256
< Date: Sun, 29 Jun 2026 12:00:00 GMT
< Connection: keep-alive
< Server: ECS (sec/9740)
<
<!doctype html>...请求结构:
<method> <URI> <HTTP-version>\r\n
<header1>: <value1>\r\n
<header2>: <value2>\r\n
...
\r\n
<body>注意空行(CRLF 单独一行)分隔 header 和 body。没有空行就没有 body——这是 HTTP 协议的关键约定。
响应结构:
<HTTP-version> <status-code> <reason-phrase>\r\n
<header1>: <value1>\r\n
...
\r\n
<body>抓包看 HTTP/2 是二进制的:
tcpdump -ni eth0 -X 'tcp port 443' -w /tmp/http2.pcap
tshark -r /tmp/http2.pcap -V 'http2'HTTP/2 的帧结构:4 字节长度 + 1 字节类型 + 1 字节标志 + 4 字节流 ID + payload。流 ID 是 HTTP/2 多路复用的关键——同一连接上的多个请求用不同流 ID 区分。
问题三:HTTP 方法的本质
| 方法 | 幂等 | 安全 | 用途 | 例子 |
|---|---|---|---|---|
| GET | 是 | 是 | 获取资源 | GET /users/123 |
| HEAD | 是 | 是 | 只取 header | HEAD /file.zip(看文件大小) |
| POST | 否 | 否 | 创建资源 | POST /users(提交表单) |
| PUT | 是 | 否 | 全量替换 | PUT /users/123(替换用户) |
| PATCH | 否 | 否 | 部分更新 | PATCH /users/123 {name: "..."} |
| DELETE | 是 | 否 | 删除 | DELETE /users/123 |
| OPTIONS | 是 | 是 | 看支持方法 | OPTIONS *(CORS 预检) |
| CONNECT | 否 | 否 | 建立隧道 | CONNECT example.com:443(HTTP 代理) |
| TRACE | 是 | 是 | 回显请求 | 调试用,几乎不用 |
幂等性(Idempotent):多次执行结果一致。PUT /users/123 重复 5 次和 1 次结果一样。POST /orders 重复 5 次会创建 5 个订单。幂等方法可以安全重试——网络抖动时浏览器可以自动重发 GET/PUT/DELETE,不用担心副作用。
安全性(Safe):执行不改变服务器状态。GET/HEAD/OPTIONS 是安全的。POST/PUT/DELETE 都不安全。CDN 缓存会主动缓存安全方法的结果。
# 测试 OPTIONS 看服务器支持的方法
curl -X OPTIONS -i http://www.example.com
# 看 WebDAV 扩展方法
curl -X PROPFIND http://www.example.com # WebDAV问题四:HTTP 状态码的实际意义
| 状态码 | 类别 | 含义 | 常见例子 |
|---|---|---|---|
| 1xx | 信息 | 临时响应 | 100 Continue(请求体大,客户端先发 header 试探) |
| 2xx | 成功 | 请求正常处理 | 200 OK, 204 No Content, 206 Partial Content(断点续传) |
| 3xx | 重定向 | 资源位置变了 | 301 永久重定向, 302 临时, 304 缓存有效, 307 保留方法 |
| 4xx | 客户端错 | 请求有问题 | 400 语法错, 401 未认证, 403 禁止, 404 找不到, 429 太频繁 |
| 5xx | 服务器错 | 服务器处理不了 | 500 内部错, 502 上游错, 503 暂时过载, 504 超时 |
301 vs 302 的关键区别:
- 301 Moved Permanently:浏览器收到后会永久记住新 URL,下次直接访问新 URL(不发请求给旧 URL)
- 302 Found:临时重定向,浏览器每次都先访问旧 URL,由服务器再 302 一次
历史问题:HTTP/1.0 的 302 规范没说浏览器重定向时是否保留原方法。浏览器实现不一致——有些把 POST 改成 GET,有些保留 POST。HTTP/1.1 引入 303 See Other(强制改成 GET)和 307 Temporary Redirect(保留方法)解决这个歧义。
304 Not Modified:
客户端 GET 一个资源时带 If-Modified-Since: Sun, 29 Jun 2026 12:00:00 GMT。服务器检查资源,如果这个时间之后没改过,返回 304 Not Modified 不带 body。客户端就用本地缓存,节省带宽。
5xx 错误排查:
# 502 Bad Gateway:Nginx 上游服务挂了
# 503 Service Unavailable:服务器过载
# 504 Gateway Timeout:上游服务超时curl -I 只看 header:
curl -I https://www.example.com
# HTTP/2 200
# server: ECS (sec/9740)
# ...问题五:HTTP 头部的关键字段
请求头:
| 头 | 作用 | 例子 |
|---|---|---|
| Host | 虚拟主机标识(同一 IP 多个网站) | Host: www.example.com |
| User-Agent | 客户端标识 | User-Agent: Mozilla/5.0 ... |
| Accept | 客户端能接受的内容类型 | Accept: text/html, application/json |
| Accept-Encoding | 客户端支持的压缩算法 | Accept-Encoding: gzip, br |
| Authorization | 认证凭据 | Authorization: Bearer eyJhbGc... |
| Cookie | 客户端 cookie | Cookie: session_id=abc123 |
| Referer | 来源页面 URL | Referer: https://www.google.com/ |
| If-None-Match | ETag 缓存验证 | If-None-Match: "abc123" |
| If-Modified-Since | 时间缓存验证 | If-Modified-Since: Sun, 29 Jun 2026 ... |
响应头:
| 头 | 作用 | 例子 |
|---|---|---|
| Content-Type | 响应体 MIME 类型 | Content-Type: text/html; charset=utf-8 |
| Content-Length | 响应体字节数 | Content-Length: 1256 |
| Content-Encoding | 实际压缩算法 | Content-Encoding: gzip |
| Cache-Control | 缓存指令 | Cache-Control: max-age=3600, public |
| Set-Cookie | 服务器设置 cookie | Set-Cookie: id=abc; HttpOnly; Secure |
| Location | 重定向目标 | Location: https://www.example.com/new |
| ETag | 资源版本标识 | ETag: "abc123" |
| Server | 服务器软件标识 | Server: nginx/1.25.0 |
| X-Frame-Options | 防点击劫持 | X-Frame-Options: DENY |
| Strict-Transport-Security | 强制 HTTPS | Strict-Transport-Security: max-age=31536000 |
Content-Length vs Transfer-Encoding: chunked:
HTTP/1.0 必须 Content-Length 知道响应体长度才能解析。HTTP/1.1 增加 chunked 编码——响应体分成多个 chunk,每个 chunk 前面有 16 进制长度,不需要预先知道总长度。流式响应必须用 chunked。
# 强制返回 chunked
curl -H 'TE: chunked' http://example.com问题六:Cookie 和 Session 的本质
HTTP 是无状态协议——服务器处理完请求就忘了客户端。Cookie + Session 是给 HTTP 加"状态"的方案。
Cookie 工作机制:
- 客户端第一次访问,服务器响应
Set-Cookie: session_id=abc123; HttpOnly; Secure - 浏览器把 cookie 保存到本地(按 domain 分类)
- 后续每次访问同域,浏览器自动在请求头加
Cookie: session_id=abc123 - 服务器收到 cookie,查到 session_id 对应的会话状态
关键 Cookie 属性:
| 属性 | 作用 |
|---|---|
| Domain | cookie 作用域名 |
| Path | cookie 作用路径 |
| Expires/Max-Age | 过期时间 |
| HttpOnly | 禁止 JS 访问,防 XSS 窃取 |
| Secure | 仅 HTTPS 传输 |
| SameSite | 跨站请求控制(防 CSRF) |
Session 的存储位置:
- 服务端 Session:服务器内存或数据库存 session_id → 用户信息
- JWT(JSON Web Token):用户信息编码在 token 里(签名防伪造),服务器无状态
# 看服务器返回的 cookie
curl -i http://www.example.com | grep -i set-cookie
# 带 cookie 请求
curl -b 'session_id=abc123' http://www.example.com
# 删除 cookie
curl -c - http://www.example.com问题七:HTTP 缓存机制
浏览器和中间 CDN 会缓存 HTTP 响应。正确使用缓存能减少 60-90% 的服务器请求。
Cache-Control 指令:
| 指令 | 含义 |
|---|---|
max-age=3600 | 缓存 3600 秒 |
no-cache | 必须重新验证(用 ETag/If-Modified-Since) |
no-store | 禁止任何缓存 |
public | 任何缓存都可存(CDN、代理) |
private | 仅浏览器缓存,不存 CDN |
s-maxage=3600 | CDN 缓存时间,覆盖 max-age |
must-revalidate | 缓存过期后必须重新验证 |
强缓存 vs 协商缓存:
- 强缓存:资源在
max-age内,浏览器不发请求直接用缓存 - 协商缓存:过期后浏览器带
If-None-Match(ETag)或If-Modified-Since问服务器,服务器返回 304 或新资源
ETag 是服务器给资源的"指纹"(hash 或版本号),资源变了 ETag 变。If-None-Match 让服务器对比指纹,没变返回 304。
# 看 ETag 流程
curl -I http://www.example.com # 第一次:ETag: "abc"
curl -H 'If-None-Match: "abc"' -I http://www.example.com # 第二次:304 Not Modified问题八:HTTP/2 的核心改进
HTTP/1.1 的关键限制:同一连接上的请求必须串行。浏览器为了并行,对每个域名开 6 个 TCP 连接(HTTP/1.1 浏览器默认行为)。一个网页 100 个资源需要 100/6 ≈ 17 个串行批次。
HTTP/2 多路复用:所有请求在同一 TCP 连接上同时传输。浏览器不再需要 6 个连接,1 个就够。100 个资源一次并发。
HTTP/2 二进制分帧:
HTTP/2 二进制分帧格式:
图注:Length 3 字节表示帧体长度,Type 区分 HEADERS/DATA/SETTINGS 等帧类型,Stream ID 标识该帧属于哪个 HTTP 请求/响应(多路复用核心)。
每个 HTTP 请求/响应是一个帧(Frame),帧有流 ID(Stream ID)标识属于哪个请求。多个帧在同一 TCP 连接上交错传输。
HPACK 头部压缩:HTTP 头部经常重复(如 User-Agent 每次都一样)。HPACK 用 Huffman 编码 + 共享表,头部平均压缩 30%。
服务器推送(Server Push):服务器主动推送关联资源(如 HTML 关联的 CSS/JS),无需客户端请求。Chrome 2019 年后默认禁用这个特性,因为实际效果不好(推送的内容客户端可能已有缓存)。
HTTP/2 的局限:
HTTP/2 多路复用是应用层多路,底层还是 TCP。TCP 队头阻塞:一个 TCP 包丢了,TCP 要重传,后续包都得等。HTTP/2 的多流在 TCP 看来还是一个流。HTTP/3 基于 QUIC(UDP 上的可靠多路)解决了这个问题。
问题九:HTTP/3 和 QUIC
HTTP/3(RFC 9114)2022 年标准化,底层是 QUIC 协议(RFC 9000)。
QUIC 关键创新:
- 基于 UDP:不依赖 TCP,内核外实现(用户态),迭代快
- 连接 ID 代替四元组:客户端换 WiFi 到 4G,IP 变了,连接不中断
- 多路独立流:每个 HTTP 请求一个独立的 QUIC 流,单流丢包不影响其他流
- 集成 TLS 1.3:QUIC 包自带 TLS,握手在 QUIC 层完成
HTTP/3 vs HTTP/2 性能对比:
- 高丢包网络(5%+)下 HTTP/3 比 HTTP/2 快 30%+
- 弱网下 HTTP/3 的 0-RTT 握手省一次 RTT
- 移动场景(WiFi ↔ 4G 切换)HTTP/3 保持连接
# 用 curl 强制走 HTTP/3
curl --http3-only -v https://www.cloudflare.com
# 看是否支持 HTTP/3
curl --http2-prior-knowledge https://example.com实战命令
# 看完整 HTTP 过程
curl -v https://www.example.com
# 模拟慢速请求
curl -w "time_total: %{time_total}\n" -o /dev/null -s https://www.example.com
# 看响应时间各阶段
curl -w "@curl-format.txt" -o /dev/null -s https://www.example.com
# curl-format.txt:
# time_namelookup: %{time_namelookup}\n
# time_connect: %{time_connect}\n
# time_appconnect: %{time_appconnect}\n
# time_pretransfer: %{time_pretransfer}\n
# time_redirect: %{time_redirect}\n
# time_starttransfer: %{time_starttransfer}\n
# time_total: %{time_total}\n
# 抓 HTTP 报文
tcpdump -ni eth0 -X 'tcp port 80' -w /tmp/http.pcap
tshark -r /tmp/http.pcap -V 'http'
# 看 HTTP/2 帧
tshark -r /tmp/http2.pcap -V 'http2.headers'
# 模拟不同 User-Agent
curl -A "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0)" https://www.example.com
# 测 HTTPS 握手时间
curl -w "ssl_time: %{time_appconnect}\n" -o /dev/null -s https://www.example.com思考题
- HTTP/1.1 浏览器对每个域名默认开 6 个 TCP 连接。为什么是 6?服务器能识别浏览器开了 6 个连接吗?网页有 100 个图片资源,HTTP/1.1 要多久?HTTP/2 多久?
- 抓一次
curl -v http://httpbin.org/get,看请求和响应的所有 header。这些 header 哪些是标准?哪些是X-开头的自定义头?这些自定义头为什么存在? - 假设你的 API 接收 POST 请求,前端表单重复提交 3 次(用户多点了一下)。后端怎么处理可以避免重复创建订单?PUT 和 POST 在幂等性上有什么区别?
- Cookie 的
SameSite=Lax和SameSite=Strict有什么区别?为什么现代浏览器默认SameSite=Lax?CSRF 攻击利用 Cookie 的什么特性? - CDN 是怎么利用 HTTP 缓存机制工作的?
Cache-Control: s-maxage和max-age的区别是什么?登录态(Cookie)请求和静态资源请求在 CDN 缓存策略上有什么不同? - 抓一次
curl --http2-prior-knowledge和curl --http3-only的请求,比较两者的时间线。HTTP/3 的 0-RTT 体现在哪个包?连接迁移在 QUIC 里怎么实现(看 connection ID 字段)?
延伸阅读
- RFC 1945: HTTP/1.0
- RFC 9110: HTTP Semantics (HTTP/1.1 现行规范)
- RFC 7540 / RFC 9113: HTTP/2
- RFC 9114: HTTP/3
- RFC 9000: QUIC
- 《HTTP 权威指南》(David Gourley)O'Reilly 经典
- 《Web 性能权威指南》(Ilya Grigorik)O'Reilly
- 动手实验:用
mitmproxy或Charles抓 HTTPS 流量并解密,分析每个请求/响应的 header