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

管线化允许不等响应就发下一个请求。 理论上能再省一个往返,实际上因为「响应必须按序回」而全线废弃。

📝 本节和前面几节的关系

上一节省了握手,这一节试图省掉「等响应」。 它是 HTTP/2 多路复用的前身,也是「为什么必须换协议」的最好例证。

一、管线化想解决什么

持久连接下,每个请求仍要等上一个响应回来才能发。五个请求还是一个接一个的五个往返。 管线化说:反正连接是通的,我把五个请求一次性全塞进去,服务端按顺序处理、按顺序回, 客户端一次收五个响应。理想情况下总耗时从「5 个往返」降到「1 个往返 + 处理时间」。 这个想法完全正确,问题全在「按顺序」这三个字上。

二、死穴:响应必须按序

协议要求响应顺序与请求顺序严格一致。 因为 HTTP/1.1 的响应里没有「这是第几个请求的响应」这个字段——客户端只能靠顺序配对。 于是第一个请求只要慢,后面四个就算早就算好了也只能堵着。 这个现象叫队头阻塞(Head-of-Line Blocking)。 Demo 里服务端按 /a → /b → /c 收到,响应也按这个顺序回来。 如果把 /a 做成 100 毫秒的慢接口,/b 和 /c 的响应时间就会各加 100 毫秒。 更糟的是中间设备:很多代理见到管线化请求会直接串行处理,甚至有的会因为乱序而错配响应。 所以浏览器全线关闭了管线化。

三、HTTP/2 怎么解决

加一个「流 ID」。每个请求有自己的编号,响应里带着同一个编号回来,于是可以乱序交错传。 这才真正解决了 HTTP 层的队头阻塞。 但 TCP 层的还在:所有流共用一条 TCP 连接,一个包丢了,后面的字节全得等重传——TCP 层队头阻塞。 所以 HTTP/3 干脆换到 QUIC(跑在 UDP 上),每个流独立重传。 这条路从 1999 年的管线化开始,走了二十年。

四、动手 Demo:一次发出三个请求,看响应按什么顺序回

把三个请求拼成一个字节串一次性写出,再连读三个响应,看服务端收到顺序与响应返回顺序是否一致。

s.sendall(_req(target=u'/a') + _req(target=u'/b') + _req(target=u'/c'))
parts = []
buf = b''
for _ in range(3):
    one, buf = H.recv_response(s, buf)
    parts.append(one)

实跑输出:

--------------------------------------------------------------------
管线化:一口气发三个,看响应按什么顺序回
--------------------------------------------------------------------
  一次写出请求数             3 个(共 108 字节)
  服务端按序收到             /a → /b → /c
  读回响应数                 3 个
    第 1 个响应              HTTP/1.1 200 OK,正文 resp-1
    第 2 个响应              HTTP/1.1 200 OK,正文 resp-2
    第 3 个响应              HTTP/1.1 200 OK,正文 resp-3
  三个请求一次发完耗时       0.36 毫秒

  请求是一次性全塞进去的,服务端也按 `/a → /b → /c` 的顺序收到,
  响应同样按这个顺序回来——**这正是管线化的死穴**。
  如果 `/a` 是个慢接口,`/b` 和 `/c` 就算算好了也得排队。
  而且中间代理对管线化的支持极差,所以浏览器全线关掉了这个功能。

一次写出请求数 3 个(共 108 字节)、服务端按序收到 /a → /b → /c、读回响应数 3 个, 正文依次是 resp-1、resp-2、resp-3。 顺序完全一致——不是服务端体贴,是协议要求它必须按这个顺序回。 耗时 0.36 毫秒,比一个一个发快得多。但这是没有慢请求的情况。 把 /a 加个 sleep(0.1),/b 和 /c 的等待时间立刻跟着涨。 注意读响应还是逐个按长度读——即使管线化,客户端也不能靠「一次 recv 拿三个」,TCP 是字节流, 切在哪完全由内核决定。

30 秒实验

  1. 给 /a 分支加 time.sleep(0.1),重跑,看三个响应的到达时间。
  2. 把三个请求的顺序改成 /c → /b → /a,看响应顺序是不是跟着变。
  3. 想一想:如果某个请求要让服务端返回 204(无正文),按序读还能不能对上?

常见误区

  1. 「管线化能提升吞吐」——理论上是,实测里因为 HOL 和代理不兼容,收益常是负的。
  2. 「HTTP/2 用管线化」——不是。HTTP/2 用多路复用,每流独立编号,可以乱序。
  3. 「管线化是浏览器的默认行为」——所有主流浏览器都关掉了。
自测三问(点开对答案) 1. 管线化的死穴是什么? —— 响应必须按请求顺序返回,一个慢请求会堵住后面所有请求。 2. HTTP/2 靠什么解决队头阻塞? —— 给每个请求分配流 ID,响应可以乱序交错传输。 3. HTTP/3 又要换协议的原因? —— HTTP/2 仍跑在 TCP 上,丢一个包会让整条连接的所有流一起等。QUIC 每流独立重传。

连接与并发讲完。下一节补上无状态缺的那一块:Cookie。

相关笔记