0%

http2的变化和

1. http2介绍

HTTP/2 出来的目的是为了改善 HTTP 的性能。协议升级有一个很重要的地方,就是要兼容老版本的协议,否则新协议推广起来就相当困难,所幸 HTTP/2 做到了兼容 HTTP/1.1 。那么,HTTP/2 是怎么做的呢?

第一点,HTTP/2 没有在 URI 里引入新的协议名,仍然用「http://」表示明文协议,用「https://」表示加密协议,于是只需要浏览器和服务器在背后自动升级协议,这样可以让用户意识不到协议的升级,很好的实现了协议的平滑升级。

第二点,只在应用层做了改变,还是基于 TCP 协议传输,应用层方面为了保持功能上的兼容,HTTP/2 把 HTTP 分解成了「语义」和「语法」两个部分,「语义」层不做改动,与 HTTP/1.1 完全一致,比如请求方法、状态码、头字段等规则保留不变。

但是,HTTP/2 在「语法」层面做了很多改造,基本改变了 HTTP 报文的传输格式。

2. http2功能

2.1 Header压缩

HTTP 协议的报文是由「Header + Body」构成的,对于 Body 部分,HTTP/1.1 协议可以使用头字段 「Content-Encoding」指定 Body 的压缩方式,比如用 gzip 压缩,这样可以节约带宽,但报文中的另外一部分 Header,是没有针对它的优化手段。

HTTP/1.1 报文中 Header 部分存在的问题:

  • 含很多固定的字段,比如Cookie、User Agent、Accept 等,这些字段加起来也高达几百字节甚至上千字节,所以有必要压缩;
  • 大量的请求和响应的报文里有很多字段值都是重复的,这样会使得大量带宽被这些冗余的数据占用了,所以有必须要避免重复性;
  • 字段是 ASCII 编码的,虽然易于人类观察,但效率低,所以有必要改成二进制编码;

HTTP/2 对 Header 部分做了大改造,把以上的问题都解决了。

HTTP/2 没使用常见的 gzip 压缩方式来压缩头部,而是开发了 HPACK 算法,HPACK 算法主要包含三个组成部分:

  • 静态字典;
  • 动态字典;
  • Huffman 编码(压缩算法);

客户端和服务器两端都会建立和维护「字典」,用长度较小的索引号表示重复的字符串,再用 Huffman 编码压缩数据,可达到 50%~90% 的高压缩率。

静态表包含了 61 种高频出现在头部的字符串,不在静态表范围内的头部字符串就要自行构建动态表,它的 Index 从 62 起步,会在编码解码的时候随时更新。

比如,第一次发送时头部中的「user-agent 」字段数据有上百个字节,经过 Huffman 编码发送出去后,客户端和服务器双方都会更新自己的动态表,添加一个新的 Index 号 62。那么在下一次发送的时候,就不用重复发这个字段的数据了,只用发 1 个字节的 Index 号就好了,因为双方都可以根据自己的动态表获取到字段的数据

所以,使得动态表生效有一个前提:必须同一个连接上,重复传输完全相同的 HTTP 头部。如果消息字段在 1 个连接上只发送了 1 次,或者重复传输时,字段总是略有变化,动态表就无法被充分利用了。

理想很美好,现实很骨感。动态表越大,占用的内存也就越大,如果占用了太多内存,是会影响服务器性能的,因此 Web 服务器都会提供类似 http2_max_requests 的配置,用于限制一个连接上能够传输的请求数量,避免动态表无限增大,请求数量到达上限后,就会关闭 HTTP/2 连接来释放内存。

HTTP/2 头部的编码通过「静态表、动态表、Huffman 编码」共同完成的。

2.2 二进制传输数据

HTTP/2 厉害的地方在于将 HTTP/1 的文本格式改成二进制格式传输数据,极大提高了 HTTP 传输效率,而且二进制数据使用位运算能高效解析。

HTTP/2 则是一个彻底的二进制协议,头信息和数据体都是二进制,并且统称为”帧”(frame):头信息帧和数据帧。

你可以从下图看到,HTTP/1.1 的响应 和 HTTP/2 的区别:

1

HTTP/2 把响应报文划分成了两个帧(Frame),图中的 HEADERS(首部)和 DATA(消息负载) 是帧的类型,也就是说一条 HTTP 响应,划分成了两个帧来传输,并且采用二进制来编码。

HTTP/2 二进制帧的结构如下图:

img

帧头(Fream Header)很小,只有 9 个字节,帧开头的前 3 个字节表示帧数据(Fream Playload)的长度

帧长度后面的一个字节是表示帧的类型,HTTP/2 总共定义了 10 种类型的帧,一般分为数据帧和控制帧两类,如下表格:

img

帧类型后面的一个字节是标志位,可以保存 8 个标志位,用于携带简单的控制信息,比如:

  • END_HEADERS 表示头数据结束标志,相当于 HTTP/1 里头后的空行(“\r\n”);
  • END_STREAM 表示单方向数据发送结束,后续不会再有数据帧。
  • PRIORITY 表示流的优先级;

