引言
因为日常有随手打开浏览器开发者工具的习惯,最近发现了一个现象
发现有的请求具有 row 选项,而有的请求没有 row 选项;就随手搜索了一些
然后通过攻略打开了F12的Protocol一栏,才发现HTTP2.0已经非常常用,在大多数网站来说已是主流,而且早在2015年就已经是RFC标准了。
(这下不得不学习了解一下了
协议特点
不得不说,HTTP2和HTTP1.1相比,真是变化巨大。HTTP2.0协议具有以下新特点:
- 二进制协议
- 多路复用
- 头部压缩
- 服务器推送
二进制分帧
大家都知道HTTP1.1是文本协议,文本协议虽然保留了易读性,但却带来了更多的带宽消耗;而HTTP2.0最直观的变化就是将文本协议转变为二进制协议,还引出了分帧的概念
与文本协议相比,二进制协议更容易解析,只需要按序将字节流转换为对象即可;而不需解析字符。
在将协议转化为二进制协议的前提下,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
各Type下还有自己的flag,比如常用的
- END_STREAM (0x1)
- END_HEADERS (0x4)
- 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之前都是不会对其进行压缩的,这使得在多次传输中白白浪费了资源来进行重复无谓的操作。对头部进行压缩,可以减小头部所消耗的带宽。
虽然HTTP1.1已经有使用gzip算法等压缩payload,但被没有考虑头部;实际上HTTP的头部有非常大的压缩空间 例如,User-Agent、Host等几乎必须的字段完全有压缩空间
HTTP2为了压缩头部采取了HPACK算法
- 使用静态映射表
- 使用动态映射表
- 使用Huffman编码
静态映射表
对于一些常用的字段,HTTP2设置了静态映射表,这个表的内容存储在HTTP2协议内;
如果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+) |
+---+---------------------------+
多路复用
HTTP2与HTTP1.x的请求方式发生了根本性的变化,可以真正同时发送多个请求并处理多个请求。在HTTP1.1以前,HTTP完全使用请求响应模型,当前请求的发出需要等到上一个请求的响应;虽然HTTP1.1引入了管道的概念使得当前请求的发送不必等待上一个响应,但服务器依然是按序处理的,故HTTP1.x存在严重的队头阻塞问题
队头阻塞:由于服务器处理当前请求队列的队首请求速度过慢,从而导致后续请求只能在队列里排队。 虽然HTTP1.1引入了pipline的概念,但依然无法处理队头阻塞问题,因为服务器依然是按序处理,所以只能依靠并发连接与域名分片来缓解
在多路复用中,引入了流与帧的概念
- 在HTTP2中,对一个域名的连接只使用一条TCP连接。
- 一条TCP中可以同时传输多个Stream,一次请求响应可视为一个流
- 一个流被拆分为多个帧,也就是上面所说的帧
在这里,不同流的帧是可以乱序发送的(但同一个流的帧是有序的),这就给并发处理带来了可能性;有了乱序发送就可以启用多个线程接收不同序号的请求.
服务器推送
HTTP2增加了服务器向客户端推送的数据的能力,但又不同于WebSocket。
HTTP2是在客户端请求某个资源的同时,预测并主动推送客户端可能需要的其他资源。例如,当客户端请求一个 HTML 页面时,服务器可以预测到客户端可能还需要该页面引用的 CSS 文件和 JavaScript 文件,于是服务器会提前将这些文件推送给客户端。主要是加快页面加载速度。
而WebSocket则是用于客户端与服务器互相通信,一旦建立连接,双方可以随时互发消息;主要是为了解决通信问题。
客户端的流使用奇数 服务器推送的流使用偶数
缺陷
HTTP2中虽然有多路复用,但依然存在队头阻塞,不过这个阻塞是由TCP造成的,因为TCP保证数据的有序性,如果存在一个包丢了,为了有序处理,后续的包就会被阻塞在TCP缓冲区。为了解决这个问题,HTTP3放弃了TCP,改用了基于UDP的QUIC协议。这个可在HTTP3的文档中学习