你在浏览器地址栏里输入一个网址,按下回车,几百毫秒后页面就出现了。看起来像是浏览器去服务器“拿了一次网页”。
可你打开开发者工具的 Network 面板,往往会看到另一幅景象:HTML 是一次请求,CSS 是一次请求,JavaScript 是一次请求,图片和字体各有各的请求,页面运行后还可能继续请求 JSON 数据。一个看似完整的页面,背后可能是几十次甚至几百次彼此独立的通信。
更反直觉的是,这些通信使用的 HTTP 并不负责把页面画出来,也不负责找到服务器的 IP 地址,更不保证服务器一定保存了你上次访问时的状态。HTTP 主要规定的是:谁向谁表达什么意图,对方用什么结果回答,以及双方怎样描述传输的数据。
这正是后端开发者需要掌握的第一张地图。路由、接口、状态码、Cookie、缓存、网关和负载均衡看起来是不同主题,往下追一层,都会落回 HTTP 的请求、响应、资源和连接。

HTTP 的全称是 Hypertext Transfer Protocol,中文通常译为“超文本传输协议”。名字里虽然有“超文本”,它今天传输的内容早已不限于 HTML。图片、音频、视频、JSON、压缩包,甚至一段没有正文的控制信息,都可以放进 HTTP 消息里。
先把“协议”这个词说得具体一点。协议不是某个程序,也不是某台服务器。它是一套通信约定。客户端用 GET 表达“读取”,服务器用 200 表达“请求成功”,双方用 Content-Type 说明正文该按什么格式理解。这些约定让浏览器、命令行工具、Java 后端和 Go 网关即使由不同团队开发,也能互相听懂。
当你发送一个 HTTP 请求时,数据还需要借助更下层的能力在网络中移动。域名通常先经过 DNS 解析,数据包依靠 IP 找路,HTTP/1.1 和 HTTP/2 通常借助 TCP 可靠传输,HTTPS 还要使用 TLS 保护通信;HTTP/3 则把 HTTP 语义映射到基于 UDP 的 QUIC 上。
所以,“HTTP 使用 TCP”只对一部分版本成立。更准确的说法是:HTTP 定义应用层语义,再由具体版本选择合适的消息格式和底层传输方式。
排查问题时要分层。域名解析失败不等于 HTTP 返回了错误,TCP 连接超时也不等于服务器返回了 504。只有收到格式正确的 HTTP 响应,才有 HTTP 状态码可谈。
HTTP 的基本节奏是客户端先发送请求,服务器再给出响应。请求说明目标资源、操作意图和附加条件;响应说明处理结果,并可能带回资源的一种表示。
例如,浏览器想读取商品列表,可以发出这样的意图:
GET /products?category=book HTTP/1.1
Host: shop.example.test
Accept: application/json服务器可能回答:
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Cache-Control: max-age=60
{"items":[{"id":42,"name":"网络入门"}]}这里真正稳定的是语义:读取 /products 指向的资源,筛选 book 类别,希望得到 JSON,最终成功返回。HTTP/2 和 HTTP/3 在网络上传输它时不会原样发送这段文本,但方法、目标、字段、状态码和内容仍表达同一件事。
你登录之后刷新页面,网站仍然知道你是谁。那为什么 HTTP 还被称为无状态协议?
因为 HTTP 的“无状态”说的是:每个请求的语义都应当能够独立理解,不能仅凭它与上一条消息共用某条连接,就推断它属于同一个用户或某个固定业务步骤。
应用当然可以保存状态。服务器可以把会话记录放进数据库或缓存,浏览器也可以保存 Cookie。后续请求带上会话标识、访问令牌或其他凭证,应用就能把这次请求关联到某个用户。状态是应用机制显式带回来的,不是 HTTP 连接天然记住的。
这样设计的收益很现实:代理可以复用连接,负载均衡器也可以把不同请求分给不同服务器。代价是,需要连续上下文的业务必须自己设计会话、认证和状态存储,不能把“刚才处理过谁”寄托在连接上。
无状态也不等于任意请求都可以放心重试。一次 POST 可能已经创建订单,只是响应在回程中丢了。是否能重试,要继续判断方法语义、幂等性和业务去重机制。
很多初学者会把“客户端”理解成自己的电脑,把“服务器”理解成机房里的一台机器。这个理解在画示意图时够用,写真实后端时却容易出错。
HTTP 中的客户端和服务器是一次通信里的角色。发起请求的程序是客户端,接收请求并返回响应的程序是服务器。同一个程序可以在不同连接里扮演不同角色:你的订单服务对浏览器是服务器,但它请求支付服务时,又成了客户端。
浏览器是最常见的客户端,也叫用户代理。它除了发送 HTTP 请求,还会解析 HTML、执行 JavaScript、应用 CSS、管理缓存和 Cookie,并把多个资源拼成用户看到的页面。
但 curl、手机 App、搜索引擎爬虫、自动化测试、智能设备和另一个后端服务都可以是 HTTP 客户端。HTTP 不要求请求背后一定坐着一个人,也不会因为客户端是浏览器就自动给予更多权限。
你请求 https://shop.example.test/orders/42,看到的域名像是指向一个服务器。真实链路可能先经过 CDN,再到负载均衡器或反向代理,然后才到应用服务;应用服务还会查询缓存、数据库或其他服务。
从客户端看,它面对的是这个源站所代表的统一接口。至于响应由一台机器直接读取文件,还是由几十个组件协作生成,HTTP 并不强迫客户端知道。
中间节点也不是单纯“转发一下”。代理可能缓存响应、检查身份、压缩内容、记录访问日志、终止 TLS,或者把请求分配给不同实例。这样可以把公共能力从业务代码中拿出来,代价是链路更长,故障和配置问题也更难定位。

