HTTP 2.0 为什么这么设计

HTTP 1.0 是 1996 年发布的,奠定了 web 的基础。时隔三年,1999 年又发布了 HTTP 1.1,对功能上做了扩充。之后又时隔十六年,2015 年发布了 HTTP 2.0。

同学们肯定会觉得,隔了这么长时间,而且还从版本号还从 1 到了 2,那肯定有很多的新功能。其实不是的,HTTP 2.0 没有没有功能上的新增,只是优化了性能

为什么要这么大的版本升级来优化性能,HTTP 1.1 的性能很差么?

那我们就来看下 HTTP 1.1 有什么问题:

HTTP 1.1 的问题

我们知道,HTTP 的下层协议是 TCP,需要经历三次握手才能建立连接。而 HTTP 1.0 的时候一次请求和响应结束就会断开链接,这样下次请求又要重新三次握手来建立连接。

图片

为了减少这种建立 TCP 链接的消耗,HTTP 1.1 支持了 keep-alive,只要请求或响应头带上 Connection: keep-alive,就可以告诉对方先不要断开链接,我之后还要用这个链接发消息。当需要断开的时候,再指定 Connection: close 的 header。

这样就可以用同一个 TCP 链接进行多次 HTTP 请求响应了:

图片

但这样虽然减少了链接的建立,在性能上却有问题,下次请求得等上一个请求返回响应才能发出。

这个问题有个名字,叫做队头阻塞,很容易理解,因为多个请求要排队嘛,队前面的卡住了,那后面的也就执行不了了。

怎么解决这个问题呢?

HTTP 1.1 提出了管道的概念,就是多个请求可以并行发送,返回响应后再依次处理。

也就是这样:

图片

其实这样能部分解决问题,但是返回的响应依然要依次处理,解决不了队头阻塞的问题。

所以说管道化是比较鸡肋的一个功能,现在绝大多数浏览器都默认关闭了,甚至都不支持。

那还能怎么解决这个队头阻塞的问题呢?

开多个队不就行了。

浏览器一般会同一个域名建立 6-8 个 TCP 链接,也就是 6-8 个队,如果一个队发生队头阻塞了,那就放到其他的队里。

这样就缓解了队头阻塞问题。

我们写的网页想尽快的打开就要利用这一点,比如把静态资源部署在不同的域名下。这样每个域名都能并发 6-8 个下载请求,网页打开的速度自然就会快很多。

这种优化手段叫做“域名分片”,CDN 一般都支持这个。

除了队头阻塞的问题,HTTP 1.1 还有没有别的问题?

有,比如 header 部分太大了。

不知道大家有没有感觉,就算你内容只传输几个字符,也得带上一大堆 header:

图片

而且这些 header 还都是文本的,这样占据的空间就格外的大。

比如,如果是二进制,表示 true 和 false 直接 1 位就行了,而文本的那就得经过编码,“true” 就占了 4 个字节,也就是 32 位。那就是 32 倍的差距呀!

所以呢,HTTP 1.1 的时候,我们就要尽量避免一些小请求,因为就算请求的内容很少,也会带上一大段 header。特别是有 cookie 的情况,问题格外明显。

因此,我们的网页就要做打包,也就是需要打包工具把模块合并成多个 chunk 来加载。需要把小图片合并成大图片,通过调整 background:position 来使用。需要把一些 css、图片等内联。而且静态资源的域名也要禁止携带 cookie。

这些都是为了减少请求次数来达到提高加载性能的目的。

而且 HTTP 的底层是 TCP,其实是可以双向传输数据的,现在却只能通过请求—响应这种一问一答的方式,并没有充分利用起 TCP 的能力。

聊了这么多,不知道大家是否有优化它的冲动了。

也就是因为这些问题,HTTP 2.0 出现了,做了很多性能优化,基本解决了上面那些问题。

那 HTTP2 都做了哪些优化呢?

HTTP 2.0 的优化

先不着急看 HTTP 2.0 是怎么优化的,就上面那些问题来说,如果让我们解决,我们会怎么解决?

比如队头阻塞的问题,也就是第二个响应要等第一个响应处理完之后才能处理。怎么解决?

这个很容易解决呀,每个请求、响应都加上一个 ID,然后每个响应和通过 ID 来找到它对应的请求。各回各家,自然就不用阻塞的等待了。

