HTTP 长连接(Keep-Alive)与连接复用是怎么回事?为什么能提升性能?
简化版
HTTP 长连接(Keep-Alive)指一次 TCP 连接建立后,多次 HTTP 请求-响应复用它,不用每次都重新握手、断开。没有长连接时,每个请求都要「三次握手建连 → 传一次数据 → 四次挥手断开」,光握手就浪费一个 RTT,还要反复经历 TCP 慢启动。开了 Keep-Alive,连接建好后保持一段时间,后续请求直接拿来用,省掉了重复握手和慢启动的开销,这就是连接复用。HTTP/1.1 默认开启 Keep-Alive;HTTP/2 更进一步,一条连接上还能并发多个请求(多路复用),把连接复用做到极致。
详细版
一、短连接的问题
HTTP/1.0 默认是短连接:每个请求一条新 TCP 连接,用完就关。加载一个有几十个资源的网页,就要建几十次连接,每次都要付出:
- TCP 三次握手:1 个 RTT 的延迟。
- TLS 握手(HTTPS):还要额外 1~2 个 RTT。
- TCP 慢启动:每条新连接都从很小的拥塞窗口开始,速度慢慢爬,短连接还没爬起来就结束了。
- 四次挥手 + TIME_WAIT:关闭也有开销,还会占用端口资源。
二、长连接(Keep-Alive)
在响应头/请求头带上 Connection: keep-alive,告诉对方「这条连接别关,我还要用」。于是:
建立连接(三次握手,一次)
├─ 请求1 → 响应1
├─ 请求2 → 响应2 ← 复用同一条连接,无需重新握手
└─ 请求3 → 响应3
(空闲超时或达到上限后才关闭)
- HTTP/1.1 默认开启 Keep-Alive,不写
Connection头也是长连接;要显式关闭得写Connection: close。 - HTTP/1.0 默认关闭,要长连接得显式写
Connection: keep-alive。
三、长连接不是永久连接
服务器不会永远留着空闲连接,一般有两个限制:
- 超时时间(如 Nginx
keepalive_timeout):连接空闲多久后关闭。 - 最大请求数:一条连接最多处理多少个请求后关闭。
四、长连接 ≠ 并发(HTTP/1.1 的局限)
HTTP/1.1 的长连接虽然复用了连接,但同一条连接同一时刻仍只能处理一个请求(一问一答),存在队头阻塞。浏览器靠同域名开 6 条并发连接来提升并行度。HTTP/2 的多路复用才实现了「一条连接上并发多个请求」,是连接复用的终极形态。
完整版教学
一、为什么连接复用如此重要:成本在「建连」而非「传输」
很多人以为网络慢主要是「数据传得慢」,其实对大量小请求来说,真正的开销在反复建立和拆除连接。建一条 TCP 连接要经历三次握手,这至少花掉一个往返时延(RTT);如果是 HTTPS,还要再叠加 TLS 握手的 1~2 个 RTT。
假设你和服务器之间 RTT 是 100ms,一个页面 30 个资源:
- 短连接:每个资源都要重新握手,光是 TCP 握手就 30 × 100ms = 3 秒白白花在握手上(HTTPS 更夸张)。
- 长连接:握手一次,后面 29 个请求直接复用,省下大量 RTT。
所以连接复用优化的是延迟(少几十个 RTT),这在高延迟网络(移动、跨国)上收益巨大。
二、慢启动:短连接的隐藏杀手
TCP 有个慢启动机制:新连接刚建立时,为了不冲垮网络,拥塞窗口从很小开始(比如几个 MSS),每收到一批 ACK 才翻倍增长,逐渐试探出能跑多快。
短连接的问题在于:每条连接都要从慢启动重新开始,往往数据还没传完、窗口还没涨起来,连接就关了。等于每次都在「起步阶段」龟速传输,永远达不到链路的真实带宽。
长连接复用后,连接的拥塞窗口经过前几个请求已经「热」起来(涨大了),后续请求一上来就能用较大的窗口高速传输。这是长连接除了省握手之外的第二个重要收益。
记忆点:连接复用省的是两笔钱——重复握手的 RTT + 反复慢启动的低速起步。
三、Keep-Alive 的工作机制与头部
请求/响应头:
Connection: keep-alive # 保持连接
Connection: close # 用完关闭
Keep-Alive: timeout=60, max=100 # 建议空闲 60 秒、最多 100 个请求(HTTP/1.1 中此头较少用,多由服务器配置控制)
版本默认行为:
| 版本 | 默认 | 要改需写 |
|---|---|---|
| HTTP/1.0 | 短连接 | 长连接写 Connection: keep-alive |
| HTTP/1.1 | 长连接 | 关闭写 Connection: close |
如何判断响应边界? 长连接下多个响应挤在一条连接上,接收端必须知道每个响应到哪结束:
- 用
Content-Length明确声明响应体长度; - 或用
Transfer-Encoding: chunked分块传输,最后一个空块表示结束。
没有这两者之一,长连接就无法切分响应,只能退回「读到连接关闭为止」的短连接语义。
四、长连接的资源管理:不能只建不关
长连接虽好,但空闲连接也占资源(内存、文件描述符、端口)。服务器必须管理它们:
keepalive_timeout(空闲超时):连接空闲超过这个时间就主动关闭。设太短,复用率低;设太长,空闲连接堆积占资源。- 每连接最大请求数:处理够多请求后主动关,避免单连接长期占用、也利于负载重新分配。
- 连接数上限:并发长连接太多会耗尽服务器资源,要有上限和 LRU 回收。
客户端侧(浏览器、HTTP 客户端库、连接池)也要管理:连接池复用空闲连接,控制每个 host 的最大连接数、空闲连接存活时间。后端服务之间调用时,用连接池(如数据库连接池、HTTP 连接池)复用连接,是高并发系统的标配。
五、HTTP/1.1 长连接的天花板:还是串行
HTTP/1.1 的长连接解决了「反复建连」,但没解决「一条连接同一时刻只能跑一个请求」。请求必须一问一答,前一个响应没回来,后面的在这条连接上排队——这就是 HTTP 层队头阻塞。
浏览器的应对是给同一域名开多条并发连接(默认约 6 条),相当于 6 条流水线并行。但连接数有限,且每条连接各自要握手、各自慢启动。
HTTP/2 的多路复用才是终极方案:一条连接上用「流」并发多个请求,帧交错传输。于是一条连接顶过去 6 条,既复用了连接、又实现了真正的并发,还省掉多连接的重复握手和慢启动。这也是为什么 HTTP/2 时代不再需要「域名分片」这类 hack。
六、和 TCP Keep-Alive 区分开(重要易混)
面试常挖的坑:HTTP Keep-Alive ≠ TCP Keep-Alive,两者完全不同:
| HTTP Keep-Alive | TCP Keep-Alive | |
|---|---|---|
| 所在层 | 应用层(HTTP) | 传输层(TCP) |
| 作用 | 复用连接跑多个 HTTP 请求 | 探测连接对端是否还活着(发保活探针) |
| 触发 | Connection: keep-alive 头 | socket 选项 SO_KEEPALIVE |
| 目的 | 提升性能,减少建连 | 清理死连接、防止防火墙断掉空闲连接 |
名字像,作用完全不同。HTTP Keep-Alive 是为了复用,TCP Keep-Alive 是为了保活探测。
七、常见误区与追问
| 考点 | 正确口径 |
|---|---|
| 短连接 | 每次请求都新建 TCP/TLS 连接 |
| Keep-Alive | 复用同一连接发送多个请求 |
| 收益 | 减少握手 RTT、慢启动和 CPU 开销 |
without reuse: TCP handshake + TLS handshake + request
with keep-alive: first handshake once, then many requests
100 requests * 2 RTT handshake saved can be significant
连接复用省的不只是握手时间,还避免频繁慢启动和 TLS 计算开销。
- 误区:Keep-Alive 永远越久越好。 空闲连接会占用 fd、内存和负载均衡状态,超时时间要按业务调。
- 误区:HTTP Keep-Alive 等于 TCP keepalive。 前者是 HTTP 连接复用策略,后者是 TCP 层探活机制。
- 误区:连接复用不会影响负载均衡。 长连接会让请求粘在已有后端,可能影响流量重新分配。
- 追问:为什么复用能降低延迟? 省掉 TCP 三次握手和 TLS 握手的往返,后续请求可直接发送。
- 追问:HTTP/2 连接复用更强在哪里? HTTP/2 可在一个连接上多路复用多个并发 stream。
- 追问:什么时候要主动关闭连接? 服务端限流、灰度切流、后端摘除、空闲过久或连接异常时都需要关闭。
八、加强记忆
HTTP 长连接(Keep-Alive)就是一条 TCP 连接反复用于多个 HTTP 请求,不再每次重新握手、断开。它省的是两笔开销:重复的三次握手/TLS 握手(每次一个多 RTT)+ 反复从头慢启动的低速起步。HTTP/1.1 默认开启,但一条连接同一时刻仍只能一问一答,靠浏览器多开 6 条连接并发;HTTP/2 的多路复用才实现单连接并发多请求。切记别把它和「探测对端死活」的 TCP Keep-Alive 搞混——一个是应用层复用,一个是传输层保活。