Http2

Http2

基本语义:数据流、消息和帧

新的二进制分帧机制改变了客户端与服务器之间交换数据的方式。为了说明这个过程,我们需要了解 HTTP/2 的三个概念:

  • 数据流:已建立的连接内的双向字节流,可以承载一条或多条消息。
  • 消息:与逻辑请求或响应消息对应的完整的一系列帧。
  • :HTTP/2通信的最小单位,每个帧都包含帧头,至少也会标识出当前帧所属的数据流。

这些概念的关系总结如下:

  • 所有通信都在一个TCP连接上完成,此连接可以承载任意数量的双向数据流。
  • 每个数据流都有一个唯一的标识符可选的优先级信息,用于承载双向消息。
  • 每条消息都是一条逻辑HTTP消息(例如请求或响应),包含一个或多个帧。
  • 帧是最小的通信单位,承载着特定类型的数据,例如HTTP标头、消息负载等等。来自不同数据流的帧可以交错发送,然后再根据每个帧头的数据流标识符重新组装。

二进制分帧

HTTP2所有性能增强的核心在于新的二进制分帧层,它定义了如何封装HTTP消息并在客户端与服务器之间传输。

这里所谓的“层”,指的是位于套接字接口与应用可见的高级HTTP API之间一个经过优化的新编码机制:HTTP的语义(包括各种动词、方法、标头)都不受影响,不同的是传输期间对它们的编码方式变了。HTTP/1.x协议以换行符作为纯文本的分隔符,而HTTP/2将所有传输的信息分割为更小的消息和帧,并采用二进制格式对它们编码。

二进制帧详解

所有HTTP/2改进的核心是新的、长度固定的二进制帧层。与换行符分隔的纯文本HTTP 1.x协议相比,二进制成帧提供了更紧凑的表示形式,不仅处理效率更高,而且更易于正确实现。

建立HTTP/2连接后,客户端和服务器就通过交换帧进行通信,这些帧是协议中最小的通信单元。所有帧使用一个公共的9字节报头,其中包含帧的长度,其类型,标志的位字段以及31位的流标识符。

具体内容:

  • 24位长度字段允许单个帧携带最多字节数据。
  • 8位类型字段确定帧的格式和语义。
  • 8位标志字段传达特定于帧类型的布尔标志。
  • 1位保留字段始终设置为0。
  • 31位流标识符唯一标识HTTP/2流。

如何保证帧的正确性?

对于TCP来说,HTTP/2也是跑上面的私有协议,而TCP有一个特点就是TCP是稳定的。所以TCP协议会保证另外一端收到的消息是完整的。换言之,如果将一条HTTP消息拆成三个帧,则TCP协议将保证我们一定可以在HTTP2层拿到完整的三个帧,不会存在缺少某一帧的情况。

HTTP2帧类型:

  • DATA:用于传输HTTP消息正文。
  • HEADERS:用于传达流的Header字段。
  • PRIORITY:用于传达流的发送者建议的优先级。
  • RST_STREAM:用于信号流终止。
  • SETTINGS:用于传达连接的配置参数。
  • PUSH_PROMISE:用于表示承诺提供引用资源。
  • PING:用于测量往返时间并执行“活性”检查。
  • GOAWAY:用于通知对端停止为当前连接创建流。
  • WINDOW_UPDATE:用于实现流和连接流控制。
  • CONTINUATION:用于继续一系列Header块片段。

多路复用

在HTTP/1.x中,如果客户端要想发起多个并行请求以提升性能,则必须使用多个TCP连接。这是HTTP/1.x交付模型的直接结果,该模型可以保证每个连接每次只交付一个响应(响应排队)。更糟糕的是,这种模型也会导致队首阻塞,从而造成底层TCP连接的效率低下。

HTTP/2中新的二进制分帧层突破了这些限制,实现了完整的请求和响应复用:客户端和服务器可以将 HTTP 消息分解为互不依赖的帧,然后交错发送,最后再在另一端把它们重新组装起来。

