【TCP网络通信】HTTP2:新时代下最常用的的HTTP协议版本

207 阅读7分钟

引言

因为日常有随手打开浏览器开发者工具的习惯,最近发现了一个现象

image.png

image.png

发现有的请求具有 row 选项,而有的请求没有 row 选项;就随手搜索了一些

image.png

然后通过攻略打开了F12的Protocol一栏,才发现HTTP2.0已经非常常用,在大多数网站来说已是主流,而且早在2015年就已经是RFC标准了。 (这下不得不学习了解一下了

协议特点

不得不说,HTTP2和HTTP1.1相比,真是变化巨大。HTTP2.0协议具有以下新特点:

  1. 二进制协议
  2. 多路复用
  3. 头部压缩
  4. 服务器推送

二进制分帧

大家都知道HTTP1.1是文本协议,文本协议虽然保留了易读性,但却带来了更多的带宽消耗;而HTTP2.0最直观的变化就是将文本协议转变为二进制协议,还引出了分帧的概念

与文本协议相比,二进制协议更容易解析,只需要按序将字节流转换为对象即可;而不需解析字符。

image.png

在将协议转化为二进制协议的前提下,HTTP2引入了分帧的概念,将整个HTTP数据包分为若干个帧

帧的结构

一个 HTTP/2 的帧结构如下:

+-----------------------------------------------+
|                 Length (24)                   |
+---------------+---------------+---------------+
|   Type (8)    |   Flags (8)   |
+-+-------------+---------------+-------------------------------+
|R|                 Stream Identifier (31)                      |
+=+=============================================================+
|                   Frame Payload (0...)                      ...
+---------------------------------------------------------------+
  • Length:3 个字节,表示该帧的数据长度(但不包括头的 9 个字节)。
  • Type:1 个字节,表示该帧后续的内容的类型(总共定义了 10 种类型的帧,大体上可以分为数据帧和控制帧两类)。
  • Flag:1 个字节,在 Type 不同的情况下有不同的定义。
  • R:一位保留位,目前未定义,且必须是 0。
  • Stream Identifier:流标识符 ID,也就是帧所属的“流”(后面会解释流的概念),接收方使用该 ID 可以从乱序的帧里识别出具有相同流 ID 的帧序列,并按照顺序重新组装起来,就实现了虚拟的“流”。
  • Frame Payload:该 Type 对应的帧 Payload。

Type

image.png

各Type下还有自己的flag,比如常用的

  1. END_STREAM (0x1)
  2. END_HEADERS (0x4)
  3. PADDED (0x8)

DATA帧

 +---------------+
 |Pad Length? (8)|
 +---------------+-----------------------------------------------+
 |                            Data (*)                         ...
 +---------------------------------------------------------------+
 |                           Padding (*)                       ...
 +---------------------------------------------------------------+

数据帧包含以下字段:

  • 填充长度(Pad Length):一个 8 位字段,包含以八进制为单位的帧填充长度。 该字段是有条件的(如图中的"?"符号所示),只有在设置了 PADDED 标志时才会出现。
  • 数据:应用数据。 数据量是帧有效载荷减去其他字段的长度后的剩余部分。
  • 填充:不包含应用语义值的填充八进制数。 发送时必须将填充八进制数设为零。

HEADERS帧

 +---------------+
 |Pad Length? (8)|
 +-+-------------+-----------------------------------------------+
 |E|                 Stream Dependency? (31)                     |
 +-+-------------+-----------------------------------------------+
 |  Weight? (8)  |
 +-+-------------+-----------------------------------------------+
 |                   Header Block Fragment (*)                 ...
 +---------------------------------------------------------------+
 |                           Padding (*)                       ...
 +---------------------------------------------------------------+

HEADERS 帧有效载荷包含以下字段:

  • Pad Length(填充长度):一个 8 位字段,包含以八位字节为单位的帧填充长度。 该字段仅在设置了 PADDED 标志时才会出现。
  • E:单比特标志,表示流依赖性是排他性的。 该字段仅在设置了 PRIORITY 标志时才会出现。
  • 流依赖性:该流依赖的 31 位流标识符。 该字段仅在设置了 PRIORITY 标志时才会出现。
  • 权重:一个无符号 8 位整数,代表流的优先权重。 在该值上加 1 可获得介于 1 和 256 之间的权重。 该字段仅在设置了 PRIORITY 标志时才会出现。
  • 头片段:一个头片段。
  • 填充:填充八位位组。

头部压缩

在同一个HTTP页面中,许多资源的Header是高度相似的,但是在HTTP2之前都是不会对其进行压缩的,这使得在多次传输中白白浪费了资源来进行重复无谓的操作。对头部进行压缩,可以减小头部所消耗的带宽。

image.png

虽然HTTP1.1已经有使用gzip算法等压缩payload,但被没有考虑头部;实际上HTTP的头部有非常大的压缩空间 例如,User-Agent、Host等几乎必须的字段完全有压缩空间

HTTP2为了压缩头部采取了HPACK算法

  1. 使用静态映射表
  2. 使用动态映射表
  3. 使用Huffman编码

静态映射表

对于一些常用的字段,HTTP2设置了静态映射表,这个表的内容存储在HTTP2协议内;

image.png httpwg.org/specs/rfc75…

如果Header Value是空的,则意味着值为动态;否则为静态值

对于动态值,使用哈夫曼编码压缩每个字符;HTTP2在协议里早就压缩好了 httpwg.org/specs/rfc75…

// 静态值格式
  0   1   2   3   4   5   6   7
+---+---+---+---+---+---+---+---+
| 1 |        Index (7+)         |
+---+---------------------------+
// 动态值格式(之后就转静态了
  0   1   2   3   4   5   6   7
+---+---+---+---+---+---+---+---+
| 0 | 1 |      Index (6+)       |
+---+---+-----------------------+
| H |     Value Length (7+)     |
+---+---------------------------+
| Value String (Length octets)  |
+-------------------------------+

动态映射表

HTTP在作为文本协议的时候,是可以随意添加自己的头部键值对的;同时,静态表只包含了 61 种高频出现在头部的字符串。为了处理不在静态表范围内的头部字符串以及保留头部的扩展性,从而设立了动态映射表。

比如,对于User-Agent键值对,静态表中是没有的,HTTP2在发送时会将对应值进行哈夫曼编码,然后,客户端和服务器双方都会更新自己的动态表,添加一个新的 Index 号 62。那么在下一次发送的时候,就不用重复发这个字段的数据了,只用发 1 个字节的 Index 号就好了,因为双方都可以根据自己的动态表获取到字段的数据

// 添加新的
  0   1   2   3   4   5   6   7
+---+---+---+---+---+---+---+---+
| 0 | 1 |           0           |
+---+---+-----------------------+
| H |     Name Length (7+)      |
+---+---------------------------+
|  Name String (Length octets)  |
+---+---------------------------+
| H |     Value Length (7+)     |
+---+---------------------------+
| Value String (Length octets)  |
+-------------------------------+
// 被添加后
  0   1   2   3   4   5   6   7
+---+---+---+---+---+---+---+---+
| 1 |        Index (7+)         |
+---+---------------------------+

image.png

过程对照 httpwg.org/specs/rfc75…

多路复用

HTTP2与HTTP1.x的请求方式发生了根本性的变化,可以真正同时发送多个请求并处理多个请求。在HTTP1.1以前,HTTP完全使用请求响应模型,当前请求的发出需要等到上一个请求的响应;虽然HTTP1.1引入了管道的概念使得当前请求的发送不必等待上一个响应,但服务器依然是按序处理的,故HTTP1.x存在严重的队头阻塞问题

队头阻塞:由于服务器处理当前请求队列的队首请求速度过慢,从而导致后续请求只能在队列里排队。 虽然HTTP1.1引入了pipline的概念,但依然无法处理队头阻塞问题,因为服务器依然是按序处理,所以只能依靠并发连接与域名分片来缓解

在多路复用中,引入了的概念

  1. 在HTTP2中,对一个域名的连接只使用一条TCP连接。
  2. 一条TCP中可以同时传输多个Stream,一次请求响应可视为一个流
  3. 一个流被拆分为多个帧,也就是上面所说的帧

在这里,不同流的帧是可以乱序发送的(但同一个流的帧是有序的),这就给并发处理带来了可能性;有了乱序发送就可以启用多个线程接收不同序号的请求.

image.png

服务器推送

HTTP2增加了服务器向客户端推送的数据的能力,但又不同于WebSocket。

HTTP2是在客户端请求某个资源的同时,预测并主动推送客户端可能需要的其他资源。例如,当客户端请求一个 HTML 页面时,服务器可以预测到客户端可能还需要该页面引用的 CSS 文件和 JavaScript 文件,于是服务器会提前将这些文件推送给客户端。主要是加快页面加载速度。

而WebSocket则是用于客户端与服务器互相通信,一旦建立连接,双方可以随时互发消息;主要是为了解决通信问题。

客户端的流使用奇数 服务器推送的流使用偶数

image.png

缺陷

HTTP2中虽然有多路复用,但依然存在队头阻塞,不过这个阻塞是由TCP造成的,因为TCP保证数据的有序性,如果存在一个包丢了,为了有序处理,后续的包就会被阻塞在TCP缓冲区。为了解决这个问题,HTTP3放弃了TCP,改用了基于UDP的QUIC协议。这个可在HTTP3的文档中学习

引用

juejin.cn/post/697714…

juejin.cn/post/707993…

httpwg.org/specs/rfc75…

httpwg.org/specs/rfc75…

xiaolincoding.com/network/2_h…