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

HTTP/2.0 一个字都没改 HTTP 的语义——方法还是那些,状态码还是那些,改的全是「怎么传」: 二进制分帧、多路复用、首部压缩、优先级、服务器推送、流量控制。

📝 本节和前面几节的关系

上一节 WebSocket 是另起炉灶。这一节是正主:把 SPDY 那套思路标准化,成为 RFC 7540。 它是这本书里唯一一个「你每天都在用」的新协议。

一、语义不动,只换传输

这一点最容易被误解。HTTP/2 里 GET 还是 GET,404 还是 404,Cache-Control 还是那个意思。 所有你在前八章学的东西一条都没作废。 变的只有一件事:报文不再是一行行文本,而是二进制帧。 每个帧有 9 字节固定头:3 字节长度、1 字节类型、1 字节标志、4 字节流号。 9 字节的头,比 HTTP/1.1 那 232 字节的首部小得多,而且它只是「信封」,里面的首部另有压缩方案。

二、改动集中在这几处

二进制分帧:一切皆帧。 首部是 HEADERS 帧,正文是 DATA 帧,控制信息是 SETTINGS、WINDOW_UPDATE、RST_STREAM 这些。 多路复用:一条连接上跑很多条流,用流号区分,响应可以乱序交错。 9.2.2 那个 1500 字节的实验就是这个机制的模型。 首部压缩:用一张固定的 61 项静态表加增量表,重复的首部只发一个索引。这套算法叫 HPACK。 请求优先级与流依赖:给流标权重和依赖关系,让挡路的资源先走。 服务器推送:服务端可以主动推一份「你马上要」的资源,省掉一个往返。 浏览器后来对这个功能兴趣不大,实际用得很少。 流量控制:每条流和整条连接各有一份窗口,接收方说了算,避免一条大流把别人挤死。

三、怎么谈成 h2

跑在 TLS 上(也就是 https)时,用 ALPN 在握手阶段商量:客户端报 h2 和 http/1.1,服务端挑一个。 挑中了 h2,后面的字节就按帧来解。 明文(http)也有一条路:先用 HTTP/1.1 发一个带 Upgrade: h2c 的请求,服务端回 101,然后切换。 这条路实际很少用,因为主流浏览器只在 TLS 上开 h2。 还有一条不成文的规矩:所有主流实现都要求 HTTP/2 必须加密。 规范没强制,但现实中不加密的 h2 基本见不到。

四、动手 Demo:手工发前言和 SETTINGS,看服务器回什么

连一台真服务器,ALPN 只报 h2。 握手成功后先发 24 字节的连接前言,再发一个空的 SETTINGS 帧,然后把服务器回来的帧一个个拆开看。 最后按 HPACK 静态表算一遍首部能压掉多少。

PREFACE = b'PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n'   # 24 字节
sendall(PREFACE)
sendall(b'\x00\x00\x00\x04\x00\x00\x00\x00\x00')  # 空 SETTINGS

# 帧头 9 字节:3 长度 + 1 类型 + 1 标志 + 4 流号
ln  = (h[0] << 16) | (h[1] << 8) | h[2]
typ, flags = h[3], h[4]
sid = struct.unpack(u'>I', h[5:9])[0] & 0x7FFFFFFF

实跑输出:

--------------------------------------------------------------------
HTTP/2.0:把前言和 SETTINGS 帧手工发过去
--------------------------------------------------------------------
  ALPN 谈成                  h2
  TLS 版本                   TLSv1.3
  前言                       24 字节:PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n

  收到 3 帧。每帧的固定头 9 个字节:3 长度 + 1 类型 + 1 标志 + 4 流号

  SETTINGS       长 18     标志 0    流 0   
        参数 MAX_CONCURRENT_STREAMS   = 100
        参数 INITIAL_WINDOW_SIZE      = 65536
        参数 MAX_FRAME_SIZE           = 16777215
  WINDOW_UPDATE  长 4      标志 0    流 0   
        窗口增加 2147418112 字节
  SETTINGS       长 0      标志 1    流 0   (ACK)

  服务端第一帧类型           SETTINGS

  HTTP/1.1 明文首部         174 字节
  HPACK 静态表索引后         85 字节
  省下                       51%

  这一行是实算的::method GET、:scheme https、:path / 在静态表里有现成的
  条目,一个字节就够;:authority、user-agent 这类只借个名字,值还得发。
  没算 Huffman 编码,所以真实值只会比 85 更小。

  两点如实说明:这次只做了「握手」(前言加 SETTINGS),没发 HEADERS 帧,
  所以没取到页面——手写 HPACK 编码不是这一节的目的。
  另外服务器回的几个 SETTINGS 值因机器而异,你重跑可能不一样,那是正常的。

ALPN 谈成 h2,TLS 版本 TLSv1.3。 前言 24 字节,内容是 PRI * HTTP/2.0 加 SM 两段,中间空一行——这是规定的开场白。 服务器回了 3 帧:第一帧是 SETTINGS,长 18 字节,里面三个参数——MAX_CONCURRENT_STREAMS = 100、 INITIAL_WINDOW_SIZE = 65536、MAX_FRAME_SIZE = 16777215。 第二帧是 WINDOW_UPDATE,把连接级窗口开到 2147418112 字节; 第三帧是 SETTINGS 带 ACK 标志、长度 0——它在确认收到我们的 SETTINGS。 这三个参数因服务器而异,你重跑可能不一样。 这一点必须说清楚:它们反映的是那台机器的配置,不是协议规定值。 首部那边:同样的请求,HTTP/1.1 明文 174 字节,按 HPACK 静态表索引后 85 字节,省 51%。 这里没算 Huffman 编码,所以真实值只会更小。

30 秒实验

  1. 把 PREFACE 里那个 SM 改成别的,看服务器会不会立刻断开。
  2. 把首部里的 User-Agent 换成一个超长字符串,看压缩率怎么变。
  3. 只报 h2 去连一个只支持 HTTP/1.1 的老站点,看握手会怎样失败。

常见误区

  1. 「HTTP/2 是新协议,以前的规则都不算了」——语义完全没变,只是传输换了。
  2. 「HTTP/2 一定比 HTTP/1.1 快」——资源很少、或者一条大文件下载时,差别很小。
  3. 「h2 只能在 TLS 上跑」——规范允许明文 h2c,只是现实中基本见不到。
自测三问(点开对答案) 1. HTTP/2 改了语义还是改了传输? —— 只改传输。方法、状态码、首部含义全都不变。 2. 一个 HTTP/2 帧的固定头是多少字节? —— 9 字节:3 长度、1 类型、1 标志、4 流号。 3. https 下怎么谈成 h2? —— TLS 握手时用 ALPN 报 `h2`,服务端选中即生效。

协议这一路讲完了。最后一个方向最朴素:不换协议,只给 HTTP 加几个方法,专门管文件。

相关笔记