HTTP/2 特性概览


楔子

下面我们来介绍一下 HTTP2 协议,不过在介绍之前我们需要先了解为什么要有 HTTP2 协议。现在的互联网基本上都被 HTTP/1.1 统治了,但 HTTP/1.1 有两个主要的缺点:安全不足、性能不高。虽然通过引入 SSL/TLS 在安全上达到了极致(HTTPS),但在性能的提升方面确实乏善可陈。只优化了握手加密的环节,对于整体的数据传输没有提出更好的改进方案,还只能依赖于长连接这种落后的技术。所以,在 HTTPS 逐渐成熟之后,就向着性能方面开始发力,走出了另一条进化的道路。

了解一下 HTTP 的发展历史,我们知道是 Google 率先发明了 SPDY 协议,并应用于自家的浏览器 Chrome,打响了 HTTP 性能优化的第一枪。随后互联网标准化组织(IETF) 以 SPDY 协议为基础,综合其他多方的意见,终于推出了 HTTP/1 的继任者,也就是今天的主角 HTTP/2,在性能方面有了一个大的飞跃。

你一定很想知道,为什么 HTTP/2 不像之前的 1.0、1.1 那样叫 2.0 呢?这个也是很多初次接触 HTTP/2 的人问的最多的一个问题,对此 HTTP/2 工作组特别给出了解释。

他们认为以前的 1.0、1.1 造成了很多的混乱和误解,让人在实际的使用中难以区分差异,所以就决定 HTTP 协议不再使用小版本号(minor version),只使用大版本号(major version),从今往后 HTTP 协议不会出现 HTTP/2.0、2.1,只会有 HTTP/2、HTTP/3、……。这样就可以明确无误地辨别出协议版本的跃进程度,让协议在一段较长的时期内保持稳定,每当发布新版本的 HTTP 协议都会有本质的不同,绝不会有零敲碎打的小改良。

因为 HTTPS 已经在安全方面已经做的非常好了,所以 HTTP/2 的唯一目标就是改进性能。但它不仅背负着众多的期待,同时还背负着 HTTP/1 庞大的历史包袱,所以协议的修改必须小心谨慎,兼容性是首要考虑的目标,否则就会破坏互联网上无数现有的资产,这方面 TLS 已经有了先例(为了兼容 TLS1.2 不得不进行伪装)。

所以接下来我们就看看 HTTP/2 在性能提升方面究竟是怎么做的?为什么 HTTP/1.1 的性能不行,HTTP/2 对 HTTP/1.1 做了哪些方面的修改?

HTTP/1.1 发展中遇到的问题

首先我们看看 HTTP/1.1 带来了哪些进步:

  • 从几 KB 大小的消息到几 MB 大小的消息
  • 每个页面小于 10 个资源到每个页面 100 多个资源
  • 从文本为主的内容到富媒体(如图片、视频、音频)为主的内容,HTTP 被称为超文本传输协议,这个超文本不仅仅可以传输文本
  • 对页面内容实时性高要求的内容越来越多

我们以打开 B 站为例:

我们看到总共发送了 113 个请求,传输了 6.7MB 的资源,有文本、有图片、有视频,传输内容的大小、种类丰富多样。并且对实时性要求高的应用也越来越多,比如视频直播等等。

以上是 HTTP/1.1 所带来的进步,但它还是有缺点的,就是性能不够好,下面举例说明。

队头阻塞

早期 HTTP/1.0 性能上的一个很大的问题,那就是每发起一个请求,都要新建一次 TCP 连接(三次握手),而且是串行请求,做了无畏的 TCP 连接建立和断开,增加了通信开销。为了解决上述 TCP 连接问题,HTTP/1.1 提出了长连接的通信方式,也叫持久连接,这种方式的好处在于减少了 TCP 连接的重复建立和断开所造成的额外开销,减轻了服务器端的负载。持久连接的特点是,只要任意一端没有明确提出断开连接,则保持 TCP 连接状态。

由于 HTTP/1.1 采用了长连接的方式,这使得管道(pipeline)网络传输成为了可能。在同一个 TCP 连接里面,客户端可以发起多个请求,只要第一个请求发出去了,不必等其回来,就可以发第二个请求出去,可以减少整体的响应时间。举例来说,客户端需要请求两个资源,以前的做法是,在同一个 TCP 连接里面,先发送 A 请求,然后等待服务器做出回应,收到后再发出 B 请求。这显然比较不够智能,而管道机制则是允许浏览器同时发出 A 请求和 B 请求。有人觉得这不是好事吗?没错,只不过还是不够完美,因为响应的处理顺序还是和请求的顺序保持一致的,先处理 A 请求的响应,然后再处理 B 请求的响应。要是 A 请求的响应特别慢,那么即使 B 请求的响应回来了,客户端也无法处理。

所以「请求 - 响应」的模式加剧了 HTTP 的性能问题,因为当顺序发送的请求序列中的一个请求因为某种原因被阻塞时,在后面排队的所有请求也一同被阻塞了,会导致客户端一直请求不到数据,这也就是「队头阻塞」,好比上班的路上塞车。

