📘 第六章 HTTP首部
📄 一句话概括

Connection 管两件事:这条连接用完关不关,以及哪些字段只对本跳有效。 它是通用首部里唯一身兼两职的。

📝 本节和前面几节的关系

上一节说的是缓存,这一节回到连接本身。顺便把 6.2.6 讲过的逐跳标记落到具体字段上。

一、两个版本,默认值相反

HTTP/1.1 默认保持连接,所以什么都不写就是持久连接;要关掉得显式写 Connection: close。 HTTP/1.0 默认关闭连接,要保持得显式写 Connection: keep-alive。 这个「默认值相反」是很多人困惑的来源:看到 keep-alive 就以为「开了持久连接」, 其实在 1.1 里它只是把默认行为说了一遍。 实测差别很直接:五次请求,close 用了 35.08 毫秒,keep-alive 只用 2.30 毫秒。 差十五倍——因为前者建了五次连接,后者只建了一次。

二、它的第二个身份:逐跳标记

Connection 的值除了 close 和 keep-alive,还可以是一串字段名。 写了谁的名字,谁就是逐跳字段,代理转发时必须删掉。 Connection: close, X-Secret 就是「用完就关,并且 X-Secret 只在本跳有效」。 这个设计的好处是不用改标准就能约定私有字段的寿命。 坏处是它很容易被忘掉——大量代理只处理 close 和 keep-alive,见到别的一律当没看见, 于是那些字段就被错误地转发出去了。 所以如果你在设计内部协议,想用一个只对本跳有效的字段,用它之前最好确认链路上的每一跳都真的会照做。

三、实测:建连成本有多高

Demo 里两个数字的差距主要来自 TCP 三次握手,外加每次新建线程的开销。 本机回环上握手本来很便宜,还能差出十五倍,说明这个成本不是可以忽略的。 跨城网络里这个差距会更大:握手要一个往返,而这个往返动辄几十毫秒。 五个请求各建一次连接,就是五个往返。 这也解释了为什么 HTTP/1.1 的持久连接是这么重要的一项改进, 以及为什么 HTTP/2 干脆把「一条连接上并发多路」做成了默认。

四、动手 Demo:close 与 keep-alive 的耗时对比

同一个服务端,先用 Connection: close 发五次请求(每次新连接), 再用 Connection: keep-alive 在一条连接上连发五次,各计时一次。

# 一次一连接
for _ in range(5):
    X.ask(port, X.req(u'GET', u'/', u'e.example',
                        [(u'Connection', u'close')]))

# 一条连接五个请求
s = socket.create_connection((u'127.0.0.1', port))
pending = b''
for _ in range(5):
    s.sendall(X.req(u'GET', u'/', u'e.example',
                      [(u'Connection', u'keep-alive')]))
    raw, pending = H.recv_response(s, pending)

实跑输出:

--------------------------------------------------------------------
Connection: close 与 keep-alive 的实际差别
--------------------------------------------------------------------
  Connection: close 五次     47.49 毫秒 | 处理 5 次(一次一连接)
  keep-alive 五次            2.78 毫秒 | 服务端处理 5 个请求
  连接数                     1 条(5 个请求排队走它)

  HTTP/1.1 默认就是持久连接,所以不写 Connection 也会保持。
  想关掉才需要显式写 close。反过来,HTTP/1.0 默认关闭,
  要靠 Connection: keep-alive 才能保持——这是两个版本的默认值相反之处。

  还有第三种用法,就是上一节讲的:Connection: X-Foo 把 X-Foo 标记成逐跳。
  也就是说这个字段既能控连接,又能给别的字段打「只对本跳有效」的标签。

close 五次:35.08 毫秒,服务端处理了 5 次,一次一连接。 keep-alive 五次:2.30 毫秒,服务端处理 5 个请求,只用 1 条连接。 时间差 15 倍,省下来的全是建连和拆连的成本。注意这里还只是本机回环,没有任何网络延迟。 第二次读响应用的是 H.recv_response,不是 read_all——持久连接上连接不关, read_all 会一直等到超时。这个坑在写客户端时非常常见。

30 秒实验

  1. 把 keep-alive 那一组改成二十次,看耗时是不是几乎不涨。
  2. 发一个 Connection: X-Trace 加一个 X-Trace 首部,用 6.2.6 那个代理试试。
  3. 想一想:HTTP/1.0 客户端不写 Connection: keep-alive,服务端为什么必须关连接?

常见误区

  1. 「keep-alive 是打开持久连接的开关」——在 1.1 里持久连接是默认值。
  2. 「Connection 只能写 close 和 keep-alive」——还能写一串字段名做逐跳标记。
  3. 「持久连接会一直占着不放」——空闲超时后服务端会主动关。
自测三问(点开对答案) 1. HTTP/1.1 里要关连接得写什么? —— `Connection: close`。不写就是保持连接。 2. `Connection: X-Foo` 的作用是什么? —— 把 X-Foo 标记成逐跳字段,代理转发时必须删掉它。 3. 持久连接省的是什么开销? —— 主要是 TCP 三次握手和四次挥手,也就是一个往返的时间。

连接管完了,下一个字段是每个响应都该有的那个时间戳。

相关笔记