HTTP2

概述

参考:

HTTP 2.0 在性能上实现了很大的飞跃,更难得的是它在改进的同时保持了语义的不变,与 HTTP 1.1 的语义完全相同!比如请求方法、URI、状态码、头字段等概念都保留不变,这样就消除了再学习的成本,在我们的日常软件升级中,向下兼容非常重要,也是促进产品大规模使用的一个前提,不然你一升级,各种接口之类的全换了,谁还敢升。 HTTP 2.0 只在语法上做了重要改进,完全变更了 HTTP 报文的传输格式

在语法上主要实现了以下改造

1、头部压缩

HTTP 1.1 考虑了 body 的压缩,但没有考虑 header 的压缩, 经常出现传了头部上百,上千字节,但 Body 却只有几十字节的情况,浪费了带宽,而且我们知道从 1.1 开始默认是长连接,几百上千个请求都用的这个连接,而请求的头部很多都是重复的,造成了带宽的极大浪费!想象一下面的这个请求,为了传输区区 「name=michale 」这几个字节,却要传输如此巨量的头部,浪费的带宽确实惊人。

那么 HTTP 2.0 是如何解决的呢?它开发了专门的 「HPACK」算法,在客户端和服务器两端建立字典,用索引号表示重复的字符串,还采用哈夫曼编码来压缩数字和整数,可以达到最高达 90% 的压缩率

这里简单解释下,头部压缩需要在支持 HTTP 2.0 的客户端和服务器之间:

  1. 维护一份静态的字典(Static table),包含常见的头部名称,以及特别常见的头部名称与值的组合。这样的话如果请求响应命中了静态字典,直接发索引号即可
  2. 维护一份相同的动态字典(Dynamic table),可以动态地添加字典,这样的话如果客户端首次请求由于「User-Agent: xxx」,「host:xxx」,「Cookie」这些的动态键值对没有命中静态字典,还是会传给服务器,但服务器收到后会基于传过来的键值对创建动态字典条目,如上图的「User-Agent: xxx」对应数字 62,「host:xxx」对应数字 63,这样双方都创建动态条目后,之后就可以用只传 62,63 这样的索引号来通信了!显而易见,传输数据急遽降低,极大地提升了传输效率!需要注意的是动态字典是每个连接自己维护的,也就是对于每个连接而言,首次都必须发送动态键值对
  3. 支持基于静态哈夫曼码表的哈夫曼编码(Huffman Coding):对于静态、动态字典中不存在的内容,可以使用哈夫曼编码来减小体积。HTTP/2 使用了一份静态哈夫曼码表(详见),也需要内置在客户端和服务端之中。

2、二进制格式

HTTP 1.1 是纯文本形式,而 2.0 是完全的二进制形式,它把 TCP 协议的部分特性挪到了应用层,把原来的 Header+Body 消息打散为了数个小版的二进制"帧"(Frame),“HEADERS”帧存放头数据、“DATA”帧存放实体数据

这些二进制帧只认 0,1,基于这种考虑 http 2.0 的协议解析决定采用二进制格式,使用二进制的形式虽然对人不友好,但大大方便了计算机的解析,原来使用纯文本容易出现多义性,如大小写,空白字符等,程序在处理时必须用复杂的状态机,效率低,还麻烦。而使用二进制的话可以严格规定字段大小、顺序、标志位等格式,“对就是对,错就是错”,解析起来没有歧义,实现简单,而且体积小、速度快。

3. 流

HTTP 2 定义了「流」(stream)的的概念,它是二进制帧的双向传输序列,同一个消息往返的数据帧 (header 帧和 data 帧)会分配一个唯一的流 ID,这样我们就能区分每一个请求。在这个虚拟的流里,数据帧按先后次序传输,到达应答方后,将这些数据帧按它们的先后顺序组装起来,最后解析 HTTP 1.1 的请求头和实体。

同一时间,请求方可以在流里发请求,应答方也可以也流里发响应,对比 HTTP 1.1 一个连接一次只能处理一次请求-应答,吞吐量大大提升

如图示,一个连接里多个流可以同时收发请求-应答数据帧,每个流中数据包按序传输组装

所有的流都是在同一个连接中流动的,这也是 HTTP 2.0 经典的多路复用( Multiplexing),另外由于每个流都是独立的,所以谁先处理好请求,谁就可以先将响应通过连接发送给对方,也就解决了队头阻塞的问题。

如图示,在 HTTP 2 中,两个请求同时发送,可以同时接收,而在 HTTP 1.1 中必须等上一个请求响应后才能处理下一个请求

HTTP 2 的队头阻塞

HTTP 2 引入的流,帧等语法层面的改造确实让其传输效率有了质的飞跃,但是它依然存在着队头阻塞,这是咋回事?

其实主要是因为 HTTP 2 的分帧主要是在应用层处理的,而分帧最终还是要传给下层的 TCP 层经由它封装后再进行传输,每个连接最终还是顺序传输这些包, 如图示:流只是我们虚拟出来的概念,最终在连接层面还是顺序传的

TCP 是可靠连接,为了保证这些包能顺序传给对方,会进行丢包重传机制,如果传了三个包,后两个包传成功,但第一个包传失败了,TCP 协议栈会把已收到的包暂存到缓存区中,停下等待第一个包的重传成功,这样的话在网络不佳的情况下只要一个包阻塞了,由于重传机制,后面的包就被阻塞了,上层应用由于拿不到包也只能干瞪眼了。

由于这是 TCP 协议层面的机制,无法改造,所以 HTTP 2 的队头阻塞是不可避免的。HTTP 3 对此进行了改进,将 TCP 换成了 UDP 来进行传输,由于 UDP 是无序的,不需要断建连,包之间没有依赖关系,所以从根本上解决了“队头阻塞”, 当然由于 UDP 本身的这些特性不足以支撑可靠的通信,所以 Google 在 UDP 的基础上也加了 TCP 的连接管理,拥塞窗口,流量控制等机制,这套协议我们称之为 QUIC 协议。 可以看到不管是 HTTP 2 还是 3 它们底层都支持用 TLS,保留了 HTTPS 安全的特性,这也可以理解,在互联网发展如此迅猛的今天,各大企业也越来越重视通信安全。