很多人学 HTTP,是从 fetch('https://rubyfun.cn/api') 开始的。能用,能调通,能拿到 JSON,就觉得自己「会 HTTP」了。这个认知停在接口层,一旦遇到下面这些问题就会卡住:为什么有的接口明明是 GET 却改了数据?为什么缓存有时候生效有时候不生效?为什么 HTTP/2 说自己更快,但抓包看到的还是一串文本?
这些问题在「会调接口」层面找不到答案,必须退一步,看清 HTTP 这套协议的骨架长什么样。HTTP 不是一个「发请求的工具」,而是一套在不可靠网络上做可靠信息交换的应用层约定。它的可靠性不靠魔法,全靠三个东西撑起来:一套全球唯一的地址(URL)、一种严格的报文格式(三段式)、一组表达意图的语义对偶(method 和 status code)。
这一篇先把这三块骨架立起来。这是整个「图解 HTTP」系列的起点,后面讲连接、缓存、安全、HTTP/2 演进时,所有机制都站在这套骨架上。
先把 URL 的「解剖图」画出来
一个 URL 看起来就是一串字符串,但它其实是一个有严格结构的定位器。RFC 3986 把它定义成几个固定组成部分,每一部分都有明确的职责:
上面这张图按真实地址 https://rubyfun.cn:443/tech/http?q=缓存#top 拆开。几个容易被忽略的点值得记住:
- scheme(
https)不只是协议名,它决定了后面用哪套传输约定。https意味着要先建立 TLS 通道再发 HTTP 报文;如果是http就是明文。同一个域名,换 scheme 等于换了一条传输链路。 - authority(
rubyfun.cn:443)里藏着 userinfo、host、port 三段。port 省略时,https默认 443、http默认 80,但这个默认值是 scheme 决定的,不是 URL 自带的。userinfo(user:pass@)在现代基本废弃——把密码写进 URL 会泄露到日志、浏览器历史、Referer 里,RFC 3986 也不鼓励,见到就该警惕。 - query(
q=缓存)和 fragment(#top)的归属完全不同:query 会随请求发给服务器,fragment 永远不会发给服务器,它只在浏览器本地用来定位页面锚点。这是很多人第一次抓包时困惑的来源——地址栏里明明带着#top,抓包却看不到。
这里要澄清一个长期被混用的概念:URI、URL、URN 不是一回事。URI(统一资源标识符)是总称,URL(统一资源定位符)是「靠地址定位」,URN(统一资源名称)是「靠名字定位」。我们日常说的「网址」基本都是 URL。URN 的例子像 urn:isbn:978-7-111-XXXX-X,它告诉你「这是一本书」,但不告诉你去哪找。理解这个区分,能解释为什么 RFC 标题写的是 URI,而 HTTP 报文里用的全是 URL——HTTP 关心的是「怎么找到资源」,这是 URL 的活。
报文三段式:为什么不能像 JSON 那样自由
URL 解决了「找谁」,报文解决「说什么、怎么说」。一个 HTTP 报文长得非常朴素,就是三段:
- 起始行(start-line):一行,说明这次要干什么。
- 头部(header):若干行
字段名: 值,描述这次交互的元信息。 - 主体(body):可选,真正要传的数据。
请求报文和响应报文都遵循这三段式,只是起始行的内容不同:
一个真实的请求报文起始行长这样:GET /tech/http HTTP/1.1——method、路径、协议版本,三个 token 用空格隔开。响应报文的起始行叫状态行:HTTP/1.1 200 OK——协议版本、状态码、原因短语。
为什么报文要严格三段式,而不是像 JSON 那样自由嵌套?因为 HTTP 设计在 1991 年,那个年代的核心约束是解析器要足够简单、足够鲁棒。一个 TCP 字节流过来了,解析器需要逐行扫描:先读第一行拿到意图,再按行读 header 直到遇到一个空行,剩下的就是 body。这种「起始行 + 头部 + 空行 + 主体」的结构,让解析可以用最朴素的逐行扫描实现,不需要复杂的语法树。JSON 式的自由嵌套会要求一个完整的解析器,在那个年代是不可接受的复杂度。
这个历史约束今天还在影响我们:header 必须一行一个、用 \r\n 结尾、整个 header 区用一个空行(\r\n\r\n)和 body 分隔。如果哪天你手写 HTTP 报文(比如做协议调试、写一个最简 HTTP server),忘了那个空行,对端会一直等下去——因为它不知道 header 结束了。这个坑,几乎每个写过底层网络代码的人都踩过。
method 和 status:一套表达意图的「语义对偶」
光有报文格式还不够,HTTP 真正的精髓在于它给 method 和 status code 赋予了语义。这不只是「动词 + 数字」,而是一套契约。
先看 method。RFC 9110 把常用的几个 method 定义得非常清楚,而且给每个 method 标注了两个关键属性:安全(safe)和幂等(idempotent)。
| method | 安全 | 幂等 | 语义 |
|---|---|---|---|
| GET | 是 | 是 | 获取资源,不应改变服务器状态 |
| HEAD | 是 | 是 | 只取响应头,不要 body |
| POST | 否 | 否 | 提交数据,通常创建新资源 |
| PUT | 否 | 是 | 用请求 body 替换目标资源 |
| DELETE | 否 | 是 | 删除目标资源 |
这两个属性不是为了考试,它们直接影响工程决策。幂等的意思是「同一个请求发一次和发十次,服务器的最终状态一样」。这有什么用?用处大了:网络请求可能因为超时而失败,但请求其实已经到了服务器。这时候客户端该不该重试?如果 method 是幂等的(GET、PUT、DELETE),重试是安全的;如果是 POST,重试可能导致重复创建。
这就回答了一个常见困惑:为什么有些团队坚持把「扣款」这种操作设计成 PUT /orders/123/status 而不是 POST /pay?因为 PUT 的幂等语义让重试变得安全——网络抖动重发三次,订单状态最终都是「已支付」,不会扣三次款。这是把协议语义当设计工具用,而不是把 HTTP method 当成 CRUD 的别名。
再看状态码。RFC 9110 把状态码分成五大类,第一位数字代表类别:
- 1xx 信息性:少见,比如
100 Continue,客户端可以先发头部探探路。 - 2xx 成功:
200 OK最常见;201 Created配合Location头告诉客户端新资源在哪;204 No Content表示成功但没有 body 要返回。 - 3xx 重定向:
301 永久重定向、302 临时重定向、304 Not Modified(配合缓存,表示「你本地的副本还能用」)。 - 4xx 客户端错误:
400 Bad Request、401 Unauthorized(没认证)、403 Forbidden(认证了但没权限)、404 Not Found、429 Too Many Requests(限流)。 - 5xx 服务端错误:
500 Internal Server Error、502 Bad Gateway、503 Service Unavailable。
这里有个反直觉的点值得深究:状态码是给机器看的,原因短语(OK、Not Found)是给人看的。RFC 明确说原因短语是「给人类阅读的,客户端不应该靠它做判断」。所以你完全可以让服务器返回 HTTP/1.1 200 一切正常,客户端只要认 200 这个数字就行。反过来,如果你的代码去匹配字符串 "OK" 来判断成功,换一个服务器实现就可能挂掉。这条原则背后是一个工程判断:协议层的契约用数字编码,展示层的文案留给人——两者解耦,才不会被文案改动牵着走。
取舍与边界
这套骨架看起来稳定,但它的每个选择都有代价。
- URL 的明文历史:早期 URL 设计时没考虑隐私,scheme、host、query 默认明文。HTTPS 通过对整条报文加密缓解了这个问题,但 URL 本身(尤其是 query)在浏览器历史、服务器日志、Referer 头里仍可能泄露。所以敏感参数(token、密码)不该放在 URL 里,要放 body。
- 报文格式的「逐行扫描」换来简单,也限制了表达力。header 一行一个、用 ASCII 文本,导致传二进制数据要先编码(base64、chunked),传结构化数据要靠 body 自己用 JSON。这也是后来 HTTP/2 引入二进制帧、HTTP/3 进一步重构的动因——但那是后话。
- method/status 的语义是「约定」不是「强制」。服务器完全可以给
GET接口实现成「会删数据」,协议拦不住。但一旦你这么干,缓存、CDN、重试逻辑全会出问题——因为它们都按 GET 的「安全、幂等」语义在工作。协议提供的是默认契约,破坏契约的代价由破坏者承担。
这条最后值得单独记一笔:HTTP 的所有机制——缓存假设 GET 安全、重试假设 DELETE 幂等、CDN 假设 304 可以复用——全都建立在「大家都遵守语义契约」之上。这套协议能跑三十多年还在用,不是因为技术多先进,而是因为它把最关键的意图编码进了 method 和 status,让无数中间件可以按统一契约工作。
收束:先有骨架,再谈优化
一个 URL,一段报文,一组语义对偶——这就是 HTTP 的全部骨架。先看清楚 URL 怎么拆、报文怎么三段式、method 和 status 各自带了什么契约,后面讲连接复用、缓存策略、TLS 握手和 HTTP/2 演进时,才知道每一个机制分别站在骨架的哪个位置。
这也是这一整套「图解 HTTP」的起点。后续会沿着连接与传输、缓存与协商、内容与编码、安全与边界、HTTP/2 与 HTTP/3 这条线,逐篇把机制拆开。下一篇讲 header——它不是参数表,而是一张请求的元信息契约。
关于十三Tech
我是十三,All in AI Agent 方向的架构师,专注 AI 工程实践。
我相信 AI 是程序员的最佳搭档,也希望帮助每一位开发者更好地驾驭 AI。
如果你想继续跟完这套「图解 HTTP」,欢迎关注公众号 「十三Tech」。后续会按 URL 与报文、连接与传输、缓存与协商、安全与边界、HTTP/2 与 HTTP/3 这条线更新。

