xxxxHub 都用上了 HTTP/2 ,它牛逼在哪?


现在很多站点都已经弃掉 HTTP/1.1,转而使用 HTTP/2 协议了,比如某Hub、B站、爱奇艺、腾讯视频、淘宝等等。

那 HTTP/2 牛逼在哪?

不多 BB 了,直接发车!


HTTP/1.1 协议的性能问题

我们得先要了解下 HTTP/1.1 协议存在的性能问题,因为 HTTP/2 协议就是把这些性能问题逐个攻破了。

现在的站点相比以前变化太多了,比如:

  • 消息的大小变大了,从几 KB 大小的消息,到几 MB 大小的消息;

  • 页面资源变多了,从每个页面不到 10 个的资源,到每页超 100 多个资源;

  • 内容形式变多样了,从单纯到文本内容,到图片、视频、音频等内容;

  • 实时性要求变高了,对页面的实时性要求的应用越来越多;

这些变化带来的最大性能问题就是 HTTP/1.1  的高延迟,延迟高必然影响的就是用户体验。主要原因如下几个:

  • 延迟难以下降,虽然现在网络的「带宽」相比以前变多了,但是延迟降到一定幅度后,就很难再下降了,说白了就是到达了延迟的下限;

  • 并发连接有限,谷歌浏览器最大并发连接数是 6 个,而且每一个连接都要经过 TCP 和 TLS 握手耗时,以及 TCP 慢启动过程给流量带来的影响;

  • 队头阻塞问题,同一连接只能在完成一个 HTTP 事务(请求和响应)后,才能处理下一个事务;

  • HTTP 头部巨大且重复,由于 HTTP 协议是无状态的,每一个请求都得携带 HTTP 头部,特别是对于有携带 cookie 的头部,而 cookie 的大小通常很大;

  • 不支持服务器推送消息,因此当客户端需要获取通知时,只能通过定时器不断地拉取消息,这无疑浪费大量了带宽和服务器资源。

为了解决 HTTP/1.1 性能问题,具体的优化手段你可以看这篇文章「我的 HTTP/1.1 好慢啊!」,这里我举例几个常见的优化手段:

  • 将多张小图合并成一张大图供浏览器 JavaScript 来切割使用,这样可以将多个请求合并成一个请求,但是带来了新的问题,当某张小图片更新了,那么需要重新请求大图片,浪费了大量的网络带宽;

  • 将图片的二进制数据通过 base64 编码后,把编码数据嵌入到 HTML 或  CSS 文件中,以此来减少网络请求次数;

  • 将多个体积较小的 JavaScript 文件使用 webpack 等工具打包成一个体积更大的 JavaScript 文件,以一个请求替代了很多个请求,但是带来的问题,当某个 js 文件变化了,需要重新请求同一个包里的所有 js 文件;

  • 将同一个页面的资源分散到不同域名,提升并发连接上限,因为浏览器通常对同一域名的 HTTP 连接最大只能是 6 个;

尽管对 HTTP/1.1 协议的优化手段如此之多,但是效果还是不尽人意,因为这些手段都是对 HTTP/1.1  协议的“外部”做优化,而一些关键的地方是没办法优化的,比如请求-响应模型、头部巨大且重复、并发连接耗时、服务器不能主动推送等,要改变这些必须重新设计 HTTP 协议,于是 HTTP/2 就出来了!


兼容 HTTP/1.1

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 报文的传输格式。


头部压缩

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% 的高压缩率

静态表编码

HTTP/2 为高频出现在头部的字符串和字段建立了一张静态表,它是写入到 HTTP/2 客户端与服务器的代码中的,不会变化的,静态表里共有 61 组,如下图:

表中的 Index 表示索引(Key),Header Value 表示索引对应的  Value,Header Name 表示字段的名字,比如 Index 为 2 代表 GET,Index 为 8 代表状态码 200。

