哈夫曼编码

https://httpwg.org/specs/rfc7541.html

简介

哈夫曼编码定义了HPACK压缩格式,用于有效的表示Http标头字段,以便在HTTP2中使用。

介绍

在HTTP / 1.1中(参见[RFC7230]),没有压缩标题字段。随着网页的增加,需要几十到数百个请求,这些请求中的冗余头字段不必要地消耗带宽,可测量地增加延迟。
SPDY 静态表,最初通过使用令人缩小的[缩小]格式压缩标题字段来解决此冗余,这在有效地表示冗余标题字段中证明非常有效。然而,这种方法暴露了犯罪(压缩比信息泄漏变得容易)攻击所证明的安全风险(见[犯罪])。

技术

  • Header Field Header Field 都用8个字节的键值对表示
  • Dynamic Table 动态表
  • Static Table 静态表 是一个表,静态关联频繁发生索引值的标头字段。此表已排序,只读,始终可访问,并且可以在所有编码或解码上下文中共享。
  • Header List 是一个有序集合的标题字段,它是联合编码的,可以包含重复的标题字段。HTTP / 2标题块中包含的标题字段的完整列表是标题列表。
  • Header Field Representation: 可以用编码形式表示为文字或索引
  • Header Block:有序的标头字段表示列表,当解码时,它会产生完整的标题列表

动态表可以包含相同的条目(即,具有相同名称和相同值的条目)。因此,重复条目不得被解码器视为错误。)

http2帧格式:
HTTP/2 会发送有着不同类型的二进制帧,但他们都有如下的公共字段:Type, Length, Flags, Stream Identifier 和 frame payload。本规范中一共定义了 10 种不同的帧,其中最基础的两种分别对应于 HTTP 1.1 的 DATA 和 HEADERS。

所有帧都以固定的 9 字节大小的头作为帧开始,后跟可变长度的有效载荷 payload。

+———————————————————————-+
| Length (24) |
+———————-+———————-+———————-+
| Type (8) | Flags (8) |
+-+——————-+———————-+———————————————-+
|R| Stream Identifier (31) |
+=+=============================================================+
| Frame Payload (0…) …
+———————————————————————————————-+
帧头的字段定义如下:

  • Length:
    帧有效负载的长度表示为无符号的 24 位整数。除非接收方为 SETTINGS_MAX_FRAME_SIZE 设置了较大的值(详情见这里),否则不得发送大于2 ^ 14(16,384)的值。帧头的 9 个八位字节不包含在此长度值中
  • Type:
    这 8 位用来表示帧类型的。帧类型确定帧的格式和语义。实现方必须忽略并丢弃任何类型未知的帧。
  • Flags:
    这个字段是为特定于帧类型的布尔标志保留的 8 位字段,为标志分配特定于指示帧类型的语义。没有为特定帧类型定义语义的标志必须被忽略,并且必须在发送时保持未设置 (0x0)。
传递HTTP包体 传递HTTP包体 传递HTTP包体
DATA 0x0 传递HTTP包体
HEADERS 0x1 传递HTTP包头
PRIORITY 0x2 指定Stream 流的优先级
RST_STREAM 0x3 终止Stream流
SETTINGS 0x4 修改连接或者Stream流的配置
PUSH_PROMISE 0x5 服务端推送资源时描述请求的帧
PING 0x6 心跳监测兼具测量RTT的功能
GOAWAY 0x7 优雅的终止错误或通知错误
WINDOW_UPDATE 0x8 实现流量控制,grpc里面需要经常发,维持滑动窗口
CONTINUATION 0x9 传递较大HTTP头部时的持续帧

常用的标志位有 END_HEADERS 表示头数据结束,相当于 HTTP/1 里头后的空行(“\r\n”),END_STREAM 表示单方向数据发送结束(即 EOS,End of Stream),相当于 HTTP/1 里 Chunked 分块结束标志(“0\r\n\r\n”)。

