3.6 HTTP/2牛逼在哪?

HTTP/1.1协议的性能问题
了解HTTP1.1协议存在的问题,HTTP/2协议把这些性能问题逐个攻破。
现在的站点相比以前变化太多,比如:
- 消息的大小变大了,从几KB大小的消息,到几MB大小的消息
- 页面资源变多了,从每个页面不到10个资源到每页超100多个资源
- 内容形式变多样了,从单纯的文本内容到图片、视频、音频等内容
- 实时性要求变高了,对页面的实时性要求的应用越来越多
上述变化带来的最大性能问题就是HTTP/1.1的高延迟,延迟高必然影响的就是用户体验,主要原因如下:
- 延迟难以下降。虽然现在网络的带宽相比以前变多了,但是延迟降到一定幅度后,就很难再下降了,到达延迟的下限。
- 并发连接有限。谷歌浏览器最大并发连接数是6个,而且每一个连接都要经过TCP和TLS握手耗时,以及TCP慢启动过程给流量带来的影响。
- 对头阻塞问题。同一个连接只能在完成一个HTTP事务(请求和响应)后,才能处理下一个事务;
- HTTP头部巨大且重复。由于HTTP协议是无状态的,每一个请求都得携带HTTP头部,特别是对于携带Cookie的头部,而Cookie的大小通常很大。
- 不支持服务器推送消息。因此当客户需要获取通知时,只能通过定时器不断地拉取消息,这无疑浪费大量带宽和服务器资源。
HTTP/1.1常见优化手段:
- 将多张小图合并成一张大图供浏览器JavaScript来切割使用,这样可以将多个请求合并成一个请求,但是带来了新的问题,当某张小图片更新了,那么需要重新请求大图片,浪费了大量地网络带宽;
- 将图片的二进制数据通过Base64编码后,把编码数据嵌入到HTML或CSS文件中,以此来减少网络请求次数
- 将多个体积较小的JavaScript文件使用Webpack等工具打包成一个体积更大的JavaScript文件,以一个请求替代很多个请求,但是,当某个js文件变化了,需要重新请求同一个包里的所有js文件
- 将同一个页面的资源分散到不同域名,提升并发连接上限,因为浏览器通常对同一个域名的HTTP连接最大只能是6个。
上述手段都是对HTTP/1.1协议的外部优化,而一些关键的地方无法优化,如请求-响应模型,头部巨大且重复,并发连接耗时,服务器不能主动推送等,要改变这些必须重新设计HTTP协议,于是有了HTTP/2。
兼容HTTP/1.1
- HTTP/2没有在URL里引入新的协议名,仍然用[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%。

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

HTTP/2头部由于基于二进制编码,就不需要冒号空格和末尾的\r\n作为分隔符,于是改用表示字符串长度(Value)来分割Index和Value.
首先,从静态表中能查到server头部字段的Index为54,二进制110110,再加上固定01,头部格式的第一个字节就是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个字节,最后7位是补位的。

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

动态表编码
静态表只包含61种高频出现在头部的字符串,不在静态表范围的头部字符串就要自行构建动态表,它的Index从62起步,会在编码解码的时候随时更新。
比如,第一次发送时头部中的[User-Agent]字段数据有上百个字节,经过Huffman编码发送出去后,客户端和服务器双方都会更新自己的动态表,添加一个新的Index号62,那么在下一次发送的时候,就不用重复发这个字段的数据了,只用发1个字节的Index号就好了,因为双方都可以根据自己的动态表获取到字段的数据。
动态表生效的前提:==必须同一个连接上,重复传输完全相同的HTTP头部。==如果消息字段在1个连接只发送1次,或者重复传输时,字段总是略有变化,动态表就无法被充分利用了。
因此,随着同一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响应,划分成了两类帧来传输,并且采用二进制编码。
比如状态码200,在HTTP/1.1是用’2’,‘0’,'0’三个字符来表示(二进制:00110010 00110000 00110000),共用3个字节。
在HTTP/2对于状态码200的二进制编码是10001000,只用了1个字节就能表示,相比于HTTP/1,1节省了2个字节。

Header::status:200 OK的编码内容为:10001000的表达含义:
- 最前面的1标识该Header是静态表中已经存在的KV.
- 静态表中,status:200 OK的编码是8,即1000
HTTP/2二进制帧的结构如图:

帧头(Frame Header)很小,只有9个字节,帧开头的前3个字节表示帧数据(Frame Playload)的长度。
帧长度后面一个字节表示帧的类型,HTTP/2总共定义了10种类型的帧,一般分为数据帧和控制帧,如下表格:

帧类型后面的一个字节是标志位,可以保存8个标志位,用于携带简单的控制信息,比如:
- END_HEADERS表示头数据结束标志,相当于HTTP/1里头后的空行(\r\n)
- END_Stream表示单方向数据发送结束,后续不会再有数据帧
- PRIORITY表示流的优先级
帧头的最后4个字节是流标识符(Stream ID),但最高位被保留不用,只有31位可以使用,因此流标识符的最大值是2^31,大约21亿,它的作用是用来标识该Frame属于哪个Stream,接收方可以根据这个信息从乱序的帧里找到相同Stream ID的帧,从而有序组装消息。
最后就是帧数据,它存放的是通过HPACK算法压缩过的HTTP头部的包体。
并发传输
HTTP/1.1的实现是基于请求-响应模型的。同一个连接中,HTTP完成一个事务(请求与响应),才能处理下一个事务。在发出请求等待响应的过程中,是没有办法做其他事情的,如果响应迟迟不来,那么后续的请求是无法发送的,造成对头阻塞。
HTTP/2,通过Stream设计,多个Stream复用一条TCP连接,达到并发的效果,解决了HTTP/1.1对头阻塞的问题,提高HTTP传输的吞吐量。

HTTP/2中的Stream、Message、Frame概念:
- 1个TCP连接包含一个或者多个Stream,Stream是HTTP/2并发的关键技术
- Stream里可以包含1个或多个Message,Message对应HTTP/1中的请求或响应,由HTTP头部和包体构成。
- Message里包含一条或者多个Frame,Frame是HTTP/2最小单位,以二进制压缩格式存放HTTP/1中的内容(头部和包体)
结论:多个Stream跑在一条TCP连接,同一个HTTP请求与请求是跑在同一个Stream中,HTTP消息可以由多个Frame构成,一个Frame可以由多个TCP报文构成。
在HTTP/2连接上,不同Stream的帧是可以乱序发送的(因此可以并发不同的Stream),因为每个帧的头部会携带Stram ID信息,所以接收端通过Stream ID有序组装成HTTP消息,而同一Stream内部的帧必须是严格有序的。

上图中,服务器并行交错地发送了两个响应:Stream1和Stream 3,这两个Stream都是跑在一个TCP连接上,客户端收到后,会根据相同的Stream ID有序组装成HTTP信息。
客户端和服务端双方都可以建立Stream,因为服务器可以主动推送资源给客户端,客户端建立的Stream必须是奇数号,而服务器建立的Stream必须是偶数号。
上图中,Stream1是客户端向服务端请求的资源,属于客户端建立的Stream,所以该Stream的ID是奇数(1);Stream2和4都是服务端主动向客户端推送的资源,属于服务端建立的Stream,所以这两个Stream的ID是偶数(2和4).

同一个连接中的Stream ID是不能复用的,只能顺序递增,所以当Stram 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文件,减少了消息传递的次数。
在Ngnix中,如果你希望客户端访问/test.html时,服务器直接推送/test.css,那么可以配置:
location /test.html{
http2_push/test.css;
}
HTTP/2推送的实现:客户端发起的请求,必须使用奇数号Stream,服务器主动的推送,使用偶数号Stream。服务器在推送资源时,会通过PUSH_PROMISE帧传输HTTP头部,并通过帧中的Promised Stream ID字段告知客户端,接下来会在哪个偶数号Stram中发送包体。

上图中,在Stream1中通知客户端CSS资源即将到来,然后在Stream 2中发送CSS资源,两个可并发。
总结
- 对于常见的HTTP头部通过静态表和Huffman编码方式,将体积压缩近一半,而且针对后续的请求头部,还可以建立动态表,将体积压缩近90%,大大提高编码效率,同时节约带宽资源。但动态表并非可以无限增大,因为动态表是会占用内存的,动态表越大,内存也越大,容易影响服务器总体的并发能力,因此服务器需要限制HTTP/2连接时长或者请求次数。
- HTTP/2实现了Stream并发,多个Stream只需复用1个TCP连接,节约了TCP和TLS握手时间,以及减少TCP慢启动阶段对流量的影响。不同的Stram ID可以并发,即使乱序发送帧也没有问题,比如A发送请求帧1->B请求帧1->A请求帧2->B请求帧2,但是同一个Stream里的帧必须严格有序。另外可以根据资源的渲染顺序来设置Stream的优先级,从而提高用户体验。
- 服务器支持主动推送资源,大大提升消息的传输性能,服务器推送资源时,会先发送PUSH_PROMISE帧,告诉客户端接下来在哪个Stream发送资源,然后用偶数号Stream发送资源。
HTTP/2通过Stream的并发能力,解决HTTP/1.1对头阻塞的问题,但仍存在“队头阻塞”问题,只不过不在HTTP层面,而是在TCP这一层。
HTTP/2是基于TCP协议来传输数据的,TCP是字节流协议,TCP层必须保证收到的字节是完整且连续的,这样内核才会将缓冲区里的数据返回给HTTP应用,那么当前一个字节数据没有到达时,后收到的字节只能存放在内核缓冲区中,只有等到这1个字节数据到达时,HTTP/2应用层才能从内核中拿到数据,这就是HTTP/2对头阻塞问题。
HTTP/3干脆放弃TCP协议,转而使用UDP协议作为传输层协议,解决HTTP/2的对头阻塞问题。
HTTP/2通过头部压缩、二进制帧、并发传输和服务器推送等特性解决了HTTP/1.1的性能问题,提高了网络传输效率和用户体验。头部压缩使用HPACK算法大幅减少头部大小,二进制帧实现更高效的传输,并发传输利用Stream实现多任务并行,服务器推送则减少了请求往返次数。
2103

被折叠的 条评论
为什么被折叠?