你可能注意到,表中有的 Index 没有对应的 Header Value,这是因为这些 Value 并不是固定的而是变化的,这些 Value 都会经过 Huffman 编码后,才会发送出去。

这么说有点抽象,我们来看个具体的例子,下面这个 server 头部字段,在 HTTP/1.1 的形式如下:

server: nghttpx\r\n

算上冒号空格和末尾的\r\n,共占用了 17 字节,而使用了静态表和 Huffman 编码,可以将它压缩成 8 字节,压缩率大概 47 %

我抓了个 HTTP/2 协议的网络包,你可以从下图看到,高亮部分就是 server 头部字段,只用了 8 个字节来表示 server 头部数据。

根据 RFC7541 规范,如果头部字段属于静态表范围,并且 Value 是变化,那么它的 HTTP/2 头部前 2 位固定为 01,所以整个头部格式如下图:

HTTP/2 头部由于基于二进制编码,就不需要冒号空格和末尾的\r\n作为分隔符,于是改用表示字符串长度(Value Length)来分割 Index 和 Value。

接下来,根据这个头部格式来分析上面抓包的 server 头部的二进制数据。

首先,从静态表中能查到 server 头部字段的 Index 为 54,二进制为 110110,再加上固定 01,头部格式第 1 个字节就是 01110110,这正是上面抓包标注的红色部分的二进制数据。

然后,第二个字节的首个比特位表示 Value 是否经过 Huffman 编码,剩余的 7 位表示 Value 的长度,比如这次例子的第二个字节为 10000110,首位比特位为 1 就代表 Value 字符串是经过 Huffman 编码的,经过 Huffman 编码的 Value 长度为 6。

最后,字符串 nghttpx 经过 Huffman 编码后压缩成了 6 个字节,Huffman 编码的原理是将高频出现的信息用「较短」的编码表示,从而缩减字符串长度。

于是,在统计大量的 HTTP 头部后,HTTP/2 根据出现频率将 ASCII 码编码为了 Huffman 编码表,可以在 RFC7541 文档找到这张静态 Huffman 表,我就不把表的全部内容列出来了,我只列出字符串 nghttpx 中每个字符对应的 Huffman 编码,如下图:

通过查表后,字符串 nghttpx 的 Huffman 编码在下图看到,共 6 个字节,每一个字符的 Huffman 编码,我用相同的颜色将他们对应起来了,最后的 7 位是补位的。

最终,server 头部的二进制数据对应的静态头部格式如下:

动态表编码

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

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

而且,随着在同一 HTTP/2 连接上发送的报文越来越多,客户端和服务器双方的「字典」积累的越来越多,理论上最终每个头部字段都会变成 1 个字节的 Index,这样便避免了大量的冗余数据的传输,大大节约了带宽。

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

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


二进制帧

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

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

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

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

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

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

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

  • END_HEADERS 表示头数据结束标志,相当于 HTTP/1 里头后的空行(“\r\n”);

  • END_STREAM 表示单方向数据发送结束,后续不会再有数据帧。

  • PRIORITY 表示流的优先级;

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

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


并发传输

知道了 HTTP/2 的帧结构后,我们再来看看它是如何实现并发传输的。

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

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

为了理解 HTTP/2 的并发是怎样实现的,我们先来理解 HTTP/2 中的 Stream、Message、Frame 这 3 个概念。

你可以从上图中看到:

  • 1 个 TCP 连接包含一个或者多个 Stream,Stream 是 HTTP/2 并发的关键技术;

  • Stream 里可以包含 1 个或多个 Message,Message 对应 HTTP/1 中的请求或响应,由 HTTP 头部和包体构成;

  • Message 里包含一条或者多个 Frame,Frame 是 HTTP/2 最小单位,以二进制压缩格式存放 HTTP/1 中的内容(头部和包体);

因此,我们可以得出 2 个结论:HTTP 消息可以由多个 Frame 构成,以及 1 个 Frame 可以由多个 TCP 报文构成。

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

