介绍铺垫算是我觉得从一层一层升级比较容易理解的啦,当然直接面肯定不会给你那么多时间去回答,直接看不同版本介绍以及优缺点即可啦
1.介绍铺垫
HTTP 的下层协议是 TCP,需要经历三次握手才能建立连接,而 HTTP 1.0 的时候一次请求和响应结束就会断开链接,这样下次请求又要重新三次握手来建立连接。
为了减少这种建立 TCP 链接的消耗,HTTP 1.1 支持了 keep-alive,只要请求或响应头带上 Connection: keep-alive,就可以告诉对方先不要断开链接,我之后还要用这个链接发消息。当需要断开的时候,再指定 Connection: close 的 header。
这样就可以用同一个 TCP 链接进行多次 HTTP 请求响应了:
但这样虽然减少了链接的建立,在性能上却有问题,下次请求得等上一个请求返回响应才能发出。
这个问题有个名字,叫做队头阻塞,很容易理解,因为多个请求要排队嘛,队前面的卡住了,那后面的也就执行不了了。
怎么解决这个问题呢?
HTTP 1.1 提出了管道的概念,就是多个请求可以并行发送,返回响应后再依次处理。
也就是这样:
其实这样能部分解决问题,但是返回的响应依然要依次处理,解决不了队头阻塞的问题。
所以说管道化是比较鸡肋的一个功能,现在绝大多数浏览器都默认关闭了,甚至都不支持。
那还能怎么解决这个队头阻塞的问题呢?
开多个队不就行了。
浏览器一般会同一个域名建立 6-8 个 TCP 链接,也就是 6-8 个队,如果一个队发生队头阻塞了,那就放到其他的队里。
这样就缓解了队头阻塞问题。
我们写的网页想尽快的打开就要利用这一点,比如把静态资源部署在不同的域名下。这样每个域名都能并发 6-8 个下载请求,网页打开的速度自然就会快很多。
这种优化手段叫做“域名分片”,CDN 一般都支持这个。
除了队头阻塞的问题,HTTP 1.1 还有没有别的问题?
有,比如 header 部分太大了。
不知道大家有没有感觉,就算你内容只传输几个字符,也得带上一大堆 header:
而且这些 header 还都是文本的,这样占据的空间就格外的大。
比如,如果是二进制,表示 true 和 false 直接 1 位就行了,而文本的那就得经过编码,“true” 就占了 4 个字节,也就是 32 位。那就是 32 倍的差距呀!
所以呢,HTTP 1.1 的时候,我们就要尽量避免一些小请求,因为就算请求的内容很少,也会带上一大段 header。特别是有 cookie 的情况,问题格外明显。
因此,我们的网页就要做打包,也就是需要打包工具把模块合并成多个 chunk 来加载。需要把小图片合并成大图片,通过调整 background:position 来使用。需要把一些 css、图片等内联。而且静态资源的域名也要禁止携带 cookie。
这些都是为了减少请求次数来达到提高加载性能的目的。
而且 HTTP 的底层是 TCP,其实是可以双向传输数据的,现在却只能通过请求---响应这种一问一答的方式,并没有充分利用起 TCP 的能力。
聊了这么多,不知道大家是否有优化它的冲动了。
也就是因为这些问题,HTTP 2.0 出现了,做了很多性能优化,基本解决了上面那些问题。
那 HTTP2 都做了哪些优化呢?
HTTP 2.0 的优化
先不着急看 HTTP 2.0 是怎么优化的,就上面那些问题来说,如果让我们解决,我们会怎么解决?
比如队头阻塞的问题,也就是第二个响应要等第一个响应处理完之后才能处理。怎么解决?
这个很容易解决呀,每个请求、响应都加上一个 ID,然后每个响应和通过 ID 来找到它对应的请求。各回各家,自然就不用阻塞的等待了。
再比如说 header 过大这个问题,怎么解决?
文本传输太占空间,换成二进制的是不是会好很多。
还有,每次传输都有很多相同的 header,能不能建立一张表,传的时候只传输下标就行了。
还有,body 可以压缩,那 header 是不是可以压缩。
这样处理之后,应该会好很多。
那没有充分利用 TCP 的能力,只支持请求--响应的方式呢?
那就支持服务端主动推送呀,但是客户端可以选择接收或者不接收。
上面是我们对这些问题的解决方案的思考,我们再来看看 HTTP2 是怎么解决这些问题的:
HTTP2 确实是通过 ID 把请求和响应关联起来了,它把这个概念叫做流 stream。
而且我们之前说了 header 需要单独的优化嘛,所以把 header 和 body 部分分开来传送,叫做不同的帧 frame。
每个帧都是这样的格式:
payload 部分是传输的内容这没啥可说的。
header 部分最开始是长度,然后是这个帧的类型,有这样几种类型:
- SETTINGS 帧:配置信息,比如最大帧 size,是否支持 server push 等。
- HEADERS 帧:请求或响应的 header
- DATA 帧:请求或响应的 body
- CONTINUATION 帧:一个帧不够装的时候,可以分帧,用这个可以引用上一个帧。
- PUSH_PROMISE 帧:服务端推送数据的帧
- END_STREAM 帧:表示流传输结束
- RST_STREAM 帧,用来终止当前流
这几种帧里面 HEADERS 和 DATA 帧没啥可说的。
SETTING 帧是配置信息,先告诉对方我这里支持什么,帧大小设置为多大等。
帧大小是有个上限的,如果帧太大了,可以分成多个,这时候帧类型就是 CONTINUATION(继续)。也很容易理解。
HTTP2 确实是支持服务端推送的,这时候帧类型也是单独的,叫做 PUSH_PROMISE。
流是用来传输请求响应或者服务端推送的,那传输完毕的时候就可以发送 END_STREAM 帧来表示传输完了,然后再传输 RST_STREAM 来结束当前流。
帧的类型讲完了,我们继续往后看,后面还有个 flags 标志位,这个在不同的帧类型里会放不同的内容:
比如 header 帧会在 flags 中设置优先级,这样高优先级的流就可以更早的被处理。
HTTP 1.1 的时候都是排队处理的,没什么优先级可言,而 HTTP 2.0 通过流的方式实现了请求的并发,那自然就可以控制优先级了。
后面还有个 R,这个现在还没啥用,是一个保留的位。
再后面的流标识符就是 stream id 了,关联同一个流的多个帧用的。
帧的格式讲完了,大家是不是有点晕晕的。确实,帧还是有很多种的。这些帧之间发送顺序也不同,不同的帧会在不同状态下发送,也会改变流的状态。
我们来看下流的状态机,也就是流收到什么帧会进入什么状态,并且在什么状态下会发送什么帧:
(看不明白可以先往后看)
刚开始,流是 idle 状态,也就是空闲。
收到或发送 HEADERS 帧以后会进入 open 状态。
oepn 状态下可以发送或接收多次 DATA 帧。
之后发送或接收 END_STREAM 帧进入 half_closed 状态。
half_closed 状态下收到或者发送 RST_STREAM 帧就关闭流。
这个流程很容易理解,就是先发送 HEADER,再发送 DATA,之后告诉对方结束,也就是 END_STREAM,然后关闭 RST_STREAM。
但是 HTTP2 还可以服务端推送呀,所以还有另一条状态转换流程。
流刚开始是 idle 状态。
接收到 PUSH_PROMISE 帧,也就是服务端推送过来的数据,变为 reserved 状态。
reserved 状态可以再发送或接收 header,之后进入 half_closed 状态。
后面的流程是一样的,也是 END_STREAM 和 RST_STREAM。
这个流程是 HTTP2 特有的,也就是先推送数据,再发送 headers,然后结束流。
这就是 http2 发送一次请求、响应,或者一次服务端推送的流程,都是封装在一个个流里面的。
流和流之间可以并发,还可以设置优先级,这样自然就没有了队头阻塞的问题,这个特性叫做多路复用。也就是复用同一个链接,建立起多条通路(流)的意思。
而且传输的 header 帧也是经过处理的,就像我们前面说的,会用二进制的方式表示,用做压缩,而且压缩算法是专门设计的,叫做 HPACK:
两端会维护一个索引表,通过下标来标识 header,这样传输量就少了不少:
首先,header 里其实不止有 header,还有一行 GET xxx/xxx 的请求行,和 200 xxx 的响应行,为了统一处理,就换成了 :host :path 等 header 来表示。
这样发送的时候只需要发送下标就行:
比如 :method: get 就只需要发送个 2: get。
这个编码也是根据频率高低来设置的,频率高的用小编码,这种方式叫做哈夫曼编码。
这样就实现了 header 的压缩。
至此, HTTP2.0 的主要特性就讲完了,也就是多路复用,服务端推送,头部压缩,二进制传输。
最主要的特性是多路复用,也就是流和帧,流在什么状态下发送什么帧。其他的特性是围绕这个来设计的。
回过头来看一下 HTTP1.1 的问题是否都得到了解决:
队头阻塞:通过流的来标识请求、响应,同一个流的分为多个帧来传输,多个流之间可以并发,不会相互阻塞。
header 太大:通过二进制的形式,加上 HPACK的压缩算法,使得 header 减小了很多。
没有充分利用 TCP 的特性:支持了服务端推送。
这样看来,HTTP2.0 确实解决了 HTTP 1.1 的问题。
看起来,HTTP 2.0 已经很完美了?
其实不是的,虽然 HTTP 层面没有了队头阻塞问题,多个请求响应可以并行处理。但是同一个流的多个帧还是有队头阻塞问题,以为你 TCP 层面会保证顺序处理,丢失了会重传,这就导致了上一个帧没收到的话,下一个帧是处理不了的。
这个问题是 TCP 的可靠传输的特性带来的,所以想彻底解决队头阻塞问题,只能把 HTTP 的底层传输协议换掉了。
这就是 HTTP3 做的事情了,它的传输层协议换成了 UDP。当然,现在 HTTP3 还不是很成熟,我们先重点关注 HTTP2 即可。
QUIC协议
无队头阻塞:QUIC的每个Stream都有自己独立的滑动窗口,不受其它Stream的影响,所以不会被其它Stream丢包问题的阻塞
更快的连接建立:用QUIC三次握手代替TCP三次握手,同时QUIC握手会携带TLS(并且是TLS1.3),所以整个握手时延是1RTT
连接迁移:TCP是用四元组来标记一条连接的,如果客户端切换网络,则连接需要重新建立,QUIC通过连接ID标记一条连接(连接ID、TLS密钥),可以无缝切换,无需重新建立连接
总结
1996 年发布 HTTP 1.0,1999 年 HTTP 1.1,2015 年 HTTP 2.0。
1.1 和 2 之间间隔了 16 年,确实改变了很多,但只是性能方面的。
1.1 的问题是第二个请求要等第一个响应之后才能发出,就算用了管道化,多个响应之间依然也会阻塞,这就是“队头阻塞”问题。
而且 header 部分太大了,还是纯文本的,可能比 body 部分传的都多。
针对 1.1 的队头阻塞问题,我们会做域名分片,针对 header 过大的问题,我们会减少请求次数,也就是打包分 chunk、资源内联、雪碧图、静态资源请求禁止 cookie 等优化策略。
HTTP 2.0 解决了 1.1 的这些问题,通过多路复用,也就是请求和响应在一个流里,通过同一个流 id 来关联多个帧的方式来传输数据。多个流可以并发。
我们看了帧的格式,有长度、类型、stream id、falgs 还有 payload 等部分。
帧的类型还是挺多的,有 HEADRS、DATA、SETTINGS、PUSH_PROMISE、END_STREAM、EST_STREAM、等。
这些帧类型之间也不是毫无关联的,流在不同的状态下会发送、接收不同的帧,而且发送、接收不同的帧也会进入不同的状态。
理解 HTTP2.0 的 stream 就要理解这样的一个状态流转流程。
此外,HTTP 2.0 通过单独设计的 HPACK 算法对 header 做了压缩,也支持服务端推送。而且内容是通过二进制传输的,解决了 HTTP 1.1 的问题。
但是 HTTP 2.0 的底层是 TCP,它的可靠传输的特性使得同一个流内的多个帧依然是顺序传输的,依然有队头阻塞问题。也是因为 HTP 3把底层协议换成 UDP。
虽然还是有一些问题,但 HTTP 2.0 已经基本上把 HTTP 1.1 的各方面性能不好的点都优化到了极致,是很有意义的一次版本升级。
2.不同版本介绍
队头阻塞
队头阻塞是指当顺序发送的请求序列中的一个请求因为某种原因被阻塞时,在后面排队的所有请求也一并被阻塞,会导致客户端迟迟收不到数据。
针对队头阻塞:
1.将同一页面的资源分散到不同域名下,提升连接上限。虽然能公用一个 TCP 管道,但是在一个管道中同一时刻只能处理一个请求,在当前的请求没有结束之前,其他的请求只能处于阻塞状态。
2.减少请求数量
3.内联一些资源:css、base64 图片等
4.合并小文件减少资源数
无状态特性
无状态是指协议对于连接状态没有记忆能力。纯净的 HTTP 是没有 cookie 等机制的,每一个连接都是一个新的连接。上一次请求验证了用户名密码,而下一次请求服务器并不知道它与上一条请求有何关联,换句话说就是掉登录态。
不安全性
传输内容没有加密,中途可能被篡改和劫持。
SPDY 协议
SPDY 是由 google 推行的改进版本的 HTTP1.1 (那时候还没有 HTTP2)。
特性:
- 多路复用 — 解决队头阻塞
- 头部压缩 — 解决巨大的 HTTP 头部
- 请求优先级 — 先获取重要数据
- 服务端推送 — 填补空缺
- 提高安全性
多路复用
SPDY 允许在一个连接上无限制并发流。因为请求在一个通道上,TCP 效率更高(参考 TCP 拥塞控制 中的慢启动)。更少的网络连接,发出更密集的包。
头部压缩
使用专门的 HPACK 算法,每次请求和响应只发送差异头部,一般可以达到 50%~90% 的高压缩率。
请求优先级
虽然无限的并发流解决了队头阻塞的问题,但如果带宽受限,客户端可能会因防止堵塞通道而阻止请求。在网络通道被非关键资源堵塞时,高优先级的请求会被优先处理。
服务端推送
服务端推送(ServerPush),可以让服务端主动把资源文件推送给客户端。当然客户端也有权利选择是否接收。
提高安全性
支持使用 HTTPS 进行加密传输。
HTTP2
HTTP2 基于 SPDY,专注于性能,最大的一个目标是在用户和网站间只用一个连接。
新增特性:
- 二进制分帧 - HTTP2 性能增强的核心
- 多路复用 - 解决串行的文件传输和连接数过多
二进制分帧
首先,HTTP2 没有改变 HTTP1 的语义,只是在应用层使用二进制分帧方式传输。因此,也引入了新的通信单位:帧、消息、流。
分帧有什么好处?服务器单位时间接收到的请求数变多,可以提高并发数。最重要的是,为多路复用提供了底层支持。
多路复用
一个域名对应一个连接,一个流代表了一个完整的请求-响应过程。帧是最小的数据单位,每个帧会标识出该帧属于哪个流,流也就是多个帧组成的数据流。多路复用,就是在一个 TCP 连接中可以存在多个流。演示
HTTP2 的缺陷
- TCP 以及 TCP+TLS 建立连接的延时
- TCP 的队头阻塞并没有彻底解决
- 多路复用导致服务器压力上升
- 多路复用容易 Timeout
建连延时
TCP 连接需要和服务器进行三次握手,即消耗完 1.5 个 RTT 之后才能进行数据传输。
TLS 连接有两个版本—— TLS1.2 和 TLS1.3,每个版本建立连接所花的时间不同,大致需要 1~2 个 RTT。
RTT(Round-Trip Time):
往返时延。表示从发送端发送数据开始,到发送端收到来自接收端的确认(接收端收到数据后便立即发送确认),总共经历的时延。
队头阻塞没有彻底解决
TCP 为了保证可靠传输,有一个“超时重传”机制,丢失的包必须等待重传确认。HTTP2 出现丢包时,整个 TCP 都要等待重传,那么就会阻塞该 TCP 连接中的所有请求。
RTO:英文全称是 Retransmission TimeOut,即重传超时时间; RTO 是一个动态值,会根据网络的改变而改变。RTO 是根据给定连接的往返时间 RTT 计算出来的。 接收方返回的 ack 是希望收到的下一组包的序列号。
多路复用导致服务器压力上升
多路复用没有限制同时请求数。请求的平均数量与往常相同,但实际会有许多请求的短暂爆发,导致瞬时 QPS 暴增。
多路复用容易 Timeout
大批量的请求同时发送,由于 HTTP2 连接内存在多个并行的流,而网络带宽和服务器资源有限,每个流的资源会被稀释,虽然它们开始时间相差更短,但却都可能超时。
即使是使用 Nginx 这样的负载均衡器,想正确进行节流也可能很棘手。 其次,就算你向应用程序引入或调整排队机制,但一次能处理的连接也是有限的。如果对请求进行排队,还要注意在响应超时后丢弃请求,以避免浪费不必要的资源。引用
QUIC
简介
Google
在推 SPDY 的时候就已经意识到了这些问题,于是就另起炉灶搞了一个基于 UDP 协议的 QUIC 协议。而这个就是 HTTP3。它真正“完美”地解决了“队头阻塞”问题。
主要特点
- 改进的拥塞控制、可靠传输
- 快速握手
- 集成了 TLS 1.3 加密
- 多路复用
- 连接迁移
改进的拥塞控制、可靠传输
从拥塞算法和可靠传输本身来看,QUIC 只是按照 TCP 协议重新实现了一遍,那么 QUIC 协议到底改进在哪些方面呢?主要有如下几点:
1. 可插拔 — 应用程序层面就能实现不同的拥塞控制算法。
一个应用程序的不同连接也能支持配置不同的拥塞控制。 应用程序不需要停机和升级就能实现拥塞控制的变更,可以针对不同业务,不同网络制式,甚至不同的 RTT,使用不同的拥塞控制算法。
关于应用层的可插拔拥塞控制模拟,可以对 socket 上的流为对象进行实验。
2. 单调递增的 Packet Number — 使用 Packet Number 代替了 TCP 的 seq。
每个 Packet Number 都严格递增,也就是说就算 Packet N 丢失了,重传的 Packet
N 的 Packet
Number 已经不是 N,而是一个比 N 大的值。而 TCP 重传策略存在二义性,比如客户端发送了一个请求,一个 RTO 后发起重传,而实际上服务器收到了第一次请求,并且响应已经在路上了,当客户端收到响应后,得出的 RTT 将会比真实 RTT 要小。当 Packet N 唯一之后,就可以计算出正确的 RTT。
3. 不允许 Reneging — 一个 Packet 只要被 Ack,就认为它一定被正确接收。
Reneging 的意思是,接收方有权把已经报给发送端 SACK(Selective Acknowledgment) 里的数据给丢了(如接收窗口不够而丢弃乱序的包)。
QUIC 中的 ACK 包含了与 TCP 中 SACK 等价的信息,但 QUIC 不允许任何(包括被确认接受的)数据包被丢弃。这样不仅可以简化发送端与接收端的实现难度,还可以减少发送端的内存压力。
4. 前向纠错(FEC)
早期的 QUIC 版本存在一个丢包恢复机制,但后来由于增加带宽消耗和效果一般而废弃。FEC 中,QUIC 数据帧的数据混合原始数据和冗余数据,来确保无论到达接收端的 n 次传输内容是什么,接收端都能够恢复所有 n 个原始数据包。FEC 的实质就是异或。示意图:
5. 更多的 Ack 块和增加 Ack Delay 时间。
QUIC 可以同时提供 256 个 Ack Block,因此在重排序时,QUIC 相对于 TCP(使用 SACK)更有弹性,这也使得在重排序或丢失出现时,QUIC 可以在网络上保留更多的在途字节。在丢包率比较高的网络下,可以提升网络的恢复速度,减少重传量。
TCP 的 Timestamp 选项存在一个问题:发送方在发送报文时设置发送时间戳,接收方在确认该报文段时把时间戳字段值复制到确认报文时间戳,但是没有计算接收端接收到包到发送 Ack 的时间。这个时间可以简称为 Ack Delay,会导致 RTT 计算误差。现在就是把这个东西加进去计算 RTT 了。
6. 基于 stream 和 connection 级别的流量控制。
为什么需要两类流量控制呢?主要是因为 QUIC 支持多路复用。 Stream 可以认为就是一条 HTTP 请求。 Connection 可以类比一条 TCP 连接。多路复用意味着在一条 Connetion 上会同时存在多条 Stream。
QUIC 接收者会通告每个流中最多想要接收到的数据的绝对字节偏移。随着数据在特定流中的发送,接收和传送,接收者发送 WINDOW_UPDATE 帧,该帧增加该流的通告偏移量限制,允许对端在该流上发送更多的数据。
除了每个流的流控制外,QUIC 还实现连接级的流控制,以限制 QUIC 接收者愿意为连接分配的总缓冲区。连接的流控制工作方式与流的流控制一样,但传送的字节和最大的接收偏移是所有流的总和。
最重要的是,我们可以在内存不足或者上游处理性能出现问题时,通过流量控制来限制传输速率,保障服务可用性。
快速握手
由于 QUIC 是基于 UDP 的,所以 QUIC 可以实现 0-RTT 或者 1-RTT 来建立连接,可以大大提升首次打开页面的速度。
集成了 TLS 1.3 加密
TLS 1.3 支持 3 种基本密钥交换模式:
(EC)DHE (基于有限域或椭圆曲线的 Diffie-Hellman)
PSK - only
PSK with (EC)DHE
在完全握手情况下,需要 1-RTT 建立连接。 TLS1.3 恢复会话可以直接发送加密后的应用数据,不需要额外的 TLS 握手,也就是 0-RTT。
TLS 1.3 0-RTT 简单原理示意(基于 DHE):
但是 TLS1.3 也并不完美。TLS 1.3 的 0-RTT 无法保证前向安全性(Forward secrecy)。简单讲就是,如果当攻击者通过某种手段获取到了 Session Ticket Key,那么该攻击者可以解密以前的加密数据。
要缓解该问题可以通过设置使得与 Session Ticket Key 相关的 DH 静态参数在短时间内过期(一般几个小时)。
多路复用
QUIC 是为多路复用从头设计的,携带个别流的的数据的包丢失时,通常只影响该流。QUIC 连接上的多个 stream 之间并没有依赖,也不会有底层协议限制。假如 stream2 丢了一个包,也只会影响 stream2 的处理。
连接迁移
TCP 是按照 4 要素(客户端 IP、端口, 服务器 IP、端口)确定一个连接的。而 QUIC 则是让客户端生成一个 Connection ID (64 位)来区别不同连接。只要 Connection ID 不变,连接就不需要重新建立,即便是客户端的网络发生变化。由于迁移客户端继续使用相同的会话密钥来加密和解密数据包,QUIC 还提供了迁移客户端的自动加密验证。
挑战
NAT 问题
NAT 概念
为了解决 IP 地址不足的问题,NAT 给一个局域网络只分配一个 IP 地址,这个网络内的主机,则分配私有地址,这些私有地址对外是不可见的,他们对外的通信都要借助那个唯一分配的 IP 地址。所有离开本地网络去往 Internet 的数据报的源 IP 地址需替换为相同的 NAT,区别仅在于端口号不同。
原因
TCP 和 UDP 的报文头部不同导致 NAT 问题的出现。
NAT 设备的端口记忆问题
对于基于 TCP 的 HTTP、HTTPS 传输,NAT 设备可以根据 TCP 报文头的 SYN/FIN 状态位,知道通信什么时候开始,什么时候结束,对应记忆 NAT 映射的开始和结束。
但是基于 UDP 传输的 HTTP3 ,不存在 SYN/FIN 状态位。NAT 设备的记忆如果短于用户会话时间,则用户会话会中断。NAT 设备的记忆时间如果长于用户会话时间,则意味着 NAT 设备的端口资源会被白白占用。
最直接的解决方案是,在 QUIC 的头部模仿 TCP 的 SYN/FIN 状态,让沿途的 NAT 设备知道会话什么时候开始、什么时候结束。但这需要升级全球所有的 NAT 设备的软件。
另外一个可行的方案是,让 QUIC 周期性地发送 Keepalive 消息,刷新 NAT 设备的记忆,避免 NAT 设备自动释放。
NAT 设备禁用 UDP
在一些 NAT 网络环境下(如某些校园网),UDP 协议会被路由器等中间网络设备禁止,这时客户端会直接降级,选择 HTTPS 等备选通道,保证正常业务请求。
NGINX 负载均衡问题
概念
QUIC 客户端存在网络制式切换,就算是同一个移动机房,可能第一次业务请求时会落到 A 这台服务器,后续再次连接,就会落到 B 实例上,重复走 1-RTT 的完整握手流程。
全局握手缓存
为所有 QUIC 服务器实例建立一个全局握手缓存。当用户网络发生切换时,下一次的业务请求无论是落到哪一个机房或哪一台实例上,握手建连都会是 0-RTT。
历代 HTTP 速度测试
结尾
从古至今实时数据传输(音频、视频、游戏等)都面临卡顿、延迟等问题,而 QUIC 基于 UDP 的架构和改进的重传等特性,能够有效的提升用户体验。目前
B 站 也已经接入 QUIC。
如果想要自己体验 QUIC,可以使用 Libquic、Caddy 等。另外 github 上面也有 C++版本的 QUIC 实现,利用 Nodejs 的 C++ 模块,前端工程师也可以快速实现一个 node-quic。
3.理解优缺点
HTTP1.1
HTTP1.1相对于HTTP1.0的优化
请求使用长连接代替短链接,不必每次请求都进行三次握手建立TCP连接,减少性能损耗(创建销毁进程)同时加快响应速度(没有慢启动、握手时延)
支持并发传输,不用等前一个请求收到响应,就可以发送请求,最大请求数据量可以到发送窗口的上限
HTTP1.1有什么不足?
请求头部未经压缩数据量大:影响传输延时
请求头部字段冗余:每次请求浪费较多
HTTP队头阻塞:服务端按照请求的顺序响应,如果服务端响应慢,可能有HTTP队头阻塞
单向请求:只有由客户端发送请求,服务端响应请求
HTTP2
HTTP2相对于HTTP1.1的优化
头部压缩:如果同时发送多个请求,通信双方有一张头信息表,只需要发送索引,可以找到对应的头部字段
二进制数据:头部和数据部分都换成二进制存储,减少传输的大小,同时对计算机友好,不用进行转换,提高解析效率
并发传输:HTTP1.1没有对请求或者响应进行区分,所以只能要求服务端顺序响应,否则服务端响应的数据客户端无法组装(会组装不同请求的响应数据到一起),在HTTP2将每个HTTP请求或者响应封装成一个Stream,每个Stream有自己的Stream ID(对应某个请求或者响应),用于数据组装
双向传输:服务端可以主动推送资源,也可以创建Stream ID,但是必须是双数,而客户端必须是单数
HTTP/2有什么不足?
TCP队头阻塞:HTTP2是基于TCP协议的,而TCP是基于字节流的,TCP只有收到完整的字节数据,内核才会把缓冲区的数据传给对应的应用层,当某个HTTP请求没有收到完整的响应,即发生了丢包,即使后面的Stream已经是完整的也没办法接收,要等到本Stream接收到重传的报文段才能顺利接收
HTTP3
HTTP3相对于HTTP2的优化
HTTP/3基于UDP协议实现:UDP是基于报文段的,没有队头阻塞问题,同时在应用层实现QUIC协议保证数据可靠到达
QUIC协议
无队头阻塞:QUIC的每个Stream都有自己独立的滑动窗口,不受其它Stream的影响,所以不会被其它Stream丢包问题的阻塞
更快的连接建立:用QUIC三次握手代替TCP三次握手,同时QUIC握手会携带TLS(并且是TLS1.3),所以整个握手时延是1RTT
连接迁移:TCP是用四元组来标记一条连接的,如果客户端切换网络,则连接需要重新建立,QUIC通过连接ID标记一条连接(连接ID、TLS密钥),可以无缝切换,无需重新建立连接