浏览器第一次拿到的通常只是 HTML 文档。解析过程中遇到样式表、脚本、图片和字体地址,它会继续发起请求。脚本执行后,可能再请求用户信息或商品数据。
这些请求不一定都去同一个域名,也不一定都到达源站。有的资源直接命中浏览器缓存,有的由 CDN 返回,有的被 Service Worker 接管。用户看到一个页面,HTTP 看到的却是一组目标不同、缓存策略不同、完成时间也不同的事务。
假设你访问“今天的天气”。这个资源显然不是服务器磁盘里永远不变的一个文件。它更像一个可以被标识的概念:某个城市在今天这个时间范围内的天气状态。
HTTP 把请求的目标称为资源。资源可以是一篇文章、一位用户、一个订单集合、一张实时生成的图,也可以是“执行某项操作的入口”。HTTP 不限制资源在服务器内部到底由文件、数据库记录还是程序计算实现。
资源通常由 URI 标识,Web 开发里最常遇到的是 URL。看下面这个地址:
https://api.example.test:443/products/42?currency=CNY#reviews
\___/ \__________________/ \__________/ \__________/ \_____/
方案 权威部分 路径 查询 片段https 表示访问这个资源时要使用受保护的 HTTP 通信。#reviews 通常由客户端在取得表示后自行定位,不会作为 HTTP 请求目标发给服务器。URL 是标识,不是服务器目录结构的承诺。/products/42 可能映射到数据库查询或远程服务调用,服务器上未必存在 products/42 这个文件。同一个资源也可能拥有多个入口地址,所以不要把“URI”机械理解成资源的唯一数据库主键。

