前言
前段时间组内小伙伴遇到了一个问题:一个页面上有 10 个视频,因为浏览器对 tcp 连接数的限制,导致同时只能加载 6 个视频。考虑到http2
协议的多路复用可以解决这个问题,特地整理此篇关于http2
的内容和大家分享。
下面我们先从http1.1
说起。
对于同一个域名,浏览器最多只能同时创建 6~8 个 TCP 连接 (不同浏览器不一样)。因为一个tcp连接一次承载一个请求,也就是说一个时刻最多只能发起6~8个请求,这就是上文说到的只能同时发起 6 个视频请求的问题。为了解决这个限制,行业内惯用域名分区的方案,即将资源分散到不同域名下 (比如二级子域名),这样就可以针对不同域名创建连接并请求。但多域名随之而来的是更多的 dns 查询耗时,以及更多 tcp 连接开销。
我们都知道,http1.1
默认设置请求头部字段keep-alive
以保持 tcp 持久连接,以实现多个请求复用同一个 tcp 连接,避免重复建立连接造成的时间开销。但一个问题是这时的 tcp 连接同一时刻只能处理一个 http 请求,即请求时序为“请求1->响应1->请求2->响应2...”,如果请求1没完成,后续的请求2只能等待。
为了尽可能并行发送请求,http1.1 引入了管线技术(pipelining),优化效果对比如下图:
图片来源于网络
管线技术部分解决了请求并发的问题,仍存在队头阻塞的问题,原因如下:
GET
和HEAD
)才能管道化。因为,意外中断时候,客户端需要把未收到响应的请求重发,非幂等请求,会造成资源破坏。那http2
是如何解决这些问题的呢?
http2
通过多路复用解决了http1.1
队头阻塞和tcp连接数的问题,大家可以先通过下面这个例子(并行加载大量小图)直观感受出http2
比http1.1
快了很多。
图片来源于网络
让我们来看看http2
是如何做到的!
http2
把原来http所传输的信息划分为多个粒度更小的帧,并对其进行二进制编码,然后将其映射到属于特定流的消息。
在一个 TCP 连接上,我们可以向对方不断发送帧,每帧的 stream identifier 的标明这一帧属于哪个流,然后在对方接收时,根据 stream identifier 拼接每个流的所有帧组成一整块数据。我们可以把每个请求或者响应都当作一个流,那么多个请求变成多个流,这不同流的数据被分成多个帧,在一个连接中交错地发送给对方,这就是 http2
中的多路复用
。
图片来源于《High Performance Browser Networking》
多路复用依赖一个关键技术点,那就是二进制分帧:
二进制分帧层指示如何在客户端和服务器之间封装和传输http消息。它会将所有传输的信息分割为粒度更小的帧,首部信息则被封装到Headers帧,body则封装到Data帧里面。每个帧都以固定的9字节首部开始,里面会至少标明其所属的流。一个流则是一个请求或者响应。正是基于帧和流,且来自不同流的帧可以交错发送,才使多路复用可以实现。
图片来源于《High Performance Browser Networking》
我们前面说到了一个连接里面承载了多个流,并且不同流的帧可以交错发送,那么客户端和服务器交付不同流的帧的顺序成为了关键的性能考虑因素,因为不同资源的优先级是不一样的,为了实现这一点,引入了流优先级
。
http2
允许每个流具有流依赖关系以及相关的权重:
权重:可以为每个流分配1到256之间的整数权重 流依赖关系:每个流可以明确依赖一个流
客户端使用权重和流依赖关系的组合信息,向服务端构造和传递“优先级树”,该树表明其希望如何接收响应,即我们期望优先级越高的请求越快得到响应,服务端使用此信息确定流处理的优先级,控制cpu、内存和其他资源的分配。一旦响应数据可用,就分配带宽以确保向客户端最佳的传递高优先级响应。那么如何确认流的优先级呢?
图片来源于《High Performance Browser Networking》
通过引用另一个流的唯一标识符作为其父级来声明http2
中的流依赖性; 如果省略,则称该流依赖于“根流”。流依赖性表明,如果可能,则希望在处理它之前先为父流分配资源。例如:C依赖于D,则表明请在响应C之前先处理并响应D。
共享相同父级的流应该按其权重比例分配资源。例如对于上图流A和流B,他们都是根流,A的权重为12,B的权重为4,则A应该接收到资源的比例为12/16=3/4。B接收到资源的比例为1/4。
不过,值得注意的是,流优先级只是表达了一种传输偏好,不表示绝对的要求,因此不保证特定的处理或传输顺序。虽然看上去觉得违反直觉,毕竟设置优先级就是希望资源按照我设定的顺序返回,可是却又并不能保证绝对的顺序。但其实这是合理的行为:当高优先级的资源阻塞的时候,低优先级的资源不会被阻塞。
流优先级是由客户端设置,发给服务端的。浏览器中有一个默认的优先级。浏览器基于自身对资源重要性的判读,为不同的资源分配相应的优先级。例如,页面 <head>
中的 <script>
标签将以 High 优先级(比优先级为 Highest 的 CSS 低)在 Chrome 中加载;但是,如果该标签具有异步属性(也就是说它能以异步方式加载和运行),其优先级将更改为 Low。
http2
除了多路复用和流优先级,还引入两个也很重要的特性,即:头部压缩
和服务端推送
。
http1.1
中,只有针对body的压缩,而http头部都是直接以纯文本的形式传输的,当请求很多的时候,未经压缩的头部会造成对网络资源的浪费,头部经过压缩后,可以极大的减少体积,以下是打开淘宝首页抓包的一个结果:
可以看到经过压缩后的头部长度只要44个字节,而解压后的头部却有559个字节。
头部压缩需要在客户端和服务器之间:
简要过程:通过对先前未见过的值使用静态哈夫曼编码,并把这个头部插入动态表中。而如果是已经存在于每一侧的静态表或动态的值进行索引的替换。
图片来源于《High Performance Browser Networking》
一个典型的web程序由很多资源组成,但所有这些资源都是客户端通过检查服务端所提供的文档发现的。而服务端推送可以让服务器除了响应原始请求以外,还可以把其他资源推送到服务端,客户端不必请求每个资源,减少了浏览器接收响应并解析html的时间。推送的资源必须遵循同源策略。如下图所示:
图片来源于《High Performance Browser Networking》
内联css、javascript或其他资源,其实也相当于是将该资源推送到客户端(但客户端不能拒绝,也没有缓存),而无需等待客户端请求。但使用http2
的服务端推送,可以使得客户端缓存这些推送的资源,可以在不同的页面上重用,客户端也可以拒绝推送资源(比如,该资源已经在缓存中时)。一旦客户端收到PUSH_PROMISE帧,它就可以选择拒绝流(通过RST_STREAM帧)(如果它想要的话)(例如,资源已经在缓存中),这是对http1.1
的重要改进。相比之下,资源内联的使用,这是http1.1
的流行“优化”,相当于“强制推送”:客户端不能单独选择退出,取消它或处理内联资源。
http2
增加了多路复用、流优先级、头部压缩、服务端推送等主要内容。其中多路复用因为在很大程度上解决了浏览器 tcp 连接数限制问题而受到大家重点关注。现在绝大部分的浏览器都已经实现了对http2
的支持(见下图)。
我们使用`http2`时,应该要注意到,在`http1.1`时代的一些优化方案如合并请求、雪碧图、域名分区等可能不再那么必要。
虽然http2
解决了很多之前旧版本的问题,但是它也没有彻底解决队头阻塞问题。因为 tcp 协议的“超时重传”机制,丢失的包必须等待重新传输确认,才能传输下一个包。因此当http2
出现丢包时,会阻塞掉复用该连接的所有请求。
为此,http3
使用了基于 UDP 传输协议的 QUIC 协议,QUIC 原生实现了多路复用,其传输的单个数据流可以保证有序交付且不会影响其他的数据流,这就解决了 http2
中 tcp 重传导致的阻塞问题。
不过 http3
目前离生产使用还有很长的路要走,服务端方面,2020年6月10日,Nginx 刚宣布实现了初始版本,而 Apache httpd 暂时还没有任何支持 http3
的消息。客户端方面,Firefox 75,Chrome 83 以上版本开始支持http3
协议。
https://hpbn.co/http2/
https://docs.google.com/presentation/d/1r7QXGYOLCh4fcUq0jDdDwKJWNqWK1o4xMtYpKZCJYjM/edit?hl=zh-cn#slide=id.g518e3c87f_0_13
https://www.wolfcstech.com/2016/10/29/http2-spec/