HTTP发展历史

在总结http2之前先来回顾下http的发展历史。以下三张图片来自Jerry Qu

HTTP/0.9 (1991)

http-0.9-connection-demo

HTTP/1.0 (1996)

http-1.0-connection-demo

HTTP/1.1 (1999)

http-1.1-connection-demo

HTTP通信过程

众所周知,http是基于tcp之上的应用层协议,即在tcp连接建立之后,在tcp的链路上传送数据。

http-data-transport-sequence-chart

  1. 首先进行TCP连接,三次握手,C --(SYN{k})--> SS --(ACK{k+1}&SYN{j})--> CC --ACK{j+1}--> S
  2. 客户端发送ACK后,就会发送一个HTTP请求
  3. 服务端接受到ACK,确认TCP连接建立,再接着收到HTTP请求,进行解析并将结果返回客户端。
  4. 客户端收到HTTP请求结果。

HTTP/0.9HTTP/1.0中,第3步之后,服务端就会关闭连接,也就是TCP的四次挥手,但是在HTTP/1.1后,客户端在发送HTTP请求时头部可以带上Connection:Keep-Alive,就是告诉服务器保持连接,不要关闭TCP。当Connection:Close时,服务器会关闭连接。

HTTP2的通信过程无外乎这是这个流程,但是通过TCP传输的数据会有不同,客户端和服务器的行为也有了新的规则。引入了Connection、Stream、Message、Frame这四个概念,从下图大概可以看出他们之间的关系。

http2-connection-stream-message-frame

  • Connection: 其实就是一个TCP连接
  • Stream:已建立的连接上的双向字节流
  • Message:请求或者响应,由一个或多个帧组合而成
  • Frame: Message中的二进制帧,HTTP/2通信的最小单位,后面会详细解释

HTTP/2 新特性

  • 二进制分帧(Binary framing layer)
  • 多路复用 (Multiplexing)
  • 单一连接(One connection per origin)
  • 数据流优先级(Stream prioritization)
  • 首部压缩(Header Compression)
  • 流控 (Flow control)
  • 服务端推送(Server Push)

这些新特性的产生,主要是为了解决之前的问题,我们来对比下之前的HTTP/1.1,看看解决了哪些问题

二进制分帧(Binary framing layer)

二进制分帧就是把http的数据按照规定的格式进行封装,类似IP和TCP的数据包, 简单画了个承载HTTP2数据的以太帧结构,方便理解。 the-ethernet-frame-of-overing-http2

通过wireshark抓包可以看到http2的结构

wireshark-http2-frame

  • Length: 无符号的自然数,24个比特表示,仅表示帧负载所占用字节数,不包括帧头所占用的9个字节。默认大小区间为为0~16,384(2^14),一旦超过默认最大值2^14(16384),发送方将不再允许发送,除非接收到接收方定义的SETTINGS_MAX_FRAME_SIZE(一般此值区间为2^14 ~ 2^24)值的通知。
  • Type: 8个比特表示,定义了帧负载的具体格式和帧的语义,HTTP/2规范定义了10个帧类型,这里不包括实验类型帧和扩展类型帧
  • Flags: 8个比特表示,服务于具体帧类型,默认值为0x0。有一个小技巧需要注意,一般来讲,8个比特可以容纳8个不同的标志,比如,PADDED值为0x8,二进制表示为00001000;END_HEADERS值为0x4,二进制表示为00000100;END_STREAM值为0X1,二进制为00000001。可以同时在一个字节中传达三种标志位,二进制表示为00001101,即0x13。因此,后面的帧结构中,标志位一般会使用8个比特表示,若某位不确定,使用问号?替代,表示此处可能会被设置标志位
  • R: 在HTTP/2语境下为保留的比特位,固定值为0X0
  • Stream Identifier: 无符号的31比特表示无符号自然数。0x0值表示为帧仅作用于连接,不隶属于单独的流。

HTTP2帧中的类型如下:参考链接

