📘 第九章 基于HTTP的功能追加协议
📄 一句话概括

SPDY 在 HTTP 和 TCP 之间插了一层:把请求切成带流号的帧,在一条连接上交错发送,再给首部做压缩、 给流排优先级、还能主动推送。这套东西后来原样变成了 HTTP/2.0。

📝 本节和前面几节的关系

上一节把三个瓶颈量出来了,这一节看第一个动手的方案。 它是谷歌 2009 年的实验品,从没成为正式标准,但它的思路全活了。

一、插在中间的那一层

SPDY 不改 HTTP 的语义——方法还是那些方法,状态码还是那些状态码, 它只在 HTTP 和 TCP 之间加了一层「会话层」。 这一层干的事很朴素:把每个请求和响应切成小片,每片带一个流号, 然后按流号交错地塞进同一条 TCP 连接。接收端照流号重新拼起来。 因为有了流号,响应不必按请求顺序回来。 前面那条「响应必须按序」的硬规定就被绕过去了——这是它最核心的一步。

二、四件具体的事

多路复用:一条连接上跑很多条流,互不排队。这是主菜,其余三样是配菜。 请求优先级:给流标上优先级,让 HTML 和 CSS 这类「挡路的」先走,大图往后排。 没有优先级的话,一条大视频流会把整条连接占满。 首部压缩:首部里有大量重复,SPDY 用一张固定的表加一套编码把它压下来。 这套做法后来演进成 HTTP/2 的 HPACK(9.4 会实算一次)。 服务器推送:服务端可以主动把「你八成还要」的资源先送过去,不用等你发现再请求。 这是 HTTP 从来没有的能力。

三、帧交错的实测

三条流各切四片,每片 125 字节。信道有限,每发一帧占 4 毫秒; 流 1 的数据要 100 毫秒后才备好,流 2、流 3 立刻就绪。 交错发的完成时刻是 116/30/35 毫秒,排队发是 115/135/155 毫秒。 两种发法的总字节完全一样,都是 1500 字节。 看流 1:两种发法都是一百一十几毫秒,因为它自己慢,谁也救不了。 但流 2 提前了 105 毫秒,流 3 提前了 120 毫秒——差别全在这里。 所以多路复用的收益不是「让总时间变短」,是不让快的去等慢的。 这句话值得记下来,后面判断一个优化值不值,就看它有没有做到这一点。

四、动手 Demo:同样的 1500 字节,两种排法

用一对 socket 做一个真实的字节流,自己定义帧格式:4 字节流号加 1 字节片号,后面跟 120 字节负载。 三条流各四片,交错发一遍、排队发一遍,量每片到达的时刻。

def frame(sid, seq):
    return struct.pack(u'>IB', sid, seq) + bytes([64 + sid]) * 120

# 交错:谁就绪就发谁,轮着来
cand = [s for s in (1, 2, 3) if pos[s] < 4 and READY[s] <= now]
s = cand[rot % len(cand)]
sendall(frame(s, pos[s]))

实跑输出:

--------------------------------------------------------------------
帧交错:把「排队」换成「穿插」
--------------------------------------------------------------------
  三条流,每条切成四片,每片 125 字节(5 字节帧头 + 120 字节负载)。
  信道是有限的:每发一帧占 4 毫秒。流 1 的数据要 100 毫秒后才备好,
  流 2、流 3 立刻就绪。两种发法各走一遍,量每片真正到达的时刻。

  【交错发】接收顺序(毫秒/流/片号)
    流2-0@0  流3-0@5  流2-1@10
    流3-1@15  流2-2@20  流3-2@25
    流2-3@30  流3-3@35  流1-0@101
    流1-1@107  流1-2@112  流1-3@117

  【排队发】接收顺序(毫秒/流/片号)
    流1-0@101  流1-1@106  流1-2@111
    流1-3@116  流2-0@121  流2-1@126
    流2-2@131  流2-3@135  流3-0@140
    流3-1@145  流3-2@150  流3-3@155

  发法       流 1 完成    流 2 完成    流 3 完成    字节
  交错       117       30        35        1500
  排队       116       135       155       1500

  两种发法字节数             1500 对 1500(一个字节都没差)
  流 2 提前                  105 毫秒
  流 3 提前                  120 毫秒

  看流 1 那一列:两种发法都是 100 毫秒出头才完成,因为它自己慢,没救。
  但流 2、流 3 在交错发里几十毫秒就回来了,在排队发里被拖到 130 毫秒之后。
  这就是多路复用的真实收益:不是让总时间变短,是不让快的去等慢的。

  SPDY 正是把「一条连接 = 一条队伍」改成了「一条连接 = 很多条流」,

交错发的接收顺序是「流2-0、流3-0、流2-1、流3-1……」,三条流交替前进,每条都在动。 排队发的接收顺序是「流1-0、流1-1、流1-2、流1-3、流2-0……」,流 1 全走完才轮得到流 2,一条一条清空。 字节数 1500 对 1500,一个字节都没差。所以多路复用不是省带宽的技术,是省等待的技术。 流 2 提前 105 毫秒、流 3 提前 120 毫秒,而流 1 一动不动。 这就是「不让快的等慢的」的具体数值。

30 秒实验

  1. 把流 1 的就绪时间改成 0,看两种发法的差别还剩多少。
  2. 把每帧的信道占用从 4 毫秒改成 1 毫秒,看提前量怎么变。
  3. 想一想:如果三条流的数据量差十倍,没有优先级会发生什么?

常见误区

  1. 「多路复用能减少传输的字节」——字节一个不少,省的是等待。
  2. 「SPDY 是一个新协议,和 HTTP 无关」——语义全沿用,只是换了传输方式。
  3. 「交错发一定更快」——如果只有一条流,两种发法没区别。
自测三问(点开对答案) 1. SPDY 那一层插在哪里? —— 插在 HTTP 和 TCP 之间,把报文切成带流号的帧,在一条连接上交错发送。 2. 多路复用解决了哪个具体问题? —— 响应必须按序返回导致的队头阻塞——有了流号,响应可以乱序交错。 3. 多路复用省的是带宽还是等待? —— 等待。总字节数一点没变。

听起来完美。可它只动了 HTTP 那一层——底下还有一层没动。

相关笔记