浅谈http2协议

浅谈http2协议

Scroll Down

浅谈http2协议

http/1.x协议有什么问题

http协议(hypertext transfer protocol)是目前运用最成功的应用层协议,几乎所有的浏览器、手机app都使用这种协议进行通讯。目前主流的http协议为http/1.1协议,然而,http/1.x时代的某些特性会对当今的应用程序产生负面的影响,主要体现在以下几点

  1. 在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)

  • 服务端为了保证按顺序回传,需要缓存多个相应,从而占用更多服务器资源

  • 如果浏览器连续发送多个请求,中间因为网络导致断开,无法得知服务器处理情况,只能进行全部重试导致服务器重复处理

  1. 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 就是新增的二进制分帧层:

imagepng

连接复用

http2支持在同一tcp连接上的多个双向数据流中发送消息,消息又由多个帧组成,多个帧之间可以乱序发送,可以根据帧首部的流标识重新组装。如下图所示:
imagepng

有了这个连接,客户端和服务器之间的所有http请求就都可以复用这个tcp连接,因为传输的单位为更小的一级单位帧,所以在上层http的报文里,也就不存在顺序的概念了,不需要等待一个http请求发送完成后第二个请求再发送。这大大提高了性能

头部压缩

前面提到,http2使用头部压缩主要是为了降低传输的大小,随着web功能越来越复杂,在web页面中传输http头的信息甚至超过了内容本身。一般来说,http消息content可以在服务端开启gzip特性来进行gzip压缩。但是在头部这块却没有压缩的机制。所以在http2中,新增了hpack头部压缩机制。

image.png

  1. 首先,http的头信息主要的特点是请求头冗余,每次http请求都会发送一模一样的信息,当在一个web页面几十个http请求的今天,这产生了大量冗余的带宽传输。hpack为了解决这个问题,引入了静态字典的概念。即:在客户端和服务器之间维护一份相同的静态字典。包含常见的头部名称,以及特别常见的头部名称与值的组合。

  2. 针对动态频繁传输的数据,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是否结束