再比如说 header 过大这个问题,怎么解决?

文本传输太占空间,换成二进制的是不是会好很多。

还有,每次传输都有很多相同的 header,能不能建立一张表,传的时候只传输下标就行了。

还有,body 可以压缩,那 header 是不是可以压缩。

这样处理之后,应该会好很多。

那没有充分利用 TCP 的能力,只支持请求–响应的方式呢?

那就支持服务端主动推送呀,但是客户端可以选择接收或者不接收。

上面是我们对这些问题的解决方案的思考,我们再来看看 HTTP2 是怎么解决这些问题的:

HTTP2 确实是通过 ID 把请求和响应关联起来了,它把这个概念叫做流 stream。

而且我们之前说了 header 需要单独的优化嘛,所以把 header 和 body 部分分开来传送,叫做不同的帧 frame。

每个帧都是这样的格式:

图片

payload 部分是传输的内容这没啥可说的。

header 部分最开始是长度,然后是这个帧的类型,有这样几种类型:

SETTINGS 帧:配置信息,比如最大帧 size,是否支持 server push 等。

HEADERS 帧:请求或响应的 header

DATA 帧:请求或响应的 body

CONTINUATION 帧:一个帧不够装的时候,可以分帧,用这个可以引用上一个帧。

PUSH_PROMISE 帧:服务端推送数据的帧

END_STREAM 帧:表示流传输结束

RST_STREAM 帧,用来终止当前流

这几种帧里面 HEADERS 和 DATA 帧没啥可说的。

SETTING 帧是配置信息,先告诉对方我这里支持什么,帧大小设置为多大等。

帧大小是有个上限的,如果帧太大了,可以分成多个,这时候帧类型就是 CONTINUATION(继续)。也很容易理解。

HTTP2 确实是支持服务端推送的,这时候帧类型也是单独的,叫做 PUSH_PROMISE。

流是用来传输请求响应或者服务端推送的,那传输完毕的时候就可以发送 END_STREAM 帧来表示传输完了,然后再传输 RST_STREAM 来结束当前流。

帧的类型讲完了,我们继续往后看,后面还有个 flags 标志位,这个在不同的帧类型里会放不同的内容:

图片

比如 header 帧会在 flags 中设置优先级,这样高优先级的流就可以更早的被处理。

HTTP 1.1 的时候都是排队处理的,没什么优先级可言,而 HTTP 2.0 通过流的方式实现了请求的并发,那自然就可以控制优先级了。

后面还有个 R,这个现在还没啥用,是一个保留的位。

再后面的流标识符就是 stream id 了,关联同一个流的多个帧用的。

帧的格式讲完了,大家是不是有点晕晕的。确实,帧还是有很多种的。这些帧之间发送顺序也不同,不同的帧会在不同状态下发送,也会改变流的状态。

我们来看下流的状态机,也就是流收到什么帧会进入什么状态,并且在什么状态下会发送什么帧:

(看不明白可以先往后看)图片

刚开始,流是 idle 状态,也就是空闲。

收到或发送 HEADERS 帧以后会进入 open 状态。

oepn 状态下可以发送或接收多次 DATA 帧。

之后发送或接收 END_STREAM 帧进入 half_closed 状态。

half_closed 状态下收到或者发送 RST_STREAM 帧就关闭流。

这个流程很容易理解,就是先发送 HEADER,再发送 DATA,之后告诉对方结束,也就是 END_STREAM,然后关闭 RST_STREAM。

图片

但是 HTTP2 还可以服务端推送呀,所以还有另一条状态转换流程。

流刚开始是 idle 状态。

接收到 PUSH_PROMISE 帧,也就是服务端推送过来的数据,变为 reserved 状态。

reserved 状态可以再发送或接收 header,之后进入 half_closed 状态。

后面的流程是一样的,也是 END_STREAM 和 RST_STREAM。

这个流程是 HTTP2 特有的,也就是先推送数据,再发送 headers,然后结束流。

图片

这就是 http2 发送一次请求、响应,或者一次服务端推送的流程,都是封装在一个个流里面的。

流和流之间可以并发,还可以设置优先级,这样自然就没有了队头阻塞的问题,这个特性叫做多路复用。也就是复用同一个链接,建立起多条通路(流)的意思。

而且传输的 header 帧也是经过处理的,就像我们前面说的,会用二进制的方式表示,用做压缩,而且压缩算法是专门设计的,叫做 HPACK:

两端会维护一个索引表,通过下标来标识 header,这样传输量就少了不少:

图片

首先,header 里其实不止有 header,还有一行 GET xxx/xxx 的请求行,和 200 xxx 的响应行,为了统一处理,就换成了 :host :path 等 header 来表示。

这样发送的时候只需要发送下标就行:

图片

比如 :method: get 就只需要发送个 2: get。

这个编码也是根据频率高低来设置的,频率高的用小编码,这种方式叫做哈夫曼编码。

这样就实现了 header 的压缩。

至此, HTTP2.0 的主要特性就讲完了,也就是多路复用,服务端推送,头部压缩,二进制传输。

最主要的特性是多路复用,也就是流和帧,流在什么状态下发送什么帧。其他的特性是围绕这个来设计的。

回过头来看一下 HTTP1.1 的问题是否都得到了解决:

队头阻塞:通过流的来标识请求、响应,同一个流的分为多个帧来传输,多个流之间可以并发,不会相互阻塞。

header 太大:通过二进制的形式,加上 HPACK的压缩算法,使得 header 减小了很多。

没有充分利用 TCP 的特性:支持了服务端推送。

这样看来,HTTP2.0 确实解决了 HTTP 1.1 的问题。

看起来,HTTP 2.0 已经很完美了?

其实不是的,虽然 HTTP 层面没有了队头阻塞问题,多个请求响应可以并行处理。但是同一个流的多个帧还是有队头阻塞问题,以为你 TCP 层面会保证顺序处理,丢失了会重传,这就导致了上一个帧没收到的话,下一个帧是处理不了的。

这个问题是 TCP 的可靠传输的特性带来的,所以想彻底解决队头阻塞问题,只能把 HTTP 的底层传输协议换掉了。

这就是 HTTP3 做的事情了,它的传输层协议换成了 UDP。当然,现在 HTTP3 还不是很成熟,我们先重点关注 HTTP2 即可。

总结

1996 年发布 HTTP 1.0,1999 年 HTTP 1.1,2015 年 HTTP 2.0。

1.1 和 2 之间间隔了 16 年,确实改变了很多,但只是性能方面的。

1.1 的问题是第二个请求要等第一个响应之后才能发出,就算用了管道化,多个响应之间依然也会阻塞,这就是“队头阻塞”问题。

而且 header 部分太大了,还是纯文本的,可能比 body 部分传的都多。

针对 1.1 的队头阻塞问题,我们会做域名分片,针对 header 过大的问题,我们会减少请求次数,也就是打包分 chunk、资源内联、雪碧图、静态资源请求禁止 cookie 等优化策略。

HTTP 2.0 解决了 1.1 的这些问题,通过多路复用,也就是请求和响应在一个流里,通过同一个流 id 来关联多个帧的方式来传输数据。多个流可以并发。

我们看了帧的格式,有长度、类型、stream id、falgs 还有 payload 等部分。

帧的类型还是挺多的,有 HEADRS、DATA、SETTINGS、PUSH_PROMISE、END_STREAM、EST_STREAM、等。

这些帧类型之间也不是毫无关联的,流在不同的状态下会发送、接收不同的帧,而且发送、接收不同的帧也会进入不同的状态。

理解 HTTP2.0 的 stream 就要理解这样的一个状态流转流程。

此外,HTTP 2.0 通过单独设计的 HPACK 算法对 header 做了压缩,也支持服务端推送。而且内容是通过二进制传输的,解决了 HTTP 1.1 的问题。

但是 HTTP 2.0 的底层是 TCP,它的可靠传输的特性使得同一个流内的多个帧依然是顺序传输的,依然有队头阻塞问题。也是因为 HTP 3把底层协议换成 UDP。

虽然还是有一些问题,但 HTTP 2.0 已经基本上把 HTTP 1.1 的各方面性能不好的点都优化到了极致,是很有意义的一次版本升级。

郑重声明:本文内容及图片均整理自互联网,不代表本站立场,版权归原作者所有,如有侵权请联系管理员(admin#wlmqw.com)删除。
(0)
用户投稿
上一篇 2022年6月29日
下一篇 2022年6月29日

相关推荐

联系我们

联系邮箱:admin#wlmqw.com
工作时间:周一至周五,10:30-18:30,节假日休息