上图展示了同一个连接内并行的多个数据流。客户端正在向服务器传输一个DATA帧(数据流5),与此同时,服务器正向客户端交错发送数据流1数据流3的一系列帧。因此,一个连接上同时有三个并行数据流。

数据流优先级

将HTTP消息分解为很多独立的帧之后,我们就可以复用多个数据流中的帧,客户端和服务器交错发送和传输这些帧的顺序就成为关键的性能决定因素。为了做到这一点,HTTP/2标准允许每个数据流都有一个关联的权重和依赖关系:

  • 可以向每个数据流分配一个介于 1 至 256 之间的整数。
  • 每个数据流与其他数据流之间可以存在显式依赖关系。

数据流依赖关系和权重的组合让客户端可以构建和传递“优先级树”,表明它倾向于如何接收响应。反过来,服务器可以使用此信息通过控制CPU、内存和其他资源的分配设定数据流处理的优先级,在资源数据可用之后,带宽分配可以确保将高优先级响应以最优方式传输至客户端。

HTTP/2内的数据流依赖关系通过将另一个数据流的唯一标识符作为父项引用进行声明;如果忽略标识符,相应数据流将依赖于“根数据流”。声明数据流依赖关系指出,应尽可能先向父数据流分配资源,然后再向其依赖项分配资源。换句话说,“请先处理和传输响应D,然后再处理和传输响应C”。

共享相同父项的数据流(即,同级数据流)应按其权重比例分配资源。例如,如果数据流A的权重为12,其同级数据流B的权重为4,那么要确定每个数据流应接收的资源比例,请执行以下操作:

  • 将所有权重求和:4 + 12 = 16
  • 将每个数据流权重除以总权重:A = 12/16, B = 4/16

头压缩

每个HTTP传输都带有一组头,这些头描述了所传输的资源及其属性。在HTTP/1.x中,此元数据始终以纯文本形式发送,每次传输会增加500-800字节的开销,如果使用HTTP cookie,则有时会增加数千字节。为了减少这种开销并提高性能,HTTP/2使用HPACK压缩格式压缩请求和响应头元数据,该格式使用两种简单但功能强大的技术:

  1. 它允许通过静态霍夫曼码对发送的标头字段进行编码,从而减小了它们各自的传输大小。
  2. 它要求客户端和服务器都维护和更新以前看到的标头字段的索引列表(即,建立共享的压缩上下文),然后将该列表用作有效编码先前传输的值的参考。

霍夫曼编码允许在传输时压缩各个值,而先前传输的值的索引列表使我们可以通过传输索引值来编码重复值(下图),该索引值可用于有效地查找和重建完整的标头键和值。

头压缩有两张表,分别是动态表和静态表,截取一部分HPACK STATIC TABLE如下:

与动态表一起组成了索引表:

以常用的User-Agent为例,它在静态表中的索引值是58,它的值是不存在表中的,因为它的值是多变的。第一次请求的时候它的key58表示,表示这是一个User-Agent,它的值部分会进行霍夫曼编码(如果编码后的字符串变更长了,则不采用霍夫曼编码)。

服务端收到请求后,会将这个User-Agent添加到动态表中(Dynamic Table)缓存起来,分配一个新的索引值。客户端下一次请求时,假设上次请求User-Agent的在表中的索引位置是62,此时只需要发送0xBE(高位置1,表示这个字节是一个完全索引值,即key和value都在索引中),便可以代表具体的值比如:User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/33.0.1750.146 Safari/537.36

服务端推送

HTTP/2的另一个强大新功能是服务器能够为单个客户端请求发送多个响应。也就是说,除了对原始请求的响应外,服务器还可以将 其他资源推送到客户端,而客户端不必显式地请求每个资源。

所有服务器推送流均通过PUSH_PROMISE帧启动,这表明服务器打算将所描述的资源推送到客户端,并且需要在请求推送资源的响应数据之前进行传递。此交付顺序至关重要:客户端需要知道服务器打算推送哪些资源,以避免对这些资源创建自己的和重复的请求。满足此要求的最简单策略是PUSH_PROMISE在父级响应(即DATA帧)之前发送所有仅包含承诺资源的HTTP标头的帧。