客户端和服务器双方都可以建立 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 的优先级来实现,以此提高用户体验。


服务器主动推送资源

HTTP/1.1 不支持服务器主动推送资源给客户端,都是由客户端向服务器发起请求后,才能获取到服务器响应的资源。

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

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

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

location /test.html {   http2_push /test.css; }

那 HTTP/2 的推送是怎么实现的?

客户端发起的请求,必须使用的是奇数号 Stream,服务器主动的推送,使用的是偶数号 Stream。服务器在推送资源时,会通过 PUSH_PROMISE 帧传输 HTTP 头部,并通过帧中的 Promised Stream ID 字段告知客户端,接下来会在哪个偶数号 Stream 中发送包体。

如上图,在 Stream 1 中通知客户端 CSS 资源即将到来,然后在 Stream 2 中发送 CSS 资源,注意 Stream 1 和 2 是可以并发的。


总结

HTTP/2 协议其实还有很多内容,比如流控制、流状态、依赖关系等等。

这次主要介绍了关于 HTTP/2 是如何提示性能的几个方向,它相比 HTTP/1 大大提高了传输效率、吞吐能力。

第一点,对于常见的 HTTP 头部通过静态表和 Huffman 编码的方式,将体积压缩了近一半,而且针对后续的请求头部,还可以建立动态表,将体积压缩近 90%,大大提高了编码效率,同时节约了带宽资源。

不过,动态表并非可以无限增大, 因为动态表是会占用内存的,动态表越大,内存也越大,容易影响服务器总体的并发能力,因此服务器需要限制 HTTP/2 连接时长或者请求次数。

第二点,HTTP/2 实现了 Stream 并发,多个 Stream 只需复用 1 个 TCP 连接,节约了 TCP 和 TLS 握手时间,以及减少了 TCP 慢启动阶段对流量的影响。不同的 Stream ID 才可以并发,即时乱序发送帧也没问题,但是同一个 Stream 里的帧必须严格有序。

另外,可以根据资源的渲染顺序来设置 Stream 的优先级,从而提高用户体验。

第三点,服务器支持主动推送资源,大大提升了消息的传输性能,服务器推送资源时,会先发送 PUSH_PROMISE 帧,告诉客户端接下来在哪个 Stream 发送资源,然后用偶数号 Stream 发送资源给客户端。

HTTP/2 通过 Stream 的并发能力,解决了 HTTP/1 队头阻塞的问题,看似很完美了,但是 HTTP/2 还是存在“队头阻塞”的问题,只不过问题不是在 HTTP 这一层面,而是在 TCP 这一层。

HTTP/2 是基于 TCP 协议来传输数据的,TCP 是字节流协议,TCP 层必须保证收到的字节数据是完整且连续的,这样内核才会将缓冲区里的数据返回给 HTTP 应用,那么当「前 1 个字节数据」没有到达时,后收到的字节数据只能存放在内核缓冲区里,只有等到这 1 个字节数据到达时,HTTP/2 应用层才能从内核中拿到数据,这就是 HTTP/2 队头阻塞问题。

有没有什么解决方案呢?既然是 TCP 协议自身的问题,那干脆放弃 TCP 协议,转而使用 UDP 协议作为传输层协议,这个大胆的决定, HTTP/3 协议做了!


巨人的肩膀
  1. https://developers.google.com/web/fundamentals/performance/http2

  2. https://http2.akamai.com/demo

  3. https://tools.ietf.org/html/rfc7541

(0)