[

](https://camo.githubusercontent.com/16ac92c1dd8fddeeee639bfc4716f0e358b895ba76a013cfd00fdce0e26db92c/68747470733a2f2f696d672e68616c66726f73742e636f6d2f426c6f672f41727469636c65496d6167652f3132345f335f302e706e67)

抓包显示的帧结构的头部结构确实是开头 9 字节大小。Length 是 18,Type 是 4,Flags 标记位是 ACK,R 是保留位,对应上图抓包图中的 Reserved。Stream Identifier 是 0 。

总结一下,stream ID 的作用:

  • 实现多路复用的关键。接收端的实现可以根据这个 ID 并发组装消息。同一个 stream 内 frame 必须是有序的。SETTINGS_MAX_CONCURRENT_STREAMS 控制着最大并发数。websocket 原生协议由于没有这个 stream ID 类似的字段,所以它原生不支持多路复用。在同一个 stream 内部的 frame 由于没有其他的 ID 编号了,所以无法乱序,必须有序,无法并发(如果想要并发,可以再新启一个 stream)。
  • 推送依赖性请求的关键。客户端发起的流是奇数编号,服务端发起的流是偶数编号。

流量控制

使用 stream 流进行多路复用会引入使用 TCP 连接的争用,从而导致阻塞 stream 流。流量控制方案确保同一连接上的流不会破坏性地相互干扰。流量控制用于单个流和整个连接。

由于 HTTP/2 数据流在一个 TCP 连接内复用,TCP 流控制既不够精细,也无法提供必要的应用级 API 来调节各个数据流的传输。 为了解决这一问题,HTTP/2 提供了一组简单的构建块,这些构建块允许客户端和服务器实现其自己的数据流和连接级流控制。

HTTP/2 通过使用 WINDOW_UPDATE 帧提供流量控制

  1. 流量控制特定于某一个连接。两种类型的流量控制都在单跳的端点之间,而不是在整个端到端路径之间。即,可信的网络中间件可以使用它来控制资源使用,以及基于自身条件和启发式算法实现资源分配机制。
  2. 流量控制基于 WINDOW_UPDATE 帧。接收者通告他们准备在 stream 流以及整个连接上接收多少个八位字节。这是一种基于 credit 信用的方案。接收端设定上限,发送端应当遵循接收端发出的指令。
  3. 流量控制是定向的,接收者提供整体控制。接收者可以选择为每个流和整个连接设置所需的任何窗口大小。发送者必须遵守接收者施加的流量控制的限制。客户端,服务器和中间件,作为接收者,都需要独立地将其流量控制窗口进行广播,并遵守其对端在发送时设置的流量控制限制。
  4. 对于一个新的 strean 流和整体连接,流量控制窗口的初始值为 65,535 个八位字节。
  5. 帧类型确定流量控制是否适用于帧。在本文档中指定的帧中,只有 DATA 帧受流量控制;所有其他帧类型在广播其流量控制窗口的时候,不占用空间。这确保了重要的控制帧不会被流量控制阻挡。
  6. 无法禁用流量控制。建立 HTTP/2 连接后,客户端将与服务器交换 SETTINGS 帧,这会在两个方向上设置流控制窗口。 流控制窗口的默认值设为 65,535 字节,但是接收方可以设置一个较大的最大窗口大小(2^31-1 字节),并在接收到任意数据时通过发送 WINDOW_UPDATE 帧来维持这一大小。
  7. HTTP/2 仅定义 WINDOW_UPDATE 帧的格式和语义(第 6.9 节)。本文档未规定接收方如何决定何时发送此帧或其发送的值,也未规定发送方如何选择发送数据包。实现方能够选择任何适合其需求的算法。


    服务器和客户端都具备流量控制能力,发送和接收可以独立的设置流量控制。


实现方还负责管理基于优先级发送请求和响应的方式,选择如何避免请求的队首阻塞以及管理新的流的创建。这些算法的选择可以与流量控制算法相互作用。

2. 适当的使用流量控制


HTTP/2 未指定任何特定算法来实现流控制。

流量控制目的是为了保护在资源约束下工作的端点。例如,proxy 需要在许多连接之间共享内存,并且还可能具有较慢的上游连接和较快的下游连接。流量控制解决了接收者无法在一个流上处理数据但又想继续处理同一连接中的其他流的情况。

不需要此功能的部署可以广播最大大小的流量控制窗口 (2^31-1),并且可以在收到任何数据时通过发送 WINDOW_UPDATE 帧来维护此窗口。这有效地禁用了该接收者的流量控制。相反,发送方始终服从接收方广播的流量控制窗口。

具有受限资源的部署(例如,内存)可以使用流量控制来限制对端可能消耗的内存大小。但请注意,如果在不知道带宽延迟的情况下启用流量控制,则可能导致可用网络资源的次优使用(参见[RFC7323])。

即使完全了解当前的带宽延迟,流量控制的实现也很困难。使用流量控制时,接收者必须及时从 TCP 接收缓冲区读取。如果不读取并执行关键帧(例如 WINDOW_UPDATE),则不这样做可能会导致死锁。


单次rep 帧
+———————————————————————-+
| Length (24) |
+———————-+———————-+———————-+
| Type (8) | Flags (8) |
+-+——————-+———————-+———————————————-+
|R| Stream Identifier (31) |
+=+=============================================================+
| Frame Payload (0…) …
+———————————————————————————————-+

单次发送过程:
2022/03/04 12:02:21 http2: Framer 0xc0000d4000: wrote SETTINGS len=0
2022/03/04 12:02:21 http2: Framer 0xc0000d4000: read SETTINGS len=6, settings: MAX_FRAME_SIZE=16384
2022/03/04 12:02:21 http2: Framer 0xc0000d4000: wrote SETTINGS flags=ACK len=0
2022/03/04 12:02:21 http2: Framer 0xc0000d4000: read SETTINGS flags=ACK len=0
// 发送数据
2022/03/04 12:02:21 http2: Framer 0xc0000d4000: wrote HEADERS flags=END_HEADERS stream=1 len=95
2022/03/04 12:02:21 http2: Framer 0xc0000d4000: wrote DATA flags=END_STREAM stream=1 len=55 data=”\x00\x00\x00\x002\n0yfsdfsddddaaaaaasssssssssssssssssssssssssssssssy”
// 服务端winUpdate
2022/03/04 12:02:21 http2: Framer 0xc0000d4000: read WINDOW_UPDATE len=4 (conn) incr=55

// 服务端header
2022/03/04 12:02:27 http2: Framer 0xc0000d4000: read HEADERS flags=END_HEADERS stream=1 len=14
2022/03/04 12:02:27 http2: decoded hpack field header field “:status” = “200”
2022/03/04 12:02:27 http2: decoded hpack field header field “content-type” = “application/grpc”
// 服务端data
2022/03/04 12:02:27 http2: Framer 0xc0000d4000: read DATA stream=1 len=60 data=”\x00\x00\x00\x007\n5Helloyfsdfsddddaaaaaasssssssssssssssssssssssssssssssy”
2022/03/04 12:02:27 http2: Framer 0xc0000d4000: read HEADERS flags=END_STREAM|END_HEADERS stream=1 len=24
// 解码
2022/03/04 12:02:27 http2: decoded hpack field header field “grpc-status” = “0”
2022/03/04 12:02:27 http2: decoded hpack field header field “grpc-message” = “”
2022/03/04 12:02:27 http2: Framer 0xc0000d4000: wrote WINDOW_UPDATE len=4 (conn) incr=60
2022/03/04 12:02:27 http2: Framer 0xc0000d4000: wrote PING len=8 ping=”\x02\x04\x10\x10\t\x0e\a\a”