很多候选人在被问到「HTTP/1.1 的长连接和 HTTP/2 的多路复用有什么区别」时,答的都是「一个是复用 TCP 连接,一个是并行传输,HTTP/2 更快」。这个回答在面试里几乎等于没答,因为它没有触及二者的本质差异:HTTP/1.1 的长连接解决的是「连接复用」问题,但仍然受制于应用层的请求-响应串行语义;HTTP/2 的多路复用解决的是「帧级并发」问题,把一条 TCP 连接变成了一条可以同时承载多个独立请求流的管道。本文把这个差异拆成可讲、可验证、可落地的几层,既能在面试中讲清楚,也能在线上排障时用得上。
一、先把概念对齐:长连接、Keep-Alive、流水线、多路复用不是一回事
面试中大量混乱来自概念混用。先把四个词的定义钉死:
- 持久连接(persistent connection):HTTP/1.1 默认行为(RFC 7230 §6.3),一个 TCP 连接在完成一次请求-响应后不立即关闭,后续请求可继续使用。对应头部
Connection: keep-alive(HTTP/1.1 中已是默认值,显式写出只是兼容 HTTP/1.0 代理)。 - HTTP 流水线(pipelining):客户端不等前一个响应返回就连续发送多个请求,但服务器必须按请求顺序返回响应(RFC 7230 §6.3.2)。这才是「串行语义」的真正来源,也是流水线在实际部署中几乎被浏览器禁用的原因(队头阻塞 + 中间设备兼容性差)。
- 域分片(domain sharding):既然一个连接上请求是串行的,浏览器就对同一域名开 6 个左右并发连接(Chrome 对同一 origin 的 HTTP/1.1 连接上限为 6)。这是对长连接串行缺陷的「工程绕行」,代价是额外的 TCP 握手、TLS 握手和拥塞控制冷启动。
- 多路复用(multiplexing):HTTP/2 在一条 TCP 连接上把请求和响应切分成二进制帧(frame),每个帧带
stream ID,不同 stream 的帧可以交错发送、乱序到达,接收端按 stream ID 重组。
面试要点:能主动指出「HTTP/1.1 的 keep-alive 是连接级复用,HTTP/2 的 multiplexing 是流级并发」,这一句话就把层次拉开了。前者复用通道,后者复用通道内的时隙。
二、本质区别一:并发模型不同,队头阻塞的层级不同
HTTP/1.1:应用层队头阻塞(head-of-line blocking)
HTTP/1.1 长连接上,请求-响应严格配对且顺序敏感。设想一个连接上依次发出 A、B、C 三个请求,A 的响应体很大或后端处理慢,B、C 的响应即使已经准备好,也不能越过 A 发送(除非启用 pipelining 且服务器严格按序返回,但即便如此 B、C 仍被 A 阻塞)。这就是应用层 HOL 阻塞。
浏览器给出的解法是并发多连接,代价可以用真实数据说明:一个 HTTPS 请求要经历 TCP 三次握手(1 RTT) + TLS 1.3 握手(1 RTT,TLS 1.2 为 2 RTT)。在 40ms RTT 的链路上,新建一条 TLS 1.3 连接至少 80ms 才发出第一个字节的应用数据,而多开 6 条连接意味着 6 份拥塞控制窗口从初始窗口(Linux 默认 initcwnd=10 个 MSS)重新爬升。
HTTP/2:传输层队头阻塞被消除,但 TCP 层 HOL 依然存在
HTTP/2 把一条连接抽象成若干并发 stream(默认 SETTINGS_MAX_CONCURRENT_STREAMS 通常由服务端下发,nginx 默认 128,Go 的 http2 默认 250),每个 stream 有自己的流量控制窗口。帧可以交错,所以 A 慢不影响 B、C 的帧被发送。
但必须讲清楚一个常被追问的点:HTTP/2 并没有消除 TCP 层的队头阻塞。所有 stream 的帧跑在同一条 TCP 连接上,一旦发生丢包,TCP 的重传会阻塞该连接上后续所有字节的交付,所有 stream 一起卡住。这正是 HTTP/3 改用 QUIC(基于 UDP,每个 stream 独立可靠传输)的根本动因。面试里能补上这一句,说明不是只背了 HTTP/2 的优点。
| 维度 | HTTP/1.1 长连接 | HTTP/2 多路复用 |
|---|---|---|
| 复用单位 | TCP 连接(连接级) | Stream(流级,帧为传输单位) |
| 并发方式 | 多开连接(每 origin 约 6 条) | 单连接内多 stream 并发 |
| 应用层 HOL | 存在,且无法在协议内解决 | 消除(stream 间独立) |
| TCP 层 HOL | 存在 | 仍然存在(HTTP/3 才解决) |
| 头部传输 | 每次请求重复发送纯文本头 | HPACK 静态/动态表压缩 |
| 传输格式 | 文本协议,行分隔 | 二进制分帧层(frame) |
三、本质区别二:协议语义从「文本行」变成「二进制帧 + 流状态机」
这不是实现细节,而是能力的来源。HTTP/1.1 的报文是纯文本,一条连接上同一时刻只存在一个「当前响应」,协议本身没有字段能标识「这段字节属于哪个请求」——只能靠顺序。HTTP/2 引入了分帧层,每个帧头部 9 字节,格式固定:
+-----------------------------------------------+
| Length (24) |
+---------------+---------------+---------------+
| Type (8) | Flags (8) |
+-+-------------+---------------+-------------------------------+
|R| Stream Identifier (31) |
+=+=============================================================+
| Frame Payload (0...) ...
+---------------------------------------------------------------+
帧类型(Type)包括 DATA(0x0)、HEADERS(0x1)、PRIORITY(0x2)、RST_STREAM(0x3)、SETTINGS(0x4)、PUSH_PROMISE(0x5)、PING(0x6)、GOAWAY(0x7)、WINDOW_UPDATE(0x8)、CONTINUATION(0x9)。有了 stream ID,多路复用才成立;有了 RST_STREAM,才能只取消一个请求而不关闭整条连接——这是 HTTP/1.1 做不到的(HTTP/1.1 里取消一个请求往往直接断 TCP,代价是摧毁同连接上的其它请求)。
用一段可运行的 Python 代码验证 HTTP/2 的多路并发。下面用 httpx(内置 h2 支持)对同一连接发起 5 个并发请求,并打印实际使用的 HTTP 版本:
import asyncio
import httpx
async def main():
# http2=True 启用 h2 协议栈;limits 里限制连接数为 1,
# 确保所有请求复用同一条 TCP 连接,从而验证 stream 级复用。
limits = httpx.Limits(max_connections=1, max_keepalive_connections=1)
async with httpx.AsyncClient(http2=True, limits=limits, timeout=10.0) as client:
urls = [f"https://http2.pro/api/v1?i={i}" for i in range(5)]
tasks = [client.get(u) for u in urls]
responses = await asyncio.gather(*tasks, return_exceptions=True)
for r in responses:
if isinstance(r, Exception):
print("ERROR:", type(r).__name__, r)
continue
print(
"url=%s http_version=%s status=%s body_len=%d"
% (r.request.url, r.http_version, r.status_code, len(r.content))
)
asyncio.run(main())
输出中 http_version=HTTP/2,且 5 个请求共享一条连接。若把 http2=False 并保持 max_connections=1,请求会在这唯一一条连接上排队(HTTP/1.1 串行),整体耗时明显上升——这就是面试里可以口述的对照实验。
用 curl 也能直接观察,注意版本参数:
# 查看协商到的协议版本与连接复用情况
curl -s -o /dev/null -w "http_version=%{http_version} num_connects=%{num_connects}\n" \
--http2 https://www.cloudflare.com/
# 连续两个 URL 复用同一连接(第二次 num_connects 为 0)
curl -s -o /dev/null -w "http=%{http_version} conn=%{num_connects}\n" \
--http2 https://www.cloudflare.com/ https://www.cloudflare.com/
如果本地 curl 报 curl: (1) Unsupported protocol 或提示 HTTP/2 client preface string missing or corrupt,先执行 curl -V 看 Features: 行里是否有 HTTP2;没有就需要重新编译带 nghttp2 的 curl(macOS 用 brew install curl,注意 /usr/bin/curl 是系统自带的旧版本,需把 /opt/homebrew/opt/curl/bin 放在 PATH 前面)。
四、本质区别三:连接数假设被反转,工程实践随之改变
这是最容易在面试中出彩的部分——协议设计哲学的反转:
- HTTP/1.1 时代,连接越多越好:因为单连接串行,所以域分片(把静态资源拆到
static1.example.com、static2.example.com)、雪碧图(CSS sprite)、资源合并(把多个 JS 打包成一个)都是为了减少请求数、绕开串行限制。 - HTTP/2 时代,连接越少越好:一条连接就能并发,多开连接反而稀释了拥塞窗口、让 HPACK 动态表无法跨连接共享、并且增加 TLS 握手开销。因此 HTTP/2 下应当合并域名、关闭域分片,打包策略也要重新评估(过度打包会破坏缓存粒度)。
实测层面的参数:Linux 上查看当前连接的拥塞控制与初始窗口,可以用
# 查看可用的拥塞控制算法(确认是否为 cubic/bbr)
sysctl net.ipv4.tcp_congestion_control
sysctl net.ipv4.tcp_slow_start_after_idle
# 查看 HTTP/2 相关的 nginx 连接状态(需要 stub_status 模块)
curl -s http://127.0.0.1/nginx_status
nginx 开启 HTTP/2 的写法(注意:http2 是 listen 的参数,不是独立指令):
server {
listen 443 ssl;
http2 on; # nginx 1.25.1+ 的推荐写法
# 1.25.1 之前写作:listen 443 ssl http2;
ssl_certificate /etc/nginx/cert/fullchain.pem;
ssl_certificate_key /etc/nginx/cert/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
# 调大并发 stream 上限(默认 128),配合后端能力设置
http2_max_concurrent_streams 256;
# 单连接上允许的最大帧大小,默认 16384,最大 16777215
http2_chunk_size 16k;
keepalive_timeout 75s;
keepalive_requests 1000; # 单连接最多承载的请求数,超过后发 GOAWAY
location / {
proxy_pass http://backend;
proxy_http_version 1.1; # 注意:nginx 到上游默认仍是 HTTP/1.1
proxy_set_header Connection "";
}
}
这段配置里有两个高频踩坑点,面试和排障都用得到:
- 「我开了 HTTP/2 但后端没变快」:nginx 的
proxy_pass默认对上游使用 HTTP/1.1(proxy_http_version默认 1.0,需显式设为 1.1 才能 keepalive)。多路复用在「客户端 ↔ nginx」这一段生效,而「nginx ↔ 上游」仍是短连接串行,瓶颈没动。要端到端 HTTP/2,需上游也支持 h2c 或 h2(如grpc_pass场景),或者接受「边缘复用 + 内部连接池」的折中。 http2 on;报错:nginx 1.25.1 之前没有http2指令,写http2 on;会得到nginx: [emerg] unknown directive "http2",应改回listen 443 ssl http2;。反之在 1.25.1+ 用旧写法会给警告:the "listen ... http2" directive is deprecated, use the "http2" directive instead。
另一个真实报错:浏览器控制台出现 ERR_HTTP2_PROTOCOL_ERROR,且只在部分响应上出现。常见原因是响应头里带了 HTTP/2 不允许的字段(如 Connection、Keep-Alive、Transfer-Encoding,这些是 connection-specific header,RFC 7540 §8.1.2.2 明确禁止),或后端返回了非法的小写化失败的头名。排查方式是抓包看 HEADERS 帧,或者先用 curl --http1.1 对比是否只在 h2 下失败。
五、把差异写成一段能在面试里讲的回答
把上面三层压成一段结构化的口述(约 90 秒),基本可以覆盖面试官追问:
「HTTP/1.1 的长连接是连接级别的复用:一个 TCP 连接可以跑多次请求-响应,但同一时刻只能有一个在途响应,请求是串行的,所以存在应用层队头阻塞。浏览器的应对是每个 origin 开 6 条左右连接,代价是握手和拥塞控制冷启动。
HTTP/2 的多路复用是流级别的并发:它引入二进制分帧层,把请求响应切成带 stream ID 的帧,多个 stream 的帧在同一条连接上交错传输,接收端按 stream ID 重组,所以消除了应用层 HOL,还能用 RST_STREAM 单独取消一个请求。代价是多路复用共享同一条 TCP 连接,一旦丢包,TCP 层重传会阻塞所有 stream,这个 TCP 级 HOL 直到 HTTP/3 用 QUIC 才解决。
所以二者的本质区别不是快慢,而是并发模型:HTTP/1.1 是『连接复用 + 应用层串行』,HTTP/2 是『单连接内的帧级并发』。这直接反转了工程实践——HTTP/1.1 时代靠域分片和资源合并减少连接数,HTTP/2 时代应该合并域名、关闭分片,因为连接越少、拥塞窗口和 HPACK 动态表越集中,收益越大。」
如果面试官继续追问「那 HTTP/2 是不是一定比 HTTP/1.1 快」,正确的态度是区分场景:在高 RTT、多小资源的页面上,HTTP/2 收益明显;在单条大文件下载上,两者差别很小(甚至因为分帧开销略慢),因为瓶颈在带宽和 TCP 拥塞控制,而不在请求并发度。这种「知道它什么时候没用」的回答,通常比一味夸协议更能拿到分。
小结
记住三条主线即可:其一,复用粒度从「连接」下沉到「stream」,这是二进制分帧层带来的能力;其二,队头阻塞从应用层搬到了 TCP 层,HTTP/2 消除前者但无法消除后者,这是 HTTP/3 的起点;其三,工程假设反转——HTTP/1.1 拼连接数,HTTP/2 拼单连接质量,所以域分片、资源合并、打包策略都要跟着改。把这三点讲清楚,比背任何性能数字都有说服力。
FAQ
Q1:HTTP/1.1 的 Connection: keep-alive 和 HTTP/2 的连接复用能同时存在吗?
不能混着理解。HTTP/2 中 Connection 是禁用头(RFC 7540 §8.1.2.2),连接是长期存在的,不存在「关闭 keep-alive」这回事。如果你在 HTTP/2 响应里下发 Connection: keep-alive,严格实现的客户端会直接判定协议错误,报 ERR_HTTP2_PROTOCOL_ERROR。清理中间件(尤其是老版本 Java Servlet Filter、PHP 框架)主动注入的这类头即可。
Q2:HTTP/2 只用一条连接,会不会更慢?
取决于瓶颈位置。单连接意味着所有 stream 共享一个拥塞窗口,在丢包率高的链路上(比如移动网络),TCP 重传会拖慢全部 stream,此时多连接反而因为拥塞窗口独立而鲁棒性更好——这也是部分 CDN 在弱网场景下仍保留「按资源类型拆分少量连接」策略的原因。另外 HTTP/2 的 PRIORITY 帧与依赖树在实际实现中大多被简化或忽略(Chrome 已废弃对优先级的严格支持),不要指望靠它做精细调度。
Q3:为什么抓包看 HTTP/2 的 HEADERS 帧里读不到明文头?
因为 HPACK 压缩。头部字段被编码进静态表(61 个预定义条目)和动态表,用索引号 + Huffman 编码表示。想还原明文,Wireshark 需要配置 TLS 解密(导入 SSLKEYLOGFILE 生成的密钥日志,或提供服务器私钥),然后在 Statistics → HTTP2 里查看解压后的头部;命令行可以用 nghttp -nv https://example.com(nghttp2 客户端)直接打印可读的帧序列。
Q4:服务端返回 GOAWAY 是什么情况?
GOAWAY(帧类型 0x7)表示服务端要关闭连接,并携带 Last-Stream-ID,客户端应当重试 ID 大于该值的请求。常见触发:keepalive_requests 达到上限、http2_max_concurrent_streams 被打满后服务端主动收缩、优雅重启(nginx reload)。如果日志里频繁出现 GOAWAY,先看客户端是否在单连接上打了过多并发 stream,再考虑调大 http2_max_concurrent_streams 或做客户端侧限流。
Q5:怎么快速判断线上到底跑的是 HTTP/1.1 还是 HTTP/2?
用 curl -sI --http2 https://域名 | head -1 看状态行是 HTTP/2 200 还是 HTTP/1.1 200;浏览器 DevTools 的 Network 面板打开「Protocol」列,会显示 h2 / http/1.1 / h3。若显示 http/1.1 却以为自己开了 h2,八成是 CDN 或负载均衡层没开 ALPN,检查 TLS 握手协商的 ALPN 是否为 h2(openssl s_client -alpn h2 -connect 域名:443,看输出里 ALPN protocol: h2)。