相关推荐

  • HTTP不懂怎么办?

    HTTP 基本概念 1. 什么是 HTTP?HTTP 的作用是什么? HTTP 全称:HyperText Transfer Protocol ,超文本传输协议. HTTP 从客户端到服务器端等一系列运 ...

  • 解读HTTP/2与HTTP/3 的新特性

    前端技术优选 今天 以下文章来源于前端工匠 ,作者浪里行舟君 前言 HTTP/2 相比于 HTTP/1.1,可以说是大幅度提高了网页的性能,只需要升级到该协议就可以减少很多之前需要做的性能优化工作,当 ...

  • 消息推送标准协议:MQTT

    随着物联网(Internet of Things,IoT)的兴起,机器之间(Machine-to-Machine,M2M)的大规模信息沟通成为重要的课堂,之前HTTP的请求/回答(Request/Re ...

  • 技术揭秘 | 港交所OCG高性能交易协议赏析

    交易所服务于交易业务的协议与服务于行情业务的协议有着诸多不同,开发者如果未曾有过具体的对接经验,可能会难以直接欣赏 OCG(Orion Central Gateway,港交所交易服务网关的名称)协议的 ...

  • 种葡萄1年要修剪多少次?你都跟上了吗?(葡萄牛人总结)

    葡萄为多年蔓生植物,在生长季节,若梢枝过于稠密,不仅影响营养供给.通风透光条件,还易遭受病虫侵害:落叶后,放任不管,往往大小年问题严重,病虫害亦会多发.所以,想要种好葡萄,修剪整形是必备的技能. 01 ...

  • 蚂上创业营:很多牛逼的企业,都是在环境不好的时候做起来的

    "不定性,是一个企业家永远要面对的东西.企业家最宝贵的能力,是拥抱和驾驭不定性,是寻找不定性中的确定性,并转化为机遇.企业家要思考的是,我要做什么事情,要设计什么样的能力,才能活得更久?活得 ...

  • 为什么CTO、技术总监、架构师都不写代码,还这么牛逼?

    文|技术领导力 常常会被问到这样的问题:CTO.技术总监.架构师很少写具体代码,为什么还很牛逼的样子,拿这么高工资? 其实,这个问题本身就错了.就好比问:导演.制片人为什么不懂演戏,还能指导演员,好像 ...

  • 南北朝民歌赏析(四):北朝的女人都能上前线打仗

    接上篇. 北朝民歌存世仅六十几首,相对于近三百年的北朝历史,这个数量确实有点少.少是少了点,但是也有精品. <木兰诗>是北朝民歌中的巅峰之作,与<孔雀东南飞>并称"乐 ...

  • LED大灯 带字母的车标都安排上了!大众新车更香了

    今时今日,新车越做越大,越做越便宜,曾经火热的合资小型车市场如今只剩寥寥几款经典车型还在坚守阵地,当中不乏还在沿用老一代设计与配置的产品. 合资品牌只要随便做一款实惠的小车就能大卖的日子早就过去了,在 ...

  • 为什么欧洲能发明出香水?不但不洗澡,就连裙子都是上厕所用的

    提到欧洲,在很多人眼中这里应该是东方人非常向往浪漫的地方,这里不仅有各式各样的欧洲建筑城堡,还有很多非常考究的贵族主义,是求婚最佳首选之地.现代化欧洲景色独特迷人,城市和自然风光并存,然而就是一个这样 ...

  • 这个需求一对多查找和Power Query都用上了

    经常遇到类似于竖向转横向,或者说横向展开的问题,这里干脆写一篇,详细说一下! 网友的源需求: 问题在年份数值这列没有填充,所以感觉很难,假设我们先填充上,那么会变得轻松而简单! 第一步:先把坑填上 本 ...

  • 14597名律师都装上了这款App

    "真没想到,我们不用去浙江,在家动动手指就能在线申请阅卷,下载代理案件的电子卷宗,一次都不用跑!"日前,北京律师何安妮成功申请在线阅卷后,不禁为浙江检察App这枚浙江数字检察的&q ...

  • 每代人都要上保健品的当,这届年轻人格外夸张

    去年爆火的塔罗算命有降温趋势,占卜师索要无度更引起官媒点名.不过,对于最时髦的那群年轻人而言,塔罗算命本就玩腻,"科学迷信"才是新宠. 毕竟受过义务教育,这届年轻人在寻求" ...