资源本身是抽象目标,HTTP 消息里传输的是资源的表示。同一个商品资源可以有 JSON 表示,也可以有 HTML 表示;同一张图片可以提供 WebP 或 PNG;同一篇内容可以有中文和英文版本。
客户端可以用 Accept、Accept-Language、Accept-Encoding 等字段表达偏好,服务器再选择合适的表示,并用 Content-Type、Content-Language、Content-Encoding 等字段告诉客户端实际返回了什么。这就是内容协商的基本思路。
内容协商减少了 URI 数量,让同一个资源可以服务不同客户端。但它会让缓存键和服务器选择逻辑变复杂。如果响应会随某个请求字段变化,缓存必须知道这个变化条件,否则可能把为甲客户端生成的表示误发给乙客户端。
今天两次请求同一个天气资源,可能得到不同内容。即便资源没有变化,也未必需要每次完整传输。服务器可以给表示附上验证信息;客户端再次请求时带回验证条件,如果表示仍然有效,服务器只需回答“没有变化”,省去正文传输。
这也是理解缓存的关键:缓存保存的通常是某次响应和它携带的表示,不是把抽象资源本身冻结起来。
你点击“查看订单”,页面迟迟没有反应。问题可能出在域名解析、连接建立、TLS 握手、请求排队、服务器处理、响应传输,甚至浏览器拿到响应后的脚本执行。把这些阶段全叫“接口慢”,很难找到真正原因。
课程里可以把一次请求及其对应响应看作一笔 HTTP 事务。这个词帮助我们划定观察单位,但要记住:事务不等于数据库事务,也不承诺原子性、提交或回滚。
以首次访问一个 HTTPS 地址为例,常见过程是:
这不是每次请求都完整重复的固定流程。DNS 结果可以缓存,连接可以复用,响应也可能直接来自缓存。性能优化常常不是让业务代码“再快两毫秒”,而是减少某个阶段,或者把一次昂贵成本摊到多笔事务上。

最常见的是一个请求对应一个最终响应,例如 200 OK 或 404 Not Found。不过服务器可以先发送一个或多个 1xx 临时响应,再发送最终响应。临时响应是在告诉客户端“目前进展到这里”,最终响应才结束这次请求的响应序列。
重定向则是另一回事。服务器返回 3xx 最终响应,并在字段里给出新位置;客户端如果决定继续访问,会对新目标发起一笔新的 HTTP 事务。浏览器最后展示一个页面,不代表中间只发生过一次请求。
状态码是给客户端程序判断结果的三位数字。2xx 表示请求已被成功接收、理解或处理到相应程度,3xx 通常提示还需进一步动作,4xx 指向请求一侧的问题,5xx 表示服务器处理请求时失败。
但状态码不是页面文案。服务器可以返回 404 和一份精心设计的 HTML 错误页,也可能错误地返回 200,正文却写着“订单不存在”。后者会让监控、缓存和客户端逻辑都得到错误信号。后端要让状态码与实际结果一致,再由正文补充业务细节。
浏览器显示“请求失败”时,先区分网络错误和 HTTP 错误。收到 500 说明 HTTP 事务已经到达服务器并拿到了最终响应;DNS 失败、证书校验失败或连接超时,通常根本没有 HTTP 响应。
如果把事务看成一次问答,HTTP 消息就是双方递出的两张纸:请求写“我想做什么”,响应写“结果是什么”。
学习消息结构时,最适合先看可读的 HTTP/1.1。HTTP/2 和 HTTP/3 改变了线上编码和分帧方式,但没有另造一套方法、状态码和资源语义。
下面是一条带 JSON 内容的 HTTP/1.1 请求:
POST /orders HTTP/1.1
Host: api.example.test
Content-Type: application/json; charset=utf-8
Accept: application/json
Content-Length: 31
{"productId":42,"quantity":1}它可以拆成四部分:
POST 语义处理的 JSON 内容。方法不是对 URL 的装饰。它表达客户端希望怎样操作目标资源。GET /orders/42 和 DELETE /orders/42 指向同一目标,却有完全不同的意图、安全性和重试含义。
服务器创建订单后可能返回:
HTTP/1.1 201 Created
Content-Type: application/json; charset=utf-8
Location: /orders/9001
Content-Length: 42
{"id":9001,"status":"pending"}响应的起始行叫状态行,包括 HTTP 版本、状态码和可选原因短语。后面同样是字段区、空行和可选消息体。
201 已经表达“创建成功”,Location 指出新资源的位置,JSON 正文再给应用需要的订单信息。三者承担不同职责,不能只写一段“success”正文就期待所有客户端猜对结果。