帧头的最后 4 个字节是流标识符(Stream ID),但最高位被保留不用,只有 31 位可以使用,因此流标识符的最大值是 2^31,大约是 21 亿,它的作用是用来标识该 Fream 属于哪个 Stream,接收方可以根据这个信息从乱序的帧里找到相同 Stream ID 的帧,从而有序组装信息。

最后面就是帧数据了,它存放的是通过 HPACK 算法压缩过的 HTTP 头部和包体。

2.3 Stream并发传输

我们都知道 HTTP/1.1 的实现是基于请求-响应模型的。同一个连接中,HTTP 完成一个事务(请求与响应),才能处理下一个事务,也就是说在发出请求等待响应的过程中,是没办法做其他事情的,如果响应迟迟不来,那么后续的请求是无法发送的,也造成了队头阻塞的问题。

而 HTTP/2 通过 Stream 这个设计,多个 Stream 复用一条 TCP 连接,达到并发的效果,解决了 HTTP/1.1 队头阻塞的问题,提高了 HTTP 传输的吞吐量。

img

可以从上图中看到:

  • 1 个 TCP 连接包含一个或者多个 Stream,Stream 是 HTTP/2 并发的关键技术;
  • Stream 里可以包含 1 个或多个 Message,Message 对应 HTTP/1 中的请求或响应,由 HTTP 头部和包体构成;
  • Message 里包含一条或者多个 Frame,Frame 是 HTTP/2 最小单位,以二进制压缩格式存放 HTTP/1 中的内容(头部和包体);

在 HTTP/2 连接上,不同 Stream 的帧是可以乱序发送的(因此可以并发不同的 Stream ),因为每个帧的头部会携带 Stream ID 信息,所以接收端可以通过 Stream ID 有序组装成 HTTP 消息,而同一 Stream 内部的帧必须是严格有序的。

img

客户端和服务器双方都可以建立 Stream, Stream ID 也是有区别的,客户端建立的 Stream 必须是奇数号,而服务器建立的 Stream 必须是偶数号。

同一个连接中的 Stream ID 是不能复用的,只能顺序递增,所以当 Stream ID 耗尽时,需要发一个控制帧 GOAWAY,用来关闭 TCP 连接。

在 Nginx 中,可以通过 http2_max_concurrent_streams 配置来设置 Stream 的上限,默认是 128 个。

HTTP/2 通过 Stream 实现的并发,比 HTTP/1.1 通过 TCP 连接实现并发要牛逼的多,因为当 HTTP/2 实现 100 个并发 Stream 时,只需要建立一次 TCP 连接,而 HTTP/1.1 需要建立 100 个 TCP 连接,每个 TCP 连接都要经过TCP 握手、慢启动以及 TLS 握手过程,这些都是很耗时的。

HTTP/2 还可以对每个 Stream 设置不同优先级,帧头中的「标志位」可以设置优先级,比如客户端访问 HTML/CSS 和图片资源时,希望服务器先传递 HTML/CSS,再传图片,那么就可以通过设置 Stream 的优先级来实现,以此提高用户体验。

2.4 服务器主动推送

比如,客户端通过 HTTP/1.1 请求从服务器那获取到了 HTML 文件,而 HTML 可能还需要依赖 CSS 来渲染页面,这时客户端还要再发起获取 CSS 文件的请求,需要两次消息往返,如下图左边部分:

img

如上图右边部分,在 HTTP/2 中,客户端在访问 HTML 时,服务器可以直接主动推送 CSS 文件,减少了消息传递的次数。

在 Nginx 中,如果你希望客户端访问 /test.html 时,服务器直接推送 /test.css,那么可以这么配置:

1
2
3
location /test.html { 
http2_push /test.css;
}

另外Server Push不同于Websocket,Server Push一般是指服务器主动向客户端推送数据,这是一种单向的主动推送,而WebSocket是双向的,这两种技术不是竞争关系。

Server Push可以用在服务器主动向客户端推送静态资源,比如浏览器请求index.html时,服务器除了返回网页内容外,还会将index.html页面里面的各种css和js一起推送到浏览器缓存起来,当浏览器分析了网页内容发现静态资源时,不需要再去服务器请求一次,它只需要从缓存里直接拿就可以了。不过现代的网站的静态资源大多都是CDN架构的,静态资源都在第三方服务器,Server Push在这方面作用并不大。

3. 长连接区别

3.1 websocket

WebSocket是全双工的,可以双向通信,主要应用在实时通信的场景中,服务器可以实时推送数据给客户端。

3.2 SSE 技术

SSE ( Server-sent Events )是 WebSocket 的一种轻量代替方案,使用 HTTP 协议。严格地说,HTTP 协议是没有办法做服务器推送的,但是当服务器向客户端声明接下来要发送流信息时,客户端就会保持连接打开,SSE 使用的就是这种原理。

