浅谈http2协议
http/1.x协议有什么问题
http协议(hypertext transfer protocol)是目前运用最成功的应用层协议,几乎所有的浏览器、手机app都使用这种协议进行通讯。目前主流的http协议为http/1.1协议,然而,http/1.x时代的某些特性会对当今的应用程序产生负面的影响,主要体现在以下几点
- 在http/1.0时代,每一次http请求都会建立一次tcp连接,建立tcp连接的消耗在高并发的场景下是不能忽略的。然而,在相同的连接四元组(发送方ip port,接收方ip port)的情况下,我们完全没有必要重新建立tcp连接。
尽管我们可以在http header中加入Keep-Alive复用同一个tcp请求,
在http/1.1时代,引入了http管道(http pipeline)概念,将浏览器的FIFO队列移动到了服务端,浏览器会将请求全部都发送给服务端,然后等着接受就ok了。这样服务器处理完第一个就处理第二个,不会有空闲等待了。但是,http/1.1不支持连接复用,这个方案有几个很难解决的问题
-
服务端收到多个管道请求后,需要按接受顺序一个一个处理,如果第一个请求特别慢,后续所有相应都会跟着阻塞。这种情况就是队首阻塞问题(head of line blocking)
-
服务端为了保证按顺序回传,需要缓存多个相应,从而占用更多服务器资源
-
如果浏览器连续发送多个请求,中间因为网络导致断开,无法得知服务器处理情况,只能进行全部重试导致服务器重复处理
- http/1.x时代的http header请求头总是重复和冗余的,协议头部使用纯文本格式,没有任何压缩,且包含很多冗余信息(例如 Cookie、UserAgent 每次都会携带)这造成了大量没有必要的网络开销,导致tcp的拥塞窗口快速填充,这回导致当在一个新的TCP连接上发出多个请求时,造成过多的延迟.
http2协议
http2协议是构建于http语义之上的,即它支持http/1.x协议的所有核心特性但是对性能上做了大量的优化。这些优化主要有:
-
二进制分帧
http2协议最大的变化在于重新定义了格式化和传输数据的方式。这是通过在高层的http-api和tcp连接之间加入二进制分桢层实现的。这样就能使原来的web应用并不用任何的改变,性能却能带来质的改变。 -
连接复用
http2支持了连接复用,即同域名下所有通信都在单个tcp连接上完成,这个连接tcp可以承载任意数量的双向数据流。可以支持任意数量的http请求。且互相之间互不影响。不用再因为队首阻塞而导致后续的请求无法处理。 -
头部压缩(header compression)
上面提到,http header请求头总是重复和冗余的,且直接以纯文本传输。随着web应用越来越复杂,请求消耗在头部的流量越多,尤其每次都要传输UserAgent、Cookie这种不变的内容。 所以,http2协议使用hpack进行了头部的压缩,以减少传输成本。 -
服务器推送(server push)
前面提到,因为http/2是连接复用的,建立的tcp连接可以承载任意数量的双向数据流,这就意味着,服务器可以支持向客户端推送消息。
http2协议的一些基础概念
- Frame(帧) 帧是http/2协议的最小通信单位。桢用于承载特性的数据类型,如http头,body等。每个帧都包含帧首部,其中会标识出当前帧存在的流。
- Message(消息) 消息指的是http/2中逻辑上的http消息,例如请求和相应等,消息由一个或多个帧组成。
- Stream(流) 流指的是存在于连接的一个虚拟通道,流可以承载双向的消息。每个流都有一个唯一的证书id
- Connection(连接) 指对应的tcp连接
二进制分帧
HTTP/1 的请求和响应报文,都是由起始行、首部和实体正文(可选)组成,各部分之间以文本换行符分隔。而 HTTP/2 将请求和响应数据分割为更小的帧,并对它们采用二进制编码。下面这幅图中的 Binary Framing 就是新增的二进制分帧层:
连接复用
http2支持在同一tcp连接上的多个双向数据流中发送消息,消息又由多个帧组成,多个帧之间可以乱序发送,可以根据帧首部的流标识重新组装。如下图所示:
有了这个连接,客户端和服务器之间的所有http请求就都可以复用这个tcp连接,因为传输的单位为更小的一级单位帧,所以在上层http的报文里,也就不存在顺序的概念了,不需要等待一个http请求发送完成后第二个请求再发送。这大大提高了性能
头部压缩
前面提到,http2使用头部压缩主要是为了降低传输的大小,随着web功能越来越复杂,在web页面中传输http头的信息甚至超过了内容本身。一般来说,http消息content可以在服务端开启gzip特性来进行gzip压缩。但是在头部这块却没有压缩的机制。所以在http2中,新增了hpack头部压缩机制。
-
首先,http的头信息主要的特点是请求头冗余,每次http请求都会发送一模一样的信息,当在一个web页面几十个http请求的今天,这产生了大量冗余的带宽传输。hpack为了解决这个问题,引入了静态字典的概念。即:在客户端和服务器之间维护一份相同的静态字典。包含常见的头部名称,以及特别常见的头部名称与值的组合。
-
针对动态频繁传输的数据,hpack也引入了动态字典,使用哈夫曼编码来动态压缩。
针对http2的hpack压缩,RFC官方还出了一个文档来专门解释
优先级机制
http2的二进制分帧和流机制提高了web应用的吞吐量,但是也带来了一些幸福的烦恼,假设一个web页面包含html、css、js以及很多其他的图片等静态资源。我们希望实现的效果是,在加载页面时,先加载html、css、js渲染出框架,然后再慢慢渲染出图片。如果在http1时代,因为请求是顺序发出的,所以不存在优先级的问题。但是在多路复用之后,如果我们给予图片的stream更多的优先级,那么浏览器将会先收到图片,而对于一个web来说,它需要优先加载html、css和js才能展示。这样也就降低了效率。针对这个问题,http2引入了优先级(priority)的概念。
每个资源都获取一个stream ID来标识连接上的资源,并且有三个参数用于定义资源优先级:
父级数据流(Parent Stream):这个数据流是一个“依赖”资源或者应该在之后被传递的数据流。有一个所有数据流共享的虚拟root stream 0。
权重(Weight):1到256之间的数字,用于标识在多个数据流共享连接时分配给此数据流的带宽量。带宽是相对于所有其他活动的数据流的权重分配的,而不是绝对值。
独占位(Exclusive bit):一个标志,表示应该在不与任何其他数据流共享带宽的情况下下载。
一般情况下,优先级都是由客户端发起的。所以对于优先级机制的利用,大多由浏览器的内核来完成,具体可以这篇文章。里面详细讲解了不同浏览器对于这个参数的不同实现。
这里对于依赖的实现
具体的协议详情
以下内容大多对于https://httpwg.org/specs/rfc7540.html的翻译和总结
http2将请求和相应数据分割为更小的帧(frame),并对每一个frame采用二进制编码,具体帧的格式为:
+-----------------------------------------------+
| Length (24) |
+---------------+---------------+---------------+
| Type (8) | Flags (8) |
+-+-------------+---------------+-------------------------------+
|R| Stream Identifier (31) |
+=+=============================================================+
| Frame Payload (0...) ...
+---------------------------------------------------------------+
-
length: length代表了一个frame的长度,是一个24bit的int。
-
type: type代表了frame的类型,用8个bit来表示,一共有以下类型
-
flag:flag是一个8bit的标志位,用于区分在不用type下的状态值
下面就针对不同的type来详细解释一下每一个帧类型的含义
FrameData 0x0
这个帧中传输的数据是承载http请求和返回的主要载体,如承载http/1.x协议中的body等数据,主要包含data和padding填充
```
+---------------+
| Pad Length? (8)|
+---------------+-----------------------------------------------+
| Data (*) ...
+---------------------------------------------------------------+
| Padding (*) ...
+---------------------------------------------------------------+
```
FrameData 有以下flag
FlagDataEndStream = 0x1 //标识data结束
FlagDataPadded = 0x8 //标识占位
FrameHeaders 0x1
这个帧用于传输http header,传输的数据序列化后封装在header block fragment在FrameHeaders帧中传输。
此外,FrameHeader帧中还有如e、stream dependency 和 weight是涉及到和包依赖和优先级的内容,和FramePriority帧一样,客户端可以告知服务器当前的流依赖于其他哪个流。该功能让客户端能建立一个优先级“树”,所有“子流”会依赖于“父流”的传输完成情况。
```
+---------------+
|Pad Length? (8)|
+-+-------------+-----------------------------------------------+
|E| Stream Dependency? (31) |
+-+-------------+-----------------------------------------------+
| Weight? (8) |
+-+-------------+-----------------------------------------------+
| Header Block Fragment (*) ...
+---------------------------------------------------------------+
| Padding (*) ...
+---------------------------------------------------------------+
```
FrameHeaders 有以下flag
FlagHeadersEndStream = 0x1 //标识这个流结束
FlagHeadersEndHeaders = 0x4 //标识这个http请求的header结束
FlagHeadersPadded = 0x8 //占位
FlagHeadersPriority = 0x20 //标识在有优先级FramePriority的情况下,是否独占。
FramePriority 0x2
FramePriority帧被用于处理流之间的优先级和依赖关系的内容,上面的FrameHeaders的一部分协议内容就是这个帧。这个只是单独抽出来了。相当于FrameHeaders帧其实是headers和priority的合并发送。后面会简单介绍,这里先贴出数据内容
```
+-+-------------------------------------------------------------+
|E| Stream Dependency (31) |
+-+-------------+-----------------------------------------------+
| Weight (8) |
+-+-------------+
```
这里主要有三个参数
- E 是否独占,如果独占,服务器将选择权重最高的一个,完成传递后,就将其从列表中弹出并重新选择。
- Stream Dependency 流的id
- Weight 流的权重,由1个字节组成,值为1~256的区间
FrameRSTStream 0x3
FrameRSTStream会立即终止一个流。发送FrameRSTStream帧一般是请求关闭一个流或者表明发生了异常的情况。
数据内容 FrameRSTStream使用一个32位的int来传输错误码
```
+---------------------------------------------------------------+
| Error Code (32) |
+---------------------------------------------------------------+
```
常见的错误码大概有一下几种
```
ErrCodeNo ErrCode = 0x0
ErrCodeProtocol ErrCode = 0x1
ErrCodeInternal ErrCode = 0x2
ErrCodeFlowControl ErrCode = 0x3
ErrCodeSettingsTimeout ErrCode = 0x4
ErrCodeStreamClosed ErrCode = 0x5
ErrCodeFrameSize ErrCode = 0x6
ErrCodeRefusedStream ErrCode = 0x7
ErrCodeCancel ErrCode = 0x8
ErrCodeCompression ErrCode = 0x9
ErrCodeConnect ErrCode = 0xa
ErrCodeEnhanceYourCalm ErrCode = 0xb
ErrCodeInadequateSecurity ErrCode = 0xc
ErrCodeHTTP11Required ErrCode = 0xd
```
FrameSettings 0x4
FrameSettings用于传输配置参数信息。它描述的是发送方的特征,相同的参数的不同的值在不同的端中可能会不同,例如,客户端可能设置了一个很高的初始滑动窗口(initial flow control window).服务器可能设置了一个较低的值
FrameSettings帧的数据格式如下,是由一个16位的标识符和32位的值
```
+-------------------------------+
| Identifier (16) |
+-------------------------------+-------------------------------+
| Value (32) |
+---------------------------------------------------------------+
```
具体的参数有如下几个:
- SETTINGS_HEADER_TABLE_SIZE 0x1
用于通知对端最大头部压缩表(header compression table)的大小,主要用于http2头部压缩使用
- SETTINGS_ENABLE_PUSH 0x2
用于设置是否开启server_push。如果设置为0,将不能发送FramePushPromise帧
- SETTINGS_MAX_CONCURRENT_STREAMS 0x3
用于设置发送方的最大并发流的数量
- SETTINGS_INITIAL_WINDOW_SIZE 0x4
用于设置滑动窗口的初始窗口大小
- SETTINGS_MAX_FRAME_SIZE 0x5
用于设置发送方能接受的最大帧大小
- SETTINGS_MAX_HEADER_LIST_SIZE 0x6
用于设置发送方能够接受的最大头列表(header list),这个值是基于未压缩的头字段的。该参数的初始值是无限的。
FrameSettings 有以下flag
FlagSettingsAck 0x1 用于返回setting ack使用
FramePushPromise 0x5
PUSH_PROMISE 帧用于在发送方打算初始化的流之前提前通知对方。
FramePing 0x6
FramePing用于检测一个连接(connection)是否还可用,是一种用于测量来自发送方的最小往返时间的机制
FramePing 有以下flag
FramePing 0x1 用于返回ping ack使用
FrameGoAway 0x7
FrameGoAway 用于启动一个连接的关闭或发出严重错误条件的信号。FrameGoAway允许一个端优雅地停止接收新的流的同时,完成目前已经建立的流。
FrameWindowUpdate 0x8
WINDOW_UPDATE 用于实现滑动窗口控制,滑动窗口可以在两个维度进行控制,一个独立的流或者一个连接。默认窗口大小为65535个字节,WINDOW_UPDATE用于传输窗口大小的增量
```
+-+-------------------------------------------------------------+
|R| Window Size Increment (31) |
+-+-------------------------------------------------------------+
```
FrameContinuation 0x9
FrameContinuation用于继续发送一个Header的header block fragment。只要在前的Frame是FrameHeader或FramePushPromise,且FrameContinuation未收到END_HEADERS这个flag,就可以发送任意个FrameContinuation帧。这个帧的发送一般是用于发送长度很大的header,如果超过了单帧能发送的最大限制,就需要拆分出来,第一帧发送FrameHeader,剩下的发送FrameContinuation。
FrameContinuation有以下flag:
FlagContinuationEndHeaders = 0x4 标识header是否结束