【传智播客郑州校区】HTTP/2技术整理
1. HTTP协议发展 1.1. HTTP的历史
HTTP于1989年正式发布,也就是HTTP/1啦,在经历10年后于1999年更新出了HTTP/1.1,也是我们现在普遍使用的版本。
在2015年初HTTP/2标准正式发表,取代HTTP1.1成为HTTP的实现标准。也就是说,到现在HTTP/2才出现不到3年。
(具体的发展可参考维基百科:
1.2. 了解HTTP
1.2.1. 讨论环境:
相对于我们后台开发来说,对前端这块的概念相对比较薄弱,比如我们开发了一个BOS物流项目,已经放在tomcat上面,现在我们可以通过http://localhost:8080/bos/index.html来进行访问。那么客户端浏览器是怎么拿到首页的资源的?浏览器和服务器直接究竟是如何通信的呢?
1.2.2. HTTP通信过程
在这里主要有3个过程:
- 建立TCP连接:也就是浏览器与服务器的3次握手.
- 客户端请求: 建立TCP连接后,客户端就会向服务器发送一个HTTP请求信息(比如请求HTML资源,我们暂且就把这个称为“HTML请求”)
- 服务器响应: 服务器接收到请求后进行处理将HTML响应回去。
当然,接下来还有浏览器解析渲染的过程~等等~我们才能最终看到页面~~
接下来我们着重看下HTTP/1.0和HTTP/1.1在这三个过程中的不同之处:
1.3. HTTP/1.0的通信
在HTTP/1.0下,每完成一次请求和响应,TCP连接就会断开。但我们知道,客户端发送一个请求只能请求一个资源,而我们的首页index.html不可能只有单单一个HTML文件吧?至少还要有CSS吧?还要有图片吧?于是又要一次TCP连接,然后请求和响应。
下图展示了HTTP/1.0请求一个HTML和一个CSS需要经历的两次TCP连接:
要知道,TCP连接有RTT(RoundTripTime,即往返时延)的,每请求一个资源就要有一次RTT,用户可是等不得这种慢节奏的响应的。于是到了HTTP/1.1,TCP可以持久连接了,也就是说,一次TCP连接要等到同域名下的所有资源请求/响应完毕了连接才会断开。恩!听起来情况好像好了很多,请求同域名下的n个资源,可以节约(n-1)*RTT的时间。
下图展示了HTTP/1.1时请求一个HTML和一个CSS只需要经历一次TCP连接:
但前面提到了,客户端发送一个请求只能请求一个资源,那么我们会产生如下疑问:
1.5.1. 为什么不一次发送多个请求?
事实上,HTTP/1.x多次请求必须严格满足先进先出(FIFO)的队列顺序:发送请求,等待响应完成,再发送客户端队伍中的下一个请求。也就是说,每个TCP连接上只能同时有一个请求/响应。这样一来,服务器在完成请求开始回传到收到下一个请求之间的时间段处于空闲状态。
1.5.2. 有什么办法去改变吗?
“HTTP管道”技术实现了客户端向服务器并行发送多个请求。而服务器也是可以并行处理多个请求的。这么一来,不就可以多路复用了吗?但是,HTTP/1.x有严格的串行返回响应机制,通过TCP连接返回响应时,就是必须一对一,前一个响应没有完成,下一个响应就不能返回。所以使用“HTTP管道”技术时,万一第一个响应时间很长,那么后面的响应处理完了也无法发送,只能被缓存起来,占用服务器内存,这就是传说中的“队首阻塞”。
1.5.3. 既然一个TCP连接解决不了问题,那么可以开多个吗?
既然一条通道(TCP连接)通信效率低,那么就开多条通道呗!的确,HTTP/1.1下,浏览器是支持同时打开多个TCP会话的(一般为6个)。一个TCP只能响应一个请求,那么六个TCP岂不就能达到六倍速?想想还有点儿小激动!但事情往往不是这么简单。开启多个TCP会话,无疑会给客户端和服务器都带来负担,比如缓存、CPU时钟周期等,而且并行的TCP也会竞争带宽,并行能力也是受限制的,往往无法达到理想状态下的六倍速。
从下面这张谷歌浏览器的网络监控可以看出每次请求6个:
2. HTTP的使命
可见,我们采取了许多方法,希望可以并行处理请求/响应,但都不能从根本上解决问题。况且,很多方法与HTTP/1.x的设计理念是背道而驰的,在HTTP/1.x下,却没有正确利用好HTTP/1.x的特性。
于是,HTTP/2带着提高性能的使命,应运而生。
那么HTTP/2做了什么改变?
先对HTTP/2产生的影响有一个直观的认识:
这里有个Akamai公司(全球最大的CDN服务商)的一个官方演示,他是分别使用HTTP/1.1和HTTP/2请求379张小图片,最终拼成一副大图片,然后对比消耗时间。
大家可以点开观察一下效果。这里我截取一下我电脑的结果:
家可以自己试试,我这个结果有点逗逼,测试了几次都是HTTP/1.1在20s以上,HTTP/2在2s以内。和网上别人的HTTP/1.1在7s以内差距有点大。但更可以明显看出,HTTP/2下加载时间和HTTP/1.1都不在一个数量级,那么HTTP/2到底为什么这么快?我们还是从它的新特性来进行全面的了解。
以下着重介绍五个特性:二进制分帧层、多向请求与响应、优先级和依赖性、首部压缩、服务器推送。
3. HTTP/2五大特性
3.1. 二进制分帧层
二进制分帧层(BinaryFramingLayer)指的是位于套接字接口与应用可见的高层HTTP API之间的一个新机制:HTTP的语义,包括各种动词、方法、首部,都不受影响,不同的是传输期间对它们的编码方式变了。
在新引进的二进制分帧层上,HTTP/2将所有传输的信息分割为更小的消息和帧,且都采用二进制格式的编码。
说了那么多都什么gui,也没听懂,还是看图吧:
从图上可以看到:在上面HTTP API和下面TCP连接中引入了一个二进制分帧层;在二进制分帧层上,它将我们以前普通的HTTP请求的请求头和请求正文分割成了两个部分:HEADERS帧和DATA帧。请求头起始行、首部被分割到HEADERS帧,实体正文被分割到DATA帧。
接下来,我们再深入地了解下这些被分割后的二进制帧是怎么工作的:
HTTP/2同域名的所有通信都是在一个TCP连接上完成,这个连接可以承载任意数量的双向数据流。而每个数据流都是以消息的形式发送的,消息由一个帧或多个帧组成。
u 流:已建立的连接上的双向字节流
u 消息:与逻辑消息对应的完整的一系列数据帧
u 帧:HTTP/2通信的最小单位,每个帧包含帧首部
好像很复杂的样子,咱们来捋一捋:
TCP连接在客户端和服务器间建立了一条运输的通道,可以双向通行,当一端要向另一端发送消息时,会先把这个消息拆分成几部分(帧),然后通过发起一个流对这些帧进行发送,最后在另一端将同一个流的帧重新组合。
这个过程就好像我们在搬家的时候,会把一个桌子先拆散成零部件,然后通过几次的搬运,到了新家后,再把桌子重新拼装起来。
下图展示了流、消息与帧的关系(注意到没,HEADERS帧总是在最前面的):
HTTP/2规范一共规定了10种不同的帧,其中最基础的两种分别对应于HTTP/1.1的DATA帧和HEADERS帧。
3.2. 多向请求与响应(多路复用)
多路复用允许同时通过单一的TCP连接发起多重的请求/响应消息,客户端和服务器可以把HTTP消息分解为互不依赖的帧,然后乱序发送,最后再在另一端根据 StreamID 把它们重新组合起来。
前面提到的一端发送消息会先对消息进行拆分,与此同时,也会给同一个消息拆分出来的帧带上一个编号(StreamID),这样在另一端接收这些帧后就可以根据编号对它们进行组合。
也正是有了这种编号的方式,当某一端发送消息时,可以发送多个消息拆分出来的多个帧(发起多个流),且这些帧可以乱序发送,因为这些帧都有自己的编号,它们之间互不影响。
下图展示了单一的TCP连接上有多个请求/响应并行交换:
从图上可以看出,服务器向客户端发送stream1的多个DATA帧(说明HEADERS帧已发送完毕)与stream3的HEADERS帧和DATA帧,客户端正在向服务器发送stream5的DATA帧,可见,帧的发送是乱序的,且请求/响应是并行的。
细心的你会发现,stream1中有多个DATA帧,这是为什么呢?因为有DATA帧有长度的控制(2的14次方-1字节,约16383个字节),应用数据过大时,会被拆分成多个DATA帧(还记得讲二进制分帧层展示的HTTP/1.1的请求被分割成更小的帧吗?DATA帧就是用来携带应用数据的)。
3.3. 优先级和依赖性
新建流的终端可以在报头帧中包含优先级信息来对流标记优先级。
优先级的目的是允许终端表达它如何让对等端管理并发流时分配资源。更重要的是,在发送容量有限时优先级能用来选择流来传输帧。
HTTP/2中,流可以有一个优先级属性(即“权重”):
可以在HEADERS帧中包含优先级priority属性;
可以单独通过PRIORITY帧专门设置流的优先级属性。
流的优先级用于发起流的终端(客户端/服务器)向对端(接收的一方)表达需要多大比重的资源支持,但这只是一个建议,不能强制要求对端一定会遵守。
借助于 PRIORITY帧,客户端同样可以告知服务器当前的流依赖于其他哪个流。该功能让客户端能建立一个优先级“树”,所有“子流”会依赖于“父流”的传输完成情况。
不依赖任何流的流的流依赖为 0x0。换句话说,不存在的流标识0组成了树的根。
我们通过以下几个例子来理解下优先级“树”:
第一种情况:流A和流B不依赖流,即为0x0;流A的权重为12,流B的权重为4;则流A分配到的资源占比为12/(12+4)=12/16,流B分配到的资源占比为4/(12+4)=4/16。
第二种情况:流D为0x0,流C依赖于流D;流D能被分配到全额资源,等到流D关闭后,依赖于流D的流C也会被分配到全额资源(它是唯一依赖于流D的流,它的权重的大小此时并不重要,因为没有竞争的流)。
第三种情况:流D为0x0,流C依赖于流D,流A和流B依赖于流C;流D能被分配到全额资源,等到流D关闭后,依赖于流D的流C也会被分配到全额资源;等到流C关闭后,依赖于流C的流A和流B根据权重分配资源(3:1)。
第四种情况:流D为0x0,流C和流E依赖于流D,流A和流B依赖于流C;流D能被分配到全额资源,等到流D关闭后,依赖于流D的流C的流E和流B根据权重分配资源(1:1);等到流C关闭后,依赖于流C的流A和流B根据权重分配资源(3:1)。
前面说到,“可以单独通过PRIORITY帧专门设置流的优先级属性”,也就是说可以对原本没有优先级属性(包括依赖关系)的流进行设置,也可以对原本已有优先级属性的流进行修改。因此,优先级可以在传输过程中被动态的改变。
3.4. 首部压缩
HPACK是专门为HTTP/2量身定制的为有效地表示HTTP首部字段的压缩技术。
在服务器和客户端各维护一个“首部表”,表中用索引代表首部名,或者首部键-值对,上一次发送两端都会记住已发送过哪些首部,下一次发送只需要传输差异的数据,相同的数据直接用索引表示即可。
具体实现如下图所示:
这个过程比较容易理解:通过索引表的对应关系,来标记首部表中的不同信息。
同一个域名下的请求/响应的首部往往有很多重复的信息,当客户端要向服务器发送某个请求时,通过查找索引表,发现该信息的首部已经发送过,此时服务器端的索引表也应该有对应的信息,则不需要再次发送;若查找发现部分首部信息不在索引表中,则发送该部分信首部息即可。
如在上图的示例中,第二个请求只需要发送变化了的路径首部(:path),其他首部没有变化,就不用再发送了。
比如我第一次请求index.html,那么我携带所有的信息过去,第二次如果我请求该服务器下面的login.html,那么只需要携带这个路径:path/login.html就行了,别的信息不用携带,减少数据发送量。
3.5. 服务器推送
服务器推送(ServerPush),服务器可以对一个客户端请求发送多个响应。也就是说,除了对最初请求的响应外,服务器还可以额外向客户端推送资源。
在了解“二进制分帧层”的时候我们提到,“HTTP/2规范规定了10种不同的帧”,其中有一种名为“PUSH_PROMISE”,就是在服务器推送的时候发送的。当客户端解析帧时,发现它是一个PUSH_PROMISE类型,便会准备接收服务端要推送的流。
从上图可以看出,当服务器响应了HTML请求后,可以知道客户端接下来要发送JS请求、CSS请求,于是服务器通过推送的方式(主动发起新流,而不是等客户端请求然后再响应),向客户端发出要约(PUSH_PROMISE)。当然,客户端可以选择缓存这个资源,也可以拒绝这个资源。
这个过程有点类似于我们常用的资源内嵌的手段:将一个图片资源转为base64编码嵌入CSS文件中,当客户端发起CSS请求时,也会请求该图片。因此在响应CSS请求后,服务器会强制(客户端是无法拒绝的)向客户端发送图片响应。但内嵌资源是无法被单独缓存的,而服务器推送的资源是可以被缓存的。
需要注意,服务器必须遵循请求-响应的循环,只能借着请求的响应来推送资源,也就是说,如果客户端没有发送请求,服务器是没法先手推送的。而且,如上图中stream4,PUSH_PROMISE帧必须在返回响应(DATA帧)之前发送,因为万一客户端请求的恰好是服务器打算推送的资源,那传输过程就会混乱了。
注:由客户端发起的流StreamID为奇数,由服务器发起的流StreamID为偶数,回顾上面的图就能发现啦!
4. 小结
u HTTP/2通过二进制分帧与多路复用机制,有效解决了HTTP/1.x下请求/响应延迟的问题。
u 新的首部压缩技术使HTTP/1.x首部信息臃肿的问题得到解决。
u 优先级和依赖性与服务器推送使得我们可以更有效地利用好这个单一的TCP连接。
可见,HTTP/2在HTTP/1.1的基础上有了一个较大的性能提升。这时候你会发现,我们针对HTTP/1.x的一些优化手段(如上文提到的资源内嵌)似乎有点不适用了。
传智播客·黑马程序员郑州校区地址
河南省郑州市 高新区长椿路11号大学科技园(西区)东门8号楼三层
联系电话 0371-56061160/61/62
来校路线 地铁一号线梧桐街站A口出