Code Frame Type Comment
0x00 DATA 数据帧,主要用来传递消息体
0x01 HEADERS 头部帧,主要用于传递消息头
0x02 PRIORITY 优先级帧,用于设置流的优先级
0x03 RST_STREAM 流结束帧,用于终止异常流
0x04 SETTINGS 连接配置参数帧,用于设置参数
0x05 PUSH_PROMISE 推送承诺帧,Server推送之前告知Client端
0x06 PING 发送端测量最小的RTT时间,检测连接是否可用
0x07 GOAWAY 超时帧,通知对端不要在连接上建新流
0x08 WINDOW_UPDATE 实现流量控制
0x09 CONTINUATION 延续帧,延续一个报头区块
0x0a ALTSVC 通知客户端可用的替代服务
0xb-0xef Unassigned 未分配
0xf0-0xff Reserved for Experimental Use 为实验性用途保留

想了解每一个类型的详细数据结构可以参考我的另一篇文章 http2帧类型详解

通过Google Developers中的一个图,我们可以更好的理解,HTTP2的分帧在网络数据中所处的位置,以及和HTTP/1.1的不同之处。

binary_framing_layer

HTTP/1.1中的头部变成HEADERS类型的帧,请求体/回应体变成DATA类型的帧,通过二进制分帧,将传输的数据使用二进制方式,对比文本方式减少数据量;通过不同类型的帧实现流控、服务器推送等功能。

多路复用 (Multiplexing) & 单一连接(One connection per origin)

我们知道在HTTP2之前,我们如果想加快网页资源的加载速度,会采用同时建立多条连接的方式,但是这样每次建立TCP连接效率比较低,并且浏览器往往会限制最大连接数(例如chrome的最大连接数为6)。另外在HTTP/1.1中引入了Pipeline,可以在一个TCP连接中连续发送多个请求,不用关心前面的响应是否到达,但是服务器必须要按照收到请求的顺序来进行响应,这样一旦前面的请求阻塞,后来的请求也将不能及时回应。

HTTP2中,因为新的二进制帧的使用,使得可以轻松复用单个TCP连接。客户端和服务器可以将 HTTP 消息分解为互不依赖的帧,然后交错发送,最后再在另一端把它们重新组装起来。

还是 Google Developers的图: multiplexing 可以看到我们可以并行交错的发送多个响应和请求,并且使用同一个TCP连接,没有先后顺序,每个帧中携带有如何组装的信息,客户端会等某项工作所需要的所有的资源都就绪之后再执行。

数据流优先级(Stream prioritization)

由于可以进行单连接复用,服务器和客户端的帧都是交错发送,对于发送给服务器的帧,为了解决哪些该先处理,哪些该后处理,因此引入了数据流的优先级,服务器根据优先级来分配资源。例如优先级高的获得更多的CPU和带宽资源。那么优先级是如何标示的呢?还记得前面的帧类型中有一个Type为PRIORITY,这种类型的帧就是为了告诉服务器这个stream的优先级,此外HEADERS帧中也包含了优先级信息。

HTTP/2通过父依赖和权重来标示优先级,每一个stream会标示一个父stream id,没有标示的默认为虚拟的root stream,这样按照这种依赖关系构建一个依赖树,树上层的stream权重较高,同一层的stream会有一个weight来区分资源分配比。。

stream_prioritization

上图是依赖树的一些示例,从左到右,共四棵树。

  • 第一个两个stream A 和 B,没有标明父stream,默认依赖虚拟的root节点,A、B处于同一层,优先级相同,根据权重分配资源,A分到12/(12+4)=3/4资源,B分到1/4资源。
  • 第二个D和C有层级结构,C的父级是D,那么服务器拿完整资源优先处理D,然后再处理C。
  • 第三个,服务器先处理D,再处理C,然后处理A和B,A分到3/4资源,B分到1/4资源。
  • 第四个,先处理D,再讲资源对半分处理E和C,之后再按照权重处理A和B

需要注意的一点是,流优先级并不是强制约束,当优先级高的流阻塞时,并不能不让服务器处理优先级低的流

首部压缩 (Header Compression)

由于当前网站内容越来越复杂,单个页面的请求数基本都是几十个甚至上百,每个请求都要带上客户端或者用户的标识,例如:UA,cookie等头部数据,请求数量多了以后,传输http头部消耗的流量也非常可观,并且头部数据中大部分都是相同的,这就是赤裸裸的浪费呀。于是产生了头部压缩技术来节省流量。 hpack-header-compression-google-io

  • 维护一份相同的静态字典(Static Table),包含常见的头部名称,以及特别常见的头部名称与值的组合
  • 维护一份相同的动态字典(Dynamic Table),可以动态地添加内容
  • 支持基于静态哈夫曼码表的哈夫曼编码(Huffman Coding)