客户端一旦接收到PUSH_PROMISE帧,就可以选择是否拒绝流(通过RST_STREAM帧)(例如,资源已经在缓存中),这是对HTTP/1.x的重要改进。相比之下,对HTTP/1.x而言,资源内联的使用是一种流行的“优化”,等效于“强制推送”:客户端无法选择退出,取消它或单独处理内联的资源。

使用HTTP/2,客户端可以完全控制服务器推送的使用方式。客户端可以限制并发推送的流的数量;调整初始流控制窗口,以控制首次打开流时推送多少数据;完全禁用服务器推送。这些首选项通过SETTINGS帧在HTTP/2连接开始时进行通信,并且可以随时更新。

为了消除客户端启动的流和服务器启动的流之间的流ID冲突,计数器要偏移:客户端启动的流具有奇数的流ID,而服务器启动的流具有偶数的流ID。

流量控制

为什么不依赖于TCP的流控?

由于HTTP/2流是在单个TCP连接中多路复用的,因此TCP流控制既不够精细,又没有提供必要的应用程序级API来调节各个流的传递。

解决:

HTTP/2提供了一组简单的构建块,它们允许客户端和服务器实现自己的流级和连接级流控制:

  • 流量控制是定向的。每个接收方都可以选择设置每个流和整个连接所需的任何窗口大小。
  • 流量控制是基于信用的。每个接收方都通告其初始连接和流控制窗口(以字节为单位),每当发送方发出一个DATA帧时,该窗口就会减小。发送方通过发送WINDOW_UPDATE帧来告诉对方增加该窗口。
  • 流量控制无法禁用。建立HTTP/2连接后,客户端和服务器交换SETTINGS帧,这将在两个方向上设置流​​控制窗口的大小,流控制窗口的默认值设置为65535字节。但是接收方可以设置较大的最大窗口大小(2的31次方 - 1 字节),并且当它收到数据时,通过回复WINDOW_UPDATE帧来维护窗口大小。
  • 流控制是逐跳的,不是端到端的。也就是说,中介可以使用它来控制资源使用并根据自己的标准和启发式方法实施资源分配机制。

HTTP/2没有指定用于实现流控制的任何特定算法。取而代之的是,它提供了简单的构建块,并将实现放到到客户端和服务器,可以使用它来实施自定义策略来规范资源使用和分配,以及实现可以帮助改善实际性能和感知性能的新功能。

例如,应用程序层流控制允许浏览器仅获取特定资源的一部分,通过将流流控制窗口减小到零来暂停获取,然后稍后再恢复。

具体算法:

  • 发送端保有一个流量控制窗口(window)初始值。初始值的设定请参考SETTING帧SETTINGS_INITIAL_WINDOW_SIZE
  • 发送端每发送一个DATA帧,就把window递减,递减量为这个帧的大小。如果当前window小于帧大小,那么这个帧就必须被拆分到不大于window,如果window等于0,就不能发送任何帧。
  • 接收端可以发送WINDOW_UPDATE帧给发送端,发送端以帧内指定的Window Size Increment作为增量,加到window上。

流量控制只适用于数据帧

启动新流

HEADERS帧用于声明和传达有关新请求的元数据。应用程序有效负载在DATA帧内独立传递。

把HEADER帧和DATA帧分开允许协议将“控制流量”的处理与应用数据的传递分离开。例如,流控制仅应用于DATA帧,而非DATA帧始终以高优先级进行处理。

发送数据

创建了新的流,并发送了HTTP头,则使用DATA帧发送应用程序有效负载。有效负载可以在多个DATA帧之间分配,最后一个帧通过切换帧头中的END_STREAM标志来指示消息的结尾。

End Stream标志被设置为false,表示客户端尚未完成应用程序有效载荷的发送;更多的DATA帧即将到来。

引用文章

[1] https://hpbn.co/http2/#http2-frame-header
[2] https://juejin.im/entry/5dba82c3e51d452a17370818#comment
[3] http://http2.github.io/http2-spec/compression.html
[4] https://blog.csdn.net/u010129119/article/details/79361949#1-%E7%AE%80%E4%BB%8B