因此 HTTP/1.1 的性能虽然不算差,但还不能完全适应现在的互联网,仍有很大的提升空间,后续的 HTTP/2 和 HTTP/3 都是在优化 HTTP/1.1 的性能。

高延迟问题

高延迟带来页面加载速度的降低,并且随着带宽增加,延迟并没有显著下降。

现在家庭的网络带宽越来越大,但是延迟并没有显著的降低,从左边的柱状图可以看出。原因就是 HTTP/1.1 处理响应的顺序和请求顺序是一致的,只有第一个响应处理完毕之后才能处理第二个响应。就类似于打卡,第一个人因为某些原因怎么也打不上卡,但他如果打不上,后面的人也没法打。总的来说,对于 HTTP/1.1 而言,没有轻重缓急的优先级,只有先后入队的顺序。而右边的柱状图则反应了,延迟越低,页面加载用的耗时越少。

无状态特性带来的巨大 HTTP 头部

我们知道无论是请求报文还是响应报文,都是由 Header + Body 组成,因为 HTTP/1.1 是无状态的,所以就要求 Header 携带很多的头字段,有时多达几百字节甚至上千字节,但 Body 却经常只有几十字节、甚至几字节(比如 GET 请求、204/301/304 响应),等于说变成了一个不折不扣的"大头儿子"。更要命的是,成千上万的请求响应报文里有很多字段值都是重复的,非常浪费,长尾效应导致大量带宽消耗在了这些冗余度极高的数据上。

以上就是 HTTP/1.1 面临的一些问题, 尽管也通过一些额外的手段来曲线救国,但仍然不能很好的解决问题。所以业界就开始「改革」了,发明了 HTTP/2 协议,这个协议很好地解决了 HTTP/1.1 所面临的问题。

HTTP/2 特性概述

首先 HTTP/2 它是安全的,和 HTTPS 一样,也是基于 SSL/TLS 之上,我们来看一张图。

图中的 HTTP/3 我们之后再说,我们看到 HTTP/2 在结构上和 HTTPS 是类似的,因为要保持向上兼容。而 HTTP/2 的做法是将 HTTP 分解成了「语义」和「语法」两个部分,语义层不做改动,与 HTTP/1 完全一致(即 RFC7231)。比如请求方法、URI、状态码、头字段等概念都保留不变,这样就消除了再学习的成本,基于 HTTP 的上层应用也不需要做任何修改,可以无缝转换到 HTTP/2,如果代理服务器不支持 HTTP/2,那么会自动降级到 HTTP/1.1(HTTPS)。特别要说的是,与 HTTPS 不同,HTTP/2 没有在 URI 里引入新的协议名,仍然用 http 表示明文协议,用 https 表示加密协议。这是一个非常了不起的决定,可以让浏览器或者服务器去自动升级或降级协议,免去了选择的麻烦,让用户在上网的时候都意识不到协议的切换,实现平滑过渡。

在语义保持稳定之后,HTTP/2 在语法层做了天翻地覆的改造,完全变更了 HTTP 报文的传输格式。

头部压缩

首先,HTTP/2 对报文的头部做了一个大手术,我们知道 HTTP/1 里可以用头字段 Content-Encoding 指定 Body 的编码方式,比如用 gzip 压缩来节约带宽,但报文的另一个组成部分「Header」却被无视了,没有针对它的优化手段。

所以,HTTP/2 把头部压缩作为性能改进的一个重点,优化的方式你也肯定能想到,还是压缩。不过 HTTP/2 并没有使用传统的压缩算法,而是开发了专门的 HPACK 算法,在客户端和服务器两端建立字典,用索引号表示重复的字符串,还釆用哈夫曼编码来压缩整数和字符串,可以达到 50%~90% 的高压缩率。

二进制格式

你可能已经很习惯于 HTTP/1 里纯文本形式的报文了,它的优点是一目了然,用最简单的工具就可以开发调试,非常方便。但 HTTP/2 在这方面没有妥协,决定改变延续了十多年的现状,不再使用肉眼可见的 ASCII 码,而是向下层的 TCP/IP 协议靠拢,全面采用二进制格式。这样虽然对人不友好,但却大大方便了计算机的解析。原来使用纯文本的时候容易出现多义性,比如大小写、空白字符、回车换行、多字少字等等,程序在处理时必须用复杂的状态机,效率低,还麻烦。

而二进制里只有 0 和 1,可以严格规定字段大小、顺序、标志位等格式,对就是对,错就是错,解析起来没有歧义,实现简单,而且体积小、速度快,做到内部提效。因此以二进制格式为基础,HTTP/2 就开始了大刀阔斧的改革。它把 TCP 协议的部分特性挪到了应用层,把原来的 Header+Body 的消息打散为数个小片的二进制帧(Frame),其中 HEADERS 帧存放头数据、DATA 帧存放实体数据。