HTTP 字段以名称和值传递附加信息。字段名不区分大小写,但不同字段的值有各自语法,不能把它们都当普通字符串拼接。
字段可以影响缓存、条件请求、内容协商、认证、连接管理和跨域行为。它们让 HTTP 能在不改变基本消息模型的前提下扩展。不过,字段越多并不代表设计越好。超大的 Cookie 或重复元数据会增加每次请求的成本,不受约束的自定义字段还会增加代理和客户端之间的兼容负担。
在持久连接上,接收方读完一个消息后,还要继续读取下一条消息。它必须知道当前消息体在哪里结束。HTTP/1.1 可以根据方法、状态码和字段判断是否存在消息体,再通过明确长度、分块传输编码,或者在部分场景中通过连接关闭确定边界。
这不是无聊的格式细节。如果发送方和中间代理对消息边界理解不同,轻则出现截断或一直等待,重则形成请求走私等安全问题。实际项目应使用成熟 HTTP 库,不要靠字符串切割自行解析线上流量。
HTTP/2 把请求和响应映射为二进制帧,并让每组请求与响应运行在各自的流上。常见字段会被压缩,多个流的帧可以交错传输。开发者工具仍会把它还原成方法、目标、字段和状态码,方便人阅读。
HTTP/3 也使用帧和流,只是把承载从 TCP 换成 QUIC,字段压缩机制也相应变化。于是我们可以把 HTTP 分成两层理解:上层是跨版本共享的语义,下层是每个版本如何在连接上传递这些语义。
如果一个页面有一百个资源,最直接的实现是为每个资源新建连接、请求一次、收完就关闭。逻辑简单,但网络延迟会不断重复:连接建立要等待,HTTPS 还要协商加密,刚升起来的传输速率又很快被关闭。
HTTP 的版本演进并没有推翻“请求资源、返回响应”这套语义,主要变化之一就是:怎样用更少的等待和更合理的连接承载更多事务。
早期 HTTP/1.0 的典型用法是一笔事务使用一条 TCP 连接,响应结束后关闭。实现容易,页面资源一多,连接与握手成本就会迅速累积。
后来一些实现增加了持久连接扩展,但行为并不总是一致。这里适合记住历史背景,不要把今天 HTTP/1.1 的规则套回所有 HTTP/1.0 实现。
HTTP/1.1 默认让连接保持可复用,除非一方明确表示当前响应后关闭。于是同一个客户端可以在一条连接上依次完成多笔事务,不必每次重新握手。
旧文档经常说“客户端必须发送 Connection: keep-alive 才能复用 HTTP/1.1 连接”,这是把 HTTP/1.0 扩展和 HTTP/1.1 默认行为混在了一起。HTTP/1.1 更常见的关闭信号是 Connection: close。
连接复用节省了建立成本,却没有自动带来理想并发。HTTP/1.1 的流水线允许连续发送多个请求,但响应仍要按请求顺序返回,前面一个慢响应会挡住后面的响应,而且中间设备兼容性曾经很棘手。浏览器因此通常会并行建立若干连接来缓解等待,但连接越多,服务器资源和网络拥塞压力也越大。
HTTP/2 把不同请求和响应放进独立的流,再把各流的帧交错放入同一条 TCP 连接。一个业务响应生成得慢,不再要求其他已就绪响应在 HTTP 消息层面排队等待。头部压缩也减少了大量重复字段的传输。
但“HTTP/2 彻底解决队头阻塞”说得太满。它解决了 HTTP/1.1 的应用层排队问题,却仍建立在一条有序可靠的 TCP 字节流上。如果某个 TCP 数据段丢失,后续字节即使已经到达,也要等缺口补齐后才能交给 HTTP/2;这期间多个 HTTP/2 流都可能被拖住。
HTTP/3 把 HTTP 映射到 QUIC。QUIC 运行在 UDP 之上,但它不是“直接使用不可靠 UDP 发网页”。可靠传输、拥塞控制、加密和多路流等能力由 QUIC 自己提供。
QUIC 为每条流分别维护有序字节。某条流的数据丢失时,需要等待的是相关流,其他已经收到完整数据的流可以继续向上交付。QUIC 还把安全握手纳入协议,并通过连接标识支持网络地址变化后的连接迁移,这对移动设备从 Wi-Fi 切到蜂窝网络尤其有用。
代价同样存在。QUIC 和 HTTP/3 的实现、观测与运维更复杂,UDP 在某些网络环境中可能受限,头部压缩仍需谨慎处理跨流依赖。HTTP/3 不是让每个请求无条件变快,而是给高延迟、易丢包和经常切换网络的场景提供更合适的传输基础。

