http1.1
http1.1的优点
- 1. 简单
HTTP 基本的报文格式就是 header + body
,头部信息也是 key-value
简单文本的形式,易于理解,降低了学习和使用的门槛。
- 2. 灵活和易于扩展
HTTP 协议里的各类请求方法、URI/URL、状态码、头字段等每个组成要求都没有被固定死,都允许开发人员自定义和扩充。
同时 HTTP 由于是工作在应用层( OSI
第七层),则它下层可以随意变化,比如:
- HTTPS 就是在 HTTP 与 TCP 层之间增加了
SSL/TLS
安全传输层; - HTTP/1.1 和 HTTP/2.0 传输协议使用的是 TCP 协议,而到了 HTTP/3.0 传输协议改用了
UDP 协议
。
- 3. 应用广泛和跨平台
互联网发展至今,HTTP 的应用范围非常的广泛,从台式机的浏览器到手机上的各种 APP,从看新闻、刷贴吧到购物、理财、吃鸡,HTTP 的应用遍地开花,同时天然具有跨平台的优越性。
http1.1的缺点
缺点很明显
- 无状态双刃剑
无状态的好处,因为服务器不会去记忆 HTTP 的状态,所以不需要额外的资源来记录状态信息,这能减轻服务器的负担,能够把更多的 CPU 和内存用来对外提供服务。
无状态的坏处,既然服务器没有记忆能力,它在完成有关联性的操作时会非常麻烦。
对于无状态的问题,解法方案有很多种,其中比较简单的方式用 Cookie
技术和Session
技术,而Session又是基于Cookie实现的,这里不过多介绍俩张方案的实现。
- 明文传输
明文意味着在传输过程中的信息,是可方便阅读的,比如 Wireshark 抓包都可以直接肉眼查看,为我们调试工作带了极大的便利性。
这正是这样,HTTP 的所有信息都暴露在了光天化日下,相当于信息裸奔
,也就是http传输非常不安全。
- 传输过程数据可能被篡改
由于数据没有进行加密,传输过程都是明文,则很有可能被中间站拦截并篡改,所以http无法证明报文的完整性,且容易被篡改。
- 无法验证通讯双方身份
不验证通信双方的身份,因此有可能遭遇伪装并劫持流量。
对于无状态
这一点特点来说,不管是http1.1还是http2.0和http3.0都保持这一特点,因为这一特点并不是完全是一个缺点。
而对于明文传输
、保证不了报文的完整性
和无法验证双方身份
的这3个缺点,https
基本都解决啦,https是在应用层和传输层中间增加了一层SSL/TSL协议,https具体解决这3个问题的实现方式这里也不过多介绍。但是需要注意的一点是普通的https仅验证了服务端的身份(通过CA证书),其实并没有验证客户端的身份,所以有的https服务仍然可以通过抓包工具获取。
你可能也发现了,有的https服务可以被抓包,有的却抓不到,原因就是看https服务是否开启的双向验证
,开始了双向验证之后不仅是在服务端需要按照证书,客户端也需要安装证书,也就是只有开启了双向验证的https来可以验证双方身份,且无法被抓到工具获取
。
http1.1的性能如何
HTTP 协议是基于 TCP/IP
,并且使用了「请求 - 应答」
的通信模式,所以性能的关键就在这两点里。
「请求 - 应答」
的通信模式是在同一个TCP连接
里客户端发出一个请求之后只能等待该请求被响应之后,客户端才可以发生下一个请求,如果上个请求一直没有被响应,那么就是会被阻塞,可以发现http的性能的关键就在于此。
- 长链接
早期 HTTP/1.0 性能上的一个很大的问题,那就是每发起一个请求,都要新建一次 TCP 连接(三次握手),请求完成后,都要断开TCP链接(4次挥手),俗称短链接,而且是串行请求,做了无谓的 TCP 连接建立和断开,增加了很大的通信开销。
为了解决上述 TCP 连接问题,HTTP/1.1 提出了长连接
的通信方式,也叫持久连接
,且是http1.1的默认方式了。这种方式的好处在于减少了 TCP 连接的重复建立
和断开
所造成的额外开销,减轻了服务器端的负载。
短连接的特点是:只有任意一方执行完代码或者任意一方显示的明确提出断开连接时则会断开链接。
长连接的特点是:双方代码执行完成后并不会断开链接,而只要任意一端显示的明确提出断开连接,才会进行4次挥手断开TCP连接。
- 管道传输
HTTP/1.1 默认采用了长连接
的方式,这使得管道
(pipeline)网络传输成为了可能。
即可在同一个 TCP 连接里面,客户端可以发起多个请求
,只要第一个请求发出去了,不必等其回来,就可以发第二个请求出去,可以减少整体的响应时间。
还记得开头说的「请求 - 应答」
的通信模式吗?该模式下同一个tcp客户端只能同时发送一个请求,只有该请求被响应之后才可以发送下一个请求,这种模式对应http的性能影响很大,而管道传输
其实就是想解决这样问题的,但是遗憾的是管道传输
并没有本质上解决上述的问题,原因继续往下看。
- 队头阻塞
举例来说,客户端需要请求两个资源。以前的做法是,在同一个 TCP 连接里面,先发送 A 请求,然后等待服务器做出回应,收到后再发出 B 请求。那么,管道机制
则是允许浏览器同时发出 A 请求和 B 请求
,如下图:
但是服务器必须按照接收请求的顺序发送对这些管道化请求的响应。
如果服务端在处理 A 请求时耗时比较长,那么后续的请求的处理都会被阻塞住,这称为「队头堵塞」。所以管道传输
只解决了请求队头阻塞
而没有解决响应队头阻塞
。也就是说管道传输
其实很鸡肋,没什么卵用。
TIP:实际上 HTTP/1.1 管道化技术不是默认开启
,而且浏览器基本都没有支持。
有没有想过为什么响应时必须按照请求的顺序返回呢?如果没有按照请求的顺序返回会发生什么情况呢?我举个🌰,如果没有按照请求顺序响应会发生什么结果:
如果有俩个请求a和b,请求a先到达服务器,但由于某种原因导致a的响应被阻塞,而请求b随后到达服务器并得到了及时的响应,那么返回的响应的顺序其实发生了颠倒
。
客户端可能会错误地将属于请求a和响应b关联,此时数据就会发生了错乱。这就导致客户端难以确定哪个请求对应哪个响应,从而引发数据错乱。
这就是为什么要求响应和请求的顺序要一致,一致时就是可以顺序匹配请求和响应
,将不会出现上述的数据错乱问题。
本质原因就是请求和响应不能一一对应而只能按照顺序来匹配,可以继续往下看,http2.0是怎么解决这个对应的问题的。
http2.0
可以发现http1.1的性能其实很一般,http2.0对其做了很多改进,使性能发生了质的提升。废话不多说,上图。
那 HTTP/2 相比 HTTP/1.1 性能上的改进:
- 内置TLS协议。
- 头部压缩:使用静态表、动态表和HPack实现。
- 二进制格式:Header+Body都使用二进制传输。
- 并发传输:使用Stream、fream实现,Stream是实现并发传输的关键。
- 服务器主动推送资源。
头部压缩
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% 的高压缩率。
静态字典
首先TCP连接建立后,客户端和服务端都会有一张静态字典
,它是写入到 HTTP/2 框架里的,不会变化的,静态表里共有61 组
,如下图:
表中的
Index
表示索引(Key),Header Value
表示索引对应的 Value,Header Name
表示字段的名字,比如 Index 为 2 代表Header头中method: GET,Index 为 8 代表Header头中的状态码 Status :200。
你可能注意到,表中有的 Index 没有对应的 Header Value,这是因为这些 Value 并不是固定
的而是变化的
,这些 Value 都会经过 Huffman 编码
后,才会发送出去,具体是怎么实现的呢?继续往下看:
我们来看个具体的例子,比如Header头中下面这个 server 头部字段,在 HTTP/1.1 的形式如下:
server: nghttpx\r\n
先给出结论:在http1.1中算上冒号空格和末尾的\r\n,共占用了
17 字节
,而使用了静态表
和Huffman 编码
,可以将它压缩成8 字节
,压缩率大概47%
。
根据 RFC7541 规范,如果头部字段
属于静态表范围
,并且Value
是变化
的,整个头部格式如下图:
我抓了个 HTTP/2 协议的网络包,你可以从下图看到,高亮部分就是 server: nghttx 头部字段的二进制数据,只用了 8 个字节而已。
对照着头部格式来一步一步分析:
如果头部字段属于静态表范围,并且Value 是变化时,
第一个字节
的前俩位固定为01
,后6位是头部字段server
在静态表中的索引值
,也就是54
,转化为二进制为110110
,拼接起来之后第一个字节为01110110
。第二个字节中的第一个位
H
表示Value 是否经过 Huffman 编码,1
表示经过 Huffman 编码,0
则相反。后面的7个bit位表示Value 的长度
,也就是nghttx
经过huffman编码后的长度为7
,也就是0111
,至于为什么是7,继续看下面,这里写暂且认为是7,拼接上首位1后,第二个字节表示为10000111
。-
接着计算
nghttx
的长度。value
的值是通过huffman编码
算出来的,而Huffman 编码的原理是将高频出现的信息用「较短」的编码表示,从而缩减字符串长度。
于是,在统计大量的 HTTP 头部后,HTTP/2 根据
出现频率
将 ASCII 码编改为了Huffman 编码表
,可以在RFC7541 文档
找到这张静态 Huffman 表
,我就不把表的全部内容列出来了,我只列出字符串 nghttpx 中每个字符对应的 Huffman 编码,如下图:
通过查表后,字符串nghttpx
的 Huffman 编码在下图看到,共6
个字节,每一个字符的 Huffman 编码
,我用相同的颜色将他们对应起来了,最后的 7 位是补位的
。
最终,server 头部
的二进制数据对应的静态头部格式如下:
动态字典
静态表
只包含了61
种高频
出现在头部的字符串,不在静态表范围内的头部字符串就要自行构建动态表
,它的 Index
从62
起步,会在编码解码的时候随时更新,也就是静态字典喝动态字典是结合使用的。
比如,第一次发送请求时的request头部中的「Cookie」
字段数据有上百个字节,经过 Huffman 编码
发送出去后,客户端就会更新自己的动态字典
,添加一个Index 号 62
的数据。
当服务器收到请求之后会更新自己的动态表
,也添加一个新的 Index 号 62
。
那么在下一次请求的时候,就不用重复发这个字段的数据了,只用发 1
个字节的 Index 号就好了,因为双方都可以根据自己的动态表获取到字段的数据。
细心的人可能发现,如果客户端请求发出后,由于网络原因服务端并没有收到请求,此时会出现的情况是客户端已经更新Index为62的记录,而服务并没有更新。
如果此时客户端再次请求时,携带的是62的Index,而服务端收到62之后不清楚是什么意思,因为服务端并没有存储62的数据,此时就会出现问题,
这是http2.0的一个潜在问题,http3.0对该问题进行了修复。
需要注意的是:新建的连接初始化时只有静态表,只有在同一个连接上后续的请求时才会动态的增加动态字典,连接销毁时对应的动态字典也就随之消失。
如果消息字段在 1 个连接上只发送了 1 次,或者重复传输时,字段总是略有变化,动态表就无法被充分利用了。
因此,随着在同一 HTTP/2 连接上发送的报文越来越多,客户端和服务器双方的「字典」积累的越来越多,理论上最终每个头部字段都会变成 1 个字节的 Index,这样便避免了大量的冗余数据的传输,大大节约了带宽。
理想很美好,现实很骨感。动态表越大,占用的内存也就越大
,如果占用了太多内存,是会影响服务器性能的,因此 Web 服务器都会提供类似 http2_max_requests
的配置,用于限制一个连接上能够传输的请求数量,避免动态表无限增大
,请求数量到达上限后,就会关闭 HTTP/2 连接来释放内存。
综上,HTTP/2 头部的编码通过「静态表、动态表、Huffman 编码」共同完成的。
二进制
HTTP/2 厉害的地方在于将 HTTP/1 的文本格式改成二进制格式传输数据,极大提高了 HTTP 传输效率,而且二进制数据使用位运算能高效解析。
二进制数据传输的基本单位是二进制帧
,即为fream
,下图为fream
的结构
- 帧开头的前 3 个字节表示帧数据(Frame Playload)的长度。
- 帧长度后面的一个字节是表示
帧的类型
,HTTP/2 总共定义了 10 种类型的帧,一般分为数据帧
和控制帧
两类,如下表格:
我们主要关注数据帧
,我们知道http2.0中的·Header+Body·都是使用·二进制帧·来实现,如果帧的类型为HEADRERS
则表示该帧的数据为·Header数据·,如果帧的类型为DATA
则表示该帧的数据为Body
数据。
帧的类型
主要的作用是表明该数据是什么类型的数据。 - 帧类型后面的一个字节是
标志位
,可以保存 8 个标志位,用于携带简单的控制信息,比如:-
END_HEADERS
表示头数据结束标志,相当于 HTTP/1 里头后的空行(“\r\n”); -
END_Stream
表示单方向数据发送结束,后续不会再有数据帧。 -
PRIORITY
表示流的优先级;
-
该
标志位
的作用是非常重要的,想象一个http请求被分成了多个帧数据
发生,服务端在接受到这么多帧数据
的时,可以根据数据帧的类别
区别出哪些帧是Header数据哪些是Body数据。但是
一个完整的Header数据或者Body数据是被分成多个帧数据发送
的,服务端是当收到帧类别为HEADRERS
且标志位为END_HEADERS
时表示该请求的header数据已经全部接受完成了,可以处理header请求了;同理当收到帧类别为
DATA
且标识位为END_Stream
时表示该请求的body数据全部发生完成了,可以处理body请求了。可以发现标志位的作用非常重要,但是有没有发现一点,如果帧数据发生顺序错乱,会发生严重的问题,比如当收到了一个帧类别为
HEADRERS
且标志位为END_HEADERS
的帧数据时,就代表着header头数据发生完毕了,但是由于网络原因导致其中的一个header帧数据又发生过来了,这不出现很重的问题了吗?我已经认为header数据发生完毕了,然后过一会,你发过来一个header数据,我该怎么处理啊,全乱了。
所以同一个stream里的帧数据是严格要求顺序的发送的,不可以乱序发送的,时序问题由tcp的顺序性保证(序列号)。
- 帧头的最后 4 个字节是
流标识符
(Stream ID),最高位被保留不用,只有 31 位可以使用,因此流标识符的最大值是 2^31,大约是 21 亿,它的作用是用来标识该Frame
属于哪个Stream
,接收方可以根据这个信息从乱序的帧(这里的乱序是指所属不同Stream的Fream是乱序的,但是同一个Stream内的Fream肯定是有序的)里找到相同 Stream ID 的帧,从而有序组装信息。
如果你不懂Stream和Fream的关系,那么继续往下看
并发传输
知道了 HTTP/2 的帧结构
后,我们再来看看它是如何实现并发传输的。
我们都知道 HTTP/1.1 的实现是基于请求-响应模型的。同一个TCP连接中
,HTTP 完成一个事务(请求与响应),才能处理下一个事务,也就是说在发出请求等待响应的过程中,是没办法做其他事情的,如果响应迟迟不来,那么后续的请求是无法发送的,也造成了队头阻塞
的问题,这也是http1.1的性能关键。
而 HTTP/2 就很牛逼了,引出了 Stream
概念,多个 Stream 可复用在一条 TCP 连接。实现了在同一个TCP连接上可以并发多个请求和响应。
为了理解 HTTP/2 的并发是怎样实现的,我们先来理解 HTTP/2 中的 Stream、Message、Frame 这 3 个概念。
你可以从上图中看到:
- 1 个 TCP 连接包含一个或者多个 Stream,Stream 是 HTTP/2 并发的关键技术。一个TCP连接由相同的四元组组成,即源ip、源端口、目标ip和目标端口,也就是客户端并发请求,服务端也可以并发响应;
- Stream 里可以包含 1 个或多个 Message,Message 对应 HTTP/1 中的请求或响应,由 HTTP 头部和包体构成;
- Message 里包含一条或者多个 Frame,Frame 是 HTTP/2 最小单位,以二进制压缩格式存放 HTTP/1 中的内容(头部和包体);
我估计你看到这里还是云里雾里的,为什么Stream能实现并发传输呢?Stream到底是个什么呢?
想知道这个问题的答案,先来回顾下http1.1中开启管道传输之后为什么只支持客户端并发请求而不支持服务端的并发响应呢?根本原因就是服务端的响应和客户端的请求对应不上,只能根据请求和响应的顺序来匹配。
而Stream
其实是一个唯一的ID标识
,在同一个tcp连接中的 Stream ID 是不能复用的,只能顺序递增
,Stream
可以理解为并不是一个真实存在的东西,就是一个唯一的自增ID。
我们知道帧数据Fream
是数据传输的基本单位,而Fream
结构中有个流标识符
是用来表示该Fream
所属的Stream ID
。
客户端的每次请求都会分配一个唯一的Stream ID
。比如客户端发出一个请求,Stream ID为123
,而请求体中Header+Body
会被分割成多个Fream
,且这些Fream
的标识符都将是123
;当服务端响应数据时,也会将响应体中的Header+Body
分割成多个Fream
,且这些响应体的所有Fream
的标识符也同样是123
。
看到这里,我想你应该明白了Stream是怎么实现并发传输的了吧。http1.1中由于不知道返回的响应是属于哪个请求的,所以只能默认按照顺序匹配。而http2.0中请求和响应共用一个Stream ID,这样就可以将请求和响应进行精准匹配啦。
因此,我们可以得出个结论:多个 Stream 跑在一条 TCP 连接,同一个 HTTP 请求与响应是跑在同一个 Stream 中,HTTP 消息可以由多个 Frame 构成, 一个 Frame 可以由多个 TCP 报文构成。
在 HTTP/2 连接上,不同 Stream 的帧是可以乱序发送的(因此可以并发不同的 Stream )
,因为每个帧的头部会携带 Stream ID 信息,所以接收端可以通过 Stream ID 有序组装成 HTTP 消息,而同一 Stream 内部的帧必须是严格有序的
,在上面帧数据结构介绍标志位
的说明为什么必须保持有序。
比如下图,服务端并行乱序
的地发送了两个响应: Stream 1 和 Stream 3,这两个 Stream 都是跑在一个 TCP 连接上,客户端收到后,会根据相同的 Stream ID 有序组装成 HTTP 消息,并将组装完成的消息发送给相同Stream ID 的请求。
客户端和服务器双方都可以建立 Stream,因为
服务端可以主动推送资源给客户端
。
客户端建立的 Stream 必须是奇数号
,而服务器建立的 Stream 必须是偶数号
。
同一个连接
中的 Stream ID 是不能复用
的,只能顺序递增。从帧结构
中可以发现,流标识符
(Stream ID),只有 31 位可以使用,因此流标识符的最大值是 2^31,大约是 21 亿
,所以当 Stream ID 耗尽时,需要发一个控制帧 GOAWAY
,用来优雅关闭 TCP 连接。
服务器主动推送资源
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;
}
http2.0存在的缺点
http2.0相对于http1.1性能确实提升了很多,但是仍然还存在一些问题
- 对头阻塞
- TCP和TLS的握手延时
- 网络迁移需要重新建立连接
TCP 对头阻塞
HTTP/2 多个请求是跑在一个 TCP 连接中的,那么当 TCP 丢包时,整个 TCP 都要等待重传,那么就会阻塞该 TCP 连接中的所有请求。
比如下图中,Stream 2 有一个 TCP 报文丢失了,那么即使收到了 Stream 3 和 Stream 4 的 TCP 报文,应用层也是无法读取读取的,相当于阻塞了 Stream 3 和 Stream 4 请求。
因为HTTP2.0是基于TCP 是字节流协议,TCP 层必须保证收到的字节数据是完整且有序的,如果序列号较低的 TCP 段在网络传输中丢失了,即使序列号较高的 TCP 段已经被接收了,也会被阻塞在
传输层
,应用层
也无法从内核中读取到这部分数据,只有当所有的TCP段都被接受成功之后,才会将数据组装之后发送给应用层
,应用层
才会读取数据。
可以发现出现阻塞的本质原因是:阻塞出现在了传输层,而导致应用层读不到数据。
那么如果让阻塞出现在应用层而不要出现在网络层是不是就可以避免对头阻塞了,可以看下http3.0的实现。
可以发现Http2.0出现对头阻塞的场景只发送在Tcp丢包
的场景下。
可以发现http2.0不管怎么优化也解决不了TCP的对头阻塞
问题,除非把TCP协议换了, 这是TCP 协议固有的问题。可以接续往下看http3.0是怎么解决TCP对头阻塞的。
TCP和TLS的握手延时
发起 HTTP 请求时,需要经过 TCP 三次握手
和 TLS 四次握手
(TLS 1.2)的过程,因此共需要 3 个 RTT 的时延才能发出请求数据。
另外,TCP 由于具有「拥塞控制」
的特性,所以刚建立连接的 TCP 会有个「慢启动」
的过程,它会对 TCP 连接产生“减速”效果,给人的感觉就是突然卡顿了一下。
网络迁移需要重新建立连接
一个 TCP 连接
是由四元组
(源 IP 地址,源端口,目标 IP 地址,目标端口)确定的,这意味着如果 IP 地址或者端口变动了,就会导致需要 TCP 与 TLS 重新握手,这不利于移动设备切换网络的场景,比如 4G 网络环境切换成 WiFi。
这些问题都是 TCP 协议固有的问题,无论应用层的 HTTP/2 在怎么设计都无法逃脱。要解决这个问题,就必须把传输层协议替换成 UDP,这个大胆的决定,HTTP/3 做了!
http3.0
http2.0有3个缺陷,http3.0都给解决了,接下来看下是怎么一步一步解决的。
可以发现http3.0有几个特点
- 传输层由TCP改用成UDP协议。我们深知,UDP 是一个简单、不可靠的传输协议,而且是 UDP 包之间是无序的,也没有依赖关系。而且,UDP 是不需要连接的,也就不需要握手和挥手的过程,所以天然的就比 TCP 快。
-
应用层
新增QUIC
协议: 虽然UDP是不具备可靠性等特性的,但是UDP的上层协议QUID
,它具有类似 TCP 的连接管理、拥塞窗口、流量控制的网络特性,相当于将不可靠传输的 UDP 协议变成“可靠”的了,所以不用担心数据包丢失的问题。 - TLS协议内置在了
QUIC
协议中。避免了TLS的4次握手。 - 由HTTP2.0中的
HPACK
编码格式改为了QPACK
编码格式。
无队头阻塞
QUIC 协议
也有类似 HTTP/2 Stream 与多路复用
的概念,也是可以在同一条连接上并发传输多个 Stream的,这个优点http3.0是继承了的。
在来回忆下,为什么HTTP2.0会有TCP对头阻塞?
主要原因是:当tcp发送丢包时,tcp所在的传输层会发生阻塞,进而应用层也发生阻塞。只有组装完成所有tcp段之后,应用层才会读取。
而上面的http3.0介绍中TCP改成了UDP,而UDP并不是对数据的连续性,时序性等问题进行验证,所以传输层肯定是不会发生阻塞了。
因为UDP是不可靠的,所以在应用层
实现了QUIC
协议来保证消息的可靠性。http2.0和http3.0对比可发现一点:阻塞发生的层级由http2.0的传输层变更到了http3.0的应用层
。正是由于这一变更,如果http3.0请求时发生丢包则应用层只会阻塞本请求,后续的请求是不会阻塞的,应用层都是可以读取到的;同理,如果ttp3.0响应时发生丢包则应用层也只会阻塞本响应,后续的响应是不会阻塞的,应用层都是可以读取到的。
所以,QUIC 连接上的多个 Stream 之间并没有依赖,都是独立的,某个流发生丢包了,只会影响该流,其他流不受影响。
更快的连接建立
对于 HTTP/1 和 HTTP/2 协议,TCP 和 TLS 是分层的,分别属于内核实现的传输层、OpenSSL 库实现的表示层,因此它们难以合并在一起,需要分批次来握手,先 3次
TCP 握手,再 4次
TLS 握手。
HTTP/3 在传输数据前虽然需要 QUIC 协议握手,这个握手过程只需要 1 RTT
,握手的目的是为确认双方的「连接 ID」,连接迁移就是基于连接 ID 实现的。
但是 HTTP/3 的 QUIC 协议并不是与 TLS 分层
,而是 QUIC 内部包含了 TLS
,它在自己的帧会携带 TLS 里的“记录”,再加上 QUIC 使用的是 TLS 1.3,因此仅需 1 个 RTT
就可以「同时」完成建立连接与密钥协商,甚至在第二次连接的时候,应用数据包可以和 QUIC 握手信息(连接信息 + TLS 信息)一起发送,达到0-RTT
的效果。
至于为什么第二次连接时为 0-RTT
,接着继续看下面。
网络迁移
在前面我们提到,基于 TCP 传输协议的 HTTP 协议,由于是通过四元组(源 IP、源端口、目的 IP、目的端口)确定一条 TCP 连接。
那么当移动设备的网络从 4G 切换到 WiFi 时,意味着 IP 地址变化了,那么就必须要断开连接,然后重新建立连接,而建立连接的过程包含 TCP 三次握手和 TLS 四次握手的时延,以及 TCP 慢启动的减速过程,给用户的感觉就是网络突然卡顿了一下,因此连接的迁移成本是很高的。
而 QUIC 协议没有用四元组
的方式来“绑定”连接
,而是通过连接 ID
来标记通信的两个端点
,客户端和服务器可以各自选择一组 ID 来标记自己,因此即使移动设备的网络变化后,导致 IP 地址变化了,只要仍保有上下文信息(比如连接 ID、TLS 密钥等),就可以“无缝”地复用原连接,消除重连的成本,没有丝毫卡顿感,达到了连接迁移的功能。