这种做法有点像是 Chunked 分块编码的方式,也是化整为零的思路,但 HTTP/2 数据分帧后 Header+Body 的报文结构就完全消失了,协议看到的只是一个个的碎片。

虚拟的流

在连接的层面上看,多个消息就是一堆乱序收发的帧,比如可以先接收消息 1 的帧、再接收消息 2 的帧,然后再接收消息 1 的帧,不要求顺序(比如先将消息 1 的帧全部接收完毕之后才能接收消息 2 的帧),否则和 HTTP/1.1 就没有区别了。

那么问题来了,这些消息碎片(二进制帧)到达目的地后应该怎么组装起来呢?HTTP/2 为此定义了一个流(Stream)的概念,它是虚拟的,可以想象成二进制帧的双向传输序列。而隶属同一个消息的所有帧都有一个相同的流 ID,不同消息的流 ID 则不同。后续在对帧进行组装的时候,根据这个 ID 来将属于同一个消息的帧组装在一起,得到类似 HTTP/1.1 中的报文,也就是传输时无序,接收时组装。所以,在 HTTP/2 中的多个请求 / 响应之间没有了顺序关系,不需要排队等待,也就不会再出现队头阻塞问题,降低了延迟,大幅度提高了连接的利用率。

说白了在 HTTP/2 中就是将二进制的报文数据切分成多个帧进行传输,而不同消息的帧可以混在一起发送(传输时无序),而在接收时再根据流 ID 将属于同一个消息的帧组装在一起(接收时组装)。

因为流是虚拟的,实际上并不存在,所以 HTTP/2 就可以在一个 TCP 连接上同时发送多个碎片化的消息,这就是常说的多路复用( Multiplexing),多个往返通信都复用一个连接来处理。

但需要注意的是,我们说传输时无序指的是多个消息之间的帧可以无序,但同一个消息的帧则必须是有序的,比如每条消息必须先传输其 HEADER 帧、再传输 DATA 帧,否则消息不就乱掉了吗?然后组装的时候,直接按照顺序进行组装即可。

另外为了更好地利用连接,加大吞吐量,HTTP/2 还添加了一些控制帧来管理虚拟的流,实现了优先级和流量控制,这些特性也和 TCP 协议非常相似。HTTP/2 还在一定程度上改变了传统的 请求 - 响应 工作模式,服务器不再是完全被动地响应请求,也可以新建流主动向客户端发送消息。比如,在浏览器刚请求 HTML 的时候就提前把可能会用到的 JS、CSS 文件发给客户端,减少等待的延迟,这被称为服务器推送(Server Push,也叫 Cache Push)。

强化安全

出于兼容的考虑,HTTP/2 延续了 HTTP/1 的明文特点,可以像以前一样使用明文传输数据,不强制使用加密通信,只不过传输内容变成了二进制(不需要解密)。并且为了区分加密和明文这两个不同的版本,HTTP/2 协议定义了两个字符串标识符:h2 表示加密的 HTTP/2,也就是在 TCP 三次握手之后,还要进行 TLS 握手,保证后续传输的数据是加密的;h2c 表示明文的 HTTP/2,在 TCP 三次握手之后直接就进行数据传输了,虽然传输内容是二进制,但没有加密的过程,而 h2c 中多出的那个字母 c 的意思就是 clear text。

但由于 HTTPS 已经是大势所趋,而且主流的浏览器 Chrome、Firefox 等都公开宣布只支持加密的 HTTP/2,所以事实上的 HTTP/2 是加密的。也就是说,互联网上通常所能见到的 HTTP/2 都是使用 https 协议名,跑在 TLS 上面。

另外,由于在 HTTP/2 标准制定的时候(2015 年)已经发现了很多 SSL/TLS 的弱点,而新的 TLS1.3 还未发布,所以加密版本的 HTTP/2 在安全方面做了强化,要求下层的通信协议必须是 TLS1.2 以上,还要支持前向安全和 SNI,并且把几百个弱密码套件列入了黑名单,比如 DES、RC4、CBC、SHA-1 都不能在 HTTP/2 里使用,相当于底层用的是 TLS1.25。

所以 HTTP/2 是建立在 HPack、Stream、TLS1.2 基础之上的,比 HTTP/1、HTTPS 复杂了一些。但它的语义还是简单的 HTTP/1.1,所以之前学习的知识不会过时,仍然可以排上用场。

补充

HTTP/2 的前身 SPDY 在压缩头部时使用了 gzip,但发现会受到 CRIME 攻击,所以开发了专用的压缩算法 HPACK。

HTTP/2 的流可以实现 HTTP/1.1 里的管道功能,而且综合性能更好,所以管道在 HTTP/2 里就被废弃了。

如果你写过 Linux 程序,用过 epoll,那么应该知道 epoll 也是一种多路复用,只不过它是 I/O Multiplexing。


免责声明!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系本站邮箱yoyou2525@163.com删除。



 
粤ICP备18138465号  © 2018-2025 CODEPRJ.COM