不要用 HTTP 版本替代性能分析。HTTP/2 或 HTTP/3 可能减少连接与排队成本,却不会自动修复慢查询、超大响应、错误缓存策略或拥塞的网络。先找到时间花在哪一层,再决定优化手段。
HTTP 的历史不是每隔几年把旧协议全部淘汰,而是在尽量保持上层语义的同时,修补当时最突出的限制。
最早的 HTTP 后来被称为 HTTP/0.9。请求只有一行 GET 加路径,没有版本号、字段和状态码;响应直接返回 HTML 内容。它适合早期的小型超文本系统,但无法清楚表达内容类型、错误结果或缓存条件。
GET /index.html它给后续版本留下了最核心的节奏:客户端指出目标,服务器返回内容。
HTTP/1.0 在实践中加入版本、方法、状态码和字段。Content-Type 让接收方知道正文是 HTML、图片还是其他格式,HTTP 从“取超文本”走向可以承载多种媒体和应用数据。
它的标准文档更像是对当时通行实现的整理。不同产品曾经先各自尝试功能,再逐渐形成共同规则,这也解释了为什么某些旧式行为今天仍会在兼容代码里出现。
HTTP/1.1 规范了持久连接、消息长度、缓存控制、条件请求、内容协商和 Host 等能力。Host 让同一个 IP 地址可以为多个站点服务,持久连接减少了页面加载时反复建连的成本。
它经多次修订,今天仍被大量系统使用。所谓“旧版本”不等于已经无用:实现简单、基础设施成熟、逐跳排障直观,都是它继续存在的原因。
HTTP/2 没有要求应用把所有接口改写一遍。GET 还是读取,404 还是目标未找到。主要改变发生在线上编码:二进制分帧、流多路复用和字段压缩,让一条连接更高效地承载并发事务。
这是一种很有启发的协议演进方式:上层接口尽量稳定,下层表示可以持续优化。浏览器和服务器升级协议,业务路由通常不需要因此重写。
HTTP/3 延续 HTTP/2 的语义和流式思路,把底层换成 QUIC,以降低 TCP 层队头阻塞的影响,并改善安全握手与连接迁移。
HTTP/1.1、HTTP/2 和 HTTP/3 至今仍会并存。客户端与服务器根据支持情况、配置和网络条件选择协议。部署 HTTP/3 也通常需要保留回退路径,因为不是每条网络都能顺利使用 QUIC。

现在再看一次“打开商品页”这件事:
遇到问题时也沿着这条链问:目标资源写对了吗?客户端真正发出了什么?中间节点是否改写或缓存?服务器返回的是网络错误还是 HTTP 响应?状态码和正文是否一致?时间消耗在建连、等待处理还是传输?
HTTP 概览的价值就在这里:它不替你直接写出某个接口,却让后面学习 URL、方法、状态码、字段、Cookie 和缓存时,每个概念都有位置可放。下一步,我们会从这张地图里最常被忽略的一块继续往下走——URL 看起来只是一串地址,实际上它已经决定了请求要去哪里、针对什么资源,以及哪些部分根本不会发送给服务器。