SSE 是单向通道,只能服务器向客户端发送消息,如果客户端需要向服务器发送消息,则需要一个新的 HTTP 请求。 这对比 WebSocket 的双工通道来说,会有更大的开销。

3.3 http2

HTTP/2 虽然也支持 Server Push,但是服务器只能主动将资源推送到客户端缓存!那不是应用程序可以感知的,主要是让浏览器(用户代理)提前缓存静态资源。

HTTP/2不是类似于Websocket或者SSE这样的推送技术的替代品。

4. 复用 tcp 连接

4.1 http1 baseline

所谓长连接,即在 HTTP 请求建立 TCP 连接时,请求结束,TCP 连接不断开,继续保持一段时间(timeout),在这段时间内,同一客户端向服务器发送请求都会复用该 TCP 连接,并重置 timeout 时间计数器,在接下来 timeout 时间内还可以继续复用 TCP 。

timeout 时间到了之后,TCP会立即断开连接吗?

若两小时(timeout)没有收到客户的数据,服务器就发送一个探测报文段,以后则每隔 75 秒发送一次。若一连发送 10 个探测报文段后仍无客户的响应,服务器就认为客户端出了故障,接着就关闭这个连接。

img

HTTP/1.x 虽然引入了 keep-alive 长连接,但它每次请求必须等待上一次响应之后才能发起。

所以,在 HTTP/1.1 中提出了管道机制(默认不开启),下一次的请求不需要等待上一个响应来之后再发送,但这要求服务端必须按照请求发送的顺序返回响应,当顺序请求多个文件时,其中一个请求因为某种原因被阻塞时,在后面排队的所有请求也一并被阻塞,这就是 HTTP 队头阻塞 (Head-Of-Line Blocking)。

4.2 http2 multiplex

  • HTTP/1.x 是基于文本的,只能整体去传;HTTP/2 是基于二进制流的,可以分解为独立的帧,交错发送
  • HTTP/1.x keep-alive 必须按照请求发送的顺序返回响应;HTTP/2 多路复用不按序响应。
  • HTTP/1.x keep-alive 为了解决 http 队头阻塞,将同一个页面的资源分散到不同域名下,开启了多个 TCP 连接;HTTP/2 同域名下所有通信都在单个连接上完成。
  • HTTP/1.x keep-alive 单个 TCP 连接在同一时刻只能处理一个请求(两个请求的生命周期不能重叠);HTTP/2 单个 TCP 同一时刻可以发送多个请求和响应。
clipboard.png

5. 队头阻塞问题

很多人在一些资料中会看到有论点说HTTP/2解决了队头阻塞的问题。但是这句话只对了一半。只能说HTTP/2解决了HTTP的队头阻塞问题,但是并没有解决TCP队头阻塞问题!

5.1 HTTP 队头阻塞

http1.x采用长连接(Connection:keep-alive),可以在一个TCP请求上,发送多个http请求。有非管道化和管道化,两种方式。

非管道化:完全串行执行,请求->响应->请求->响应…,后一个请求必须在前一个响应之后发送。

管道化:请求可以并行发出,但是响应必须串行返回。后一个响应必须在前一个响应之后。原因是,没有序号标明顺序,只能串行接收。

无论是非管道化还是管道化,都会造成队头阻塞(请求阻塞)。

5.2 TCP 队头阻塞

TCP数据包是有序传输,中间一个数据包丢失,会等待该数据包重传,造成后面的数据包的阻塞。

5.3 总结

HTTP/1.1的管道化持久连接也是使得同一个TCP链接可以被多个HTTP使用,但是HTTP/1.1中规定一个域名可以有6个TCP连接。

HTTP/2使用一个域名单一TCP连接发送请求,请求包被二进制分帧,不同请求可以互相穿插,避免了 http 层面的请求队头阻塞。但是不能避免TCP层面的队头阻塞。

所以,在HTTP/2中,TCP队头阻塞造成的影响会更大,因为HTTP/2的多路复用技术使得多个请求其实是基于同一个TCP连接的,那如果某一个请求造成了TCP队头阻塞,那么多个请求都会受到影响。

6. http3是什么

HTTP/2 使用了多路复用,一般来说同一域名下只需要使用一个 TCP 连接。但当这个连接中出现了丢包的情况,那就会导致 HTTP/2 的表现情况反倒不如 HTTP/1 了。

因为在出现丢包的情况下,整个 TCP 都要开始等待重传,也就导致了后面的所有数据都被阻塞了。但是对于 HTTP/1.1 来说,可以开启多个 TCP 连接,出现这种情况反到只会影响其中一个连接,剩余的 TCP 连接还可以正常传输数据。

Google 就更起炉灶搞了一个基于 UDP 协议的 QUIC 协议,并且使用在了 HTTP/3 上,HTTP/3 之前名为 HTTP-over-QUIC,从这个名字中我们也可以发现,HTTP/3 最大的改造就是使用了 QUIC。

7. 参考资料

给作者打赏,可以加首页微信,咨询作者相关问题!