静态字典

静态字典就是把常用的头部映射为字节较短的索引序号,如下图所示,截取了前面几个映射,全部定义可以看Static Table Definition part-of-http2-hpack-static-table 例如当头部有个字段是:method: GET,那么查表可知,可以用序号2标识,于是这个字段的数据就是0000010(2的二进制表示)

动态字典

静态字典能表示的头部数据毕竟有限,压缩率也不会高。但是对于一个站点来讲,和某个用户交互时会发生非常多的请求,但是每次请求头部差别不大,会有很多重复数据,因为用户和浏览器的标识是不变的。那么我们可以针对一次HTTP2的连接生成一个可添加映射的动态字典,这样再后面的连接中就可以使用动态字典中的序号。动态字典的生成过程其实就是通知对方添加映射,客户端可以通知服务端添加,反之亦可。

具体的通知方式就是按照协议规定的格式传输数据,具体请看文末参考文章。

Huffman Coding

哈弗曼编码的特性是出现频率越高,编码长度越短。HTTP2协议中根据大量的请求头部数据样本生成了一种canonical Huffman code,具体在Huffman Code列出。

流控 (Flow control)

HTTP/2 流量控制的目标,在流量窗口初始值的约束下,给予接收端以全权,控制当下想要接受的流量大小。

算法:

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

服务端推送 (Server Push)

流程:

  1. 客户端在交换 SETTINGS 帧时,设置字段SETTINGS_ENABLE_PUSH(0x2)为1显式允许服务器推送
  2. 服务器在接受到请求时,分析出要推送的资源,先发个 PUSH_PROMISE 帧给浏览器
  3. 然后再发送各个response header和response body
  4. 浏览器收到 PUSH_PROMISE 帧时,根据header block fragment字段里的url,可以知道当前有没有缓存,从而判断是否要接收。如果不要,浏览器就要发送个 RST_STREAM 来终止服务器推送

问题: 流量浪费。若浏览器有缓存,不要这个推送,就会出现浪费流量的现象,因为整个过程都是异步的,在服务器接收到RST_STREAM时,响应很有可能部份发出或者全部发出了。

HTTP/2简单实践

Okhttp是一个java生态中有名的的http client,由于其简单易用,性能较好,支持http2。下面用这个工具来实践下,因为本人博客已经在nginx上配置了http2,就拿本博客来实验下。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
public class Http2Example {
    final static OkHttpClient client = new OkHttpClient.Builder().build();
    public static void main(String[] args) {
        Request request = new Request.Builder()
                .url("https://blog.fliaping.com")
                .build();
        try {
            Response response = client.newCall(request).execute();
            System.out.println(JSON.toJSONString(response.protocol()));
            System.out.println(response.headers().toString());
            System.out.println(response.body().string());
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

用过Okhttp的同学就会发现,这跟平时用的方法一样啊,没有任何区别,是的没错,就是没有任何区别。别的不多说,执行下看看,不幸的是你会发现protocol还是http1.1,并不是h2,这是怎么回事?这是因为HTTP2新加入了ALPN(Application Layer Protocol Negotiation),从字面意思理解就是应用层协议协商,即双方商量下用哪个协议。不幸的是jdk8是在2014年发布的,当时HTTP2协议还没出生,幸运的是通过第三方jar包就可以支持ALPN。另外jdk9已经支持了HTTP2,虽然还没正式发布,但是我们可以试用下JDK 9 Early-Access Builds。

jdk7和jdk8通过添加jvm参数加入第三方alpn支持包,注意版本不能搞错,jdk7使用alpn-boot-7.*.jar ,jdk8使用alpn-boot-8.*.jar,这里有版本对应关系 alpn-versions

1
2
3
4
5
6
7
8
# jdk8
-Xbootclasspath/p:/home/payne/Downloads/alpn-boot-8.1.11.v20170118.jar
# jdk7
-Xbootclasspath/p:/home/payne/Downloads/alpn-boot-7.1.3.v20150130.jar
# jdk9
# 使用jdk9平台时,注意okhttp版本大于3.3.0

# https://mvnrepository.com/artifact/org.mortbay.jetty.alpn/alpn-boot

参考内容

原文链接