HTTP/1.1 默认不再用完就关,一条 TCP 连接可以连发多个请求。省下的是每一次的握手与慢启动。
上一节讲方法语义,这一节回到连接层。1.2.3 节算过「每资源一条连接」有多贵,这里给出解法。
一、1.0 和 1.1 的默认值反过来了
HTTP/1.0 默认 Connection: close:一个请求一个响应,然后关连接。
HTTP/1.1 反过来,默认保持连接,要关得显式写 Connection: close。
这一条是 1.0 到 1.1 最重要的一次改动,收益也最大:五个请求,握手次数从 5 次降到 1 次。
跨城按 40 毫秒算,省下 160 毫秒。
注意它不是新功能——1.0 时代就有 Connection: keep-alive 扩展,1.1 只是把它变成了默认。
默认值的力量往往大于功能本身。
二、省下的不只是握手
握手是一个往返,但省下的更多:TCP 的慢启动。新连接一开始拥塞窗口很小,要一个往返一个往返地涨。 复用连接就跳过了这个爬坡过程。 还有 TLS。如果用 HTTPS,每条新连接都要再做一次 TLS 握手——TLS 1.2 要两个往返,1.3 要一个。 复用连接连这部分也省了。 所以「持久连接」的价值在 HTTPS 场景下比 HTTP 更大。 书里写这一节时(2013 年)HTTPS 还没普及,现在则是最主要的收益来源。
三、连接能保持多久
保持多久由两边共同决定:Keep-Alive: timeout=5, max=100 这种首部(非标准,
但广泛支持)告诉客户端「我等你 5 秒、这条连接最多接 100 个请求」。
服务端通常还有个总超时(Nginx 的 keepalive_timeout 默认 75 秒)。
所以「连接池里的连接」随时可能已经被对端悄悄关了。
这就是线上最常见的故障之一:拿到的是一条已被关闭的连接,发出去没反应。
客户端必须能识别并重连——这也是为什么「幂等」在 2.6 节那么重要。
四、动手 Demo:一条连接连发五个,对比五条连接
两种走法各跑一遍:每条请求新建连接,以及一条连接连发。
注意读响应用的是按 Content-Length 读,不是「读到关闭」——后者在持久连接上会一直等到超时。
# ① 每请求一条新连接
for _ in range(n):
s = socket.create_connection((u'127.0.0.1', port), timeout=2.0)
s.sendall(_req())
H.read_all(s, timeout=0.3)
s.close()
# ② 一条连接连发
buf = b''
for _ in range(n):
s.sendall(_req())
_, buf = H.recv_response(s, buf)
实跑输出:
--------------------------------------------------------------------
持久连接:一条 TCP 连发五个请求
--------------------------------------------------------------------
请求数 5
① 每请求一条新连接 18.13 毫秒
② 一条连接连发 16.30 毫秒
差值 1.83 毫秒
倍数 1.11 倍
本机回环上建连本来就便宜,所以倍数不大;跨城时每一次握手都是 40 毫秒,
五个请求就省下 160 毫秒——**这就是持久连接的全部价值**。
HTTP/1.1 默认就是持久连接,要关掉得显式写 `Connection: close`。
书里提到的这个默认值变化,是 1.0 到 1.1 最重要的一条改动。
① 18.13 毫秒 对 ② 16.30 毫秒,只有 1.11 倍——因为本机回环的建连太便宜(0.6 毫秒级),
省不出多少。
这个结果值得诚实说明:在本机测持久连接的收益,基本测不出来。
真实的收益在跨城(每次握手 40 毫秒)和 TLS(再叠一个往返)场景。
顺手踩了一个坑:如果用「读到对端关闭」的方式读响应,持久连接上永远等不到关闭,
只能等到超时——这会把「更快」测成「慢一百倍」。读响应必须按声明的长度读。
30 秒实验
- 把
n改成 50,再跑一次,看倍数有没有变化。 - 在服务端加一个
time.sleep(0.01)模拟处理时间,看两种走法的差距怎么变。 - 想想:如果连接池里的连接被对端悄悄关了,客户端怎么发现? (提示:发出去收不到响应,或者收到 RST)
常见误区
- 「keep-alive 就是长连接不断」——它有超时,空闲太久会被关。
- 「持久连接能提升单请求速度」——它省的是重复开销,单请求速度不变。
- 「连接越多越快」——浏览器对同域名的并发连接数是有限制的,太多反而拖慢(还要抢带宽)。
自测三问(点开对答案)
1. HTTP/1.1 默认保持连接还是关闭连接? —— 保持。要关得显式写 `Connection: close`。 2. 持久连接省下的开销有哪些? —— TCP 三次握手、慢启动爬坡,以及 HTTPS 下的 TLS 握手。 3. 为什么在持久连接上不能用「读到关闭」来读响应? —— 因为连接不会关,读到关闭等于等到超时。必须按 Content-Length 或分块编码读。连接能复用了。下一节看一个更激进的方案:请求也不用等回应。
- ☐ (本节完,下一节 2.7.2 管线化)