📘 第二章 简单的HTTP协议
📄 一句话概括

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 秒实验

  1. 把 n 改成 50,再跑一次,看倍数有没有变化。
  2. 在服务端加一个 time.sleep(0.01) 模拟处理时间,看两种走法的差距怎么变。
  3. 想想:如果连接池里的连接被对端悄悄关了,客户端怎么发现? (提示:发出去收不到响应,或者收到 RST)

常见误区

  1. 「keep-alive 就是长连接不断」——它有超时,空闲太久会被关。
  2. 「持久连接能提升单请求速度」——它省的是重复开销,单请求速度不变。
  3. 「连接越多越快」——浏览器对同域名的并发连接数是有限制的,太多反而拖慢(还要抢带宽)。
自测三问(点开对答案) 1. HTTP/1.1 默认保持连接还是关闭连接? —— 保持。要关得显式写 `Connection: close`。 2. 持久连接省下的开销有哪些? —— TCP 三次握手、慢启动爬坡,以及 HTTPS 下的 TLS 握手。 3. 为什么在持久连接上不能用「读到关闭」来读响应? —— 因为连接不会关,读到关闭等于等到超时。必须按 Content-Length 或分块编码读。

连接能复用了。下一节看一个更激进的方案:请求也不用等回应。

相关笔记