整理一下最近学到的一个有趣的东西
一 前言
1 QUIC简介
QUIC(Quick UDP Internet Connection)是谷歌推出的一套基于UDP的传输协议,它实现了TCP + HTTPS + HTTP/2的功能,目的是保证可靠性的同时降低网络延迟。因为UDP是一个简单传输协议,基于UDP可以摆脱TCP传输确认、重传慢启动等因素,建立安全连接只需要一的个往返时间,它还实现了HTTP/2多路复用、头部压缩等功能
首先UDP比TCP传输速度快,TCP是可靠协议,但是代价是双方确认数据而衍生的一系列消耗。其次TCP是系统内核实现的,如果升级TCP协议,就得让用户升级系统,这个的门槛比较高,而QUIC在UDP基础上由客户端自由发挥,只要有服务器能对接就可以
2 HTTP协议发展
2.1 HTTP1.0到HTTP1.1
区别
- HTTP 1.0:仅支持保持短暂的TCP连接(连接无法复用);不支持断点续传;前一个请求响应到达之后下一个请求才能发送,存在队头阻塞
- HTTP 1.1:默认支持长连接(请求可复用TCP连接);支持断点续传(通过在 Header 设置参数);优化了缓存控制策略;管道化,可以一次发送多个请求,但是响应仍是顺序返回,仍然无法解决队头阻塞的问题;新增错误状态码通知;请求消息和响应消息都支持Host头域
局限
- 队头阻塞:下个请求必须在前一个请求返回后才能发出,导致带宽无法被充分利用,后续请求被阻塞(HTTP 1.1 尝试使用流水线(Pipelining)技术,但先天 FIFO(先进先出)机制导致当前请求的执行依赖于上一个请求执行的完成,容易引起队头阻塞,并没有从根本上解决问题)
- 协议开销大:header里携带的内容过大,且不能压缩,增加了传输的成本
- 单向请求:只能单向请求,客户端请求什么,服务器返回什么
2.2 HTTP1.1到HTTP2.0
优化
HTTP2.0的优化很多,大致包括二进制传输、多路复用、Header压缩、服务端推送和流优先级等方面,具体可以参考上篇转发://http2.0多路复用 | Hexo (eslody.github.io),在这里就不详述了
HTTP2.0成功解决了队头阻塞的问题,但仍存在一些缺陷
缺陷
假设多个请求在一个TCP双向流中的,出现了丢包时,HTTP2.0的性能甚至不如HTTP1.1。因为,TCP为了保证可靠传输,有个特别的“丢包重传”机制,丢失的包必须要等待重新传输确认,HTTP2.0出现丢包时,整个 TCP 都要开始等待重传,那么就会阻塞该TCP连接中的所有请求。而对于HTTP1.1来说,可以开启多个 TCP 连接,出现这种情况反到只会影响其中一个连接,剩余的 TCP 连接还可以正常传输数据
2.3 基于QUIC的HTTP3
HTTP2.0的问题根源上在传输层TCP的机制上(慢启动、确认重传等),所以要么修改TCP,要么更换传输层协议,HTTP3采用基于UDP的QUIC协议,成功解决了HTTP2的TCP问题
二 QUIC的特性浅析
1 连接迁移
1.1 TCP的连接重连
一条TCP连接是由四元组(源 IP,源端口,目的 IP,目的端口)标识的。什么叫连接迁移呢?就是当其中任何一个元素发生变化时,这条连接依然维持着,能够保持业务逻辑不中断。当然这里面主要关注的是客户端的变化,因为客户端不可控并且网络环境经常发生变化,而服务端的IP和端口一般都是固定的
比如使用手机在WIFI和4G移动网络切换时,客户端的IP肯定会发生变化,需要重新建立和服务端的TCP连接;或者使用公共NAT出口时,有些连接竞争时需要重新绑定端口,导致客户端的端口发生变化,同样需要重新建立TCP连接
而从TCP连接的角度来讲,这个问题无解
1.2 QUIC的连接迁移
如上所述,TCP采用IP+端口的形式标识客户端。而QUIC则基于连接ID进行唯一标识,所以当源地址改变时,QUIC仍然可以保证维持连接
这个所谓的连接ID,其实就是一个64位由客户端随机产生的随机数。当IP或者端口发生变化时,只要ID不变,这条连接依然维持着,上层业务逻辑感知不到变化,不会中断,也就不需要重连
2 低连接时延
2.1 TLS的连接时延
以一次简单的浏览器访问为例,在地址栏中输入https://www.xxx.com,实际会产生以下动作:
- DNS递归查询www.xxx.com,解析域名对应的IP
- TCP三次握手,需要一个RTT(往返时延)
- TLS四次握手,需要两个RTT(证书验证+密钥生成)。对于非首次建连,可以选择启用会话重用,则可缩小握手时间到1个RTT
- 业务数据交互,这个往返时延取决于业务需求
综上,要完成一次简短的HTTPS业务数据交互,即使没有证书验证的阶段,至少也要经过3RTT+DNS(包括业务交互的一次RTT),在网络状况不佳的情况下会极其影响用户体验
2.2 QUIC的0-RTT握手
QUIC由于基于UDP,在最好情况下,短连接QUIC可以做到0-RTT开启数据传输。而基于TCP的 HTTPS,即使在最好的TLS1.3的early data下仍然需要1RTT开启数据传输。而对于目前线上常见的TLS1.2完全握手的情况,则需要3RTT开启数据传输。对于 RTT 敏感的业务,QUIC 可以有效的降低连接建立延迟
究其原因一方面是TCP和TLS分层设计导致的:分层的设计需要每个逻辑层次分别建立自己的连接状态。另一方面是TLS的握手阶段复杂的密钥协商机制导致的。要降低建立连接的时耗,需要从这两方面着手
QUIC将生成密钥和数据传输阶段整合在一次握手中,实现了0-RTT握手
3 可插拔的拥塞控制
QUIC使用可插拔的拥塞控制,相较于TCP,它能提供更丰富的拥塞控制信息。比如对于每一个包,不管是原始包还是重传包,都带有一个新的序列号(seq),这使得QUIC能够区分ACK是重传包还是原始包,从而避免了TCP重传模糊的问题。QUIC同时还带有收到数据包与发出ACK之间的时延信息。这些信息能够帮助更精确的计算RTT。此外,QUIC的ACK Frame支持256个NACK区间,相比于TCP的SACK(Selective Acknowledgment)更弹性化
同时,QUIC的传输控制不再依赖内核的拥塞控制算法,而是实现在应用层上,这意味着我们根据不同的业务场景,实现和配置不同的拥塞控制算法以及参数
4 队头阻塞优化
4.1 TCP的队头阻塞
虽然HTTP2实现了多路复用,但是因为其基于面向字节流的 TCP,一旦丢包将会影响多路复用下的所有请求流。QUIC基于UDP,在设计上就解决了队头阻塞问题
TCP队头阻塞的主要原因是数据包超时确认或丢失阻塞了当前窗口向右滑动,我们最容易想到的解决队头阻塞的方案是不让超时确认或丢失的数据包将当前窗口阻塞在原地。TCP为了保证可靠性,使用了基于字节序号的Seq及Ack来确认消息的有序到达
如图,应用层可以顺利读取stream1中的内容,但由于stream2中的第三个segment发生了丢包,TCP 为了保证数据的可靠性,需要发送端重传第 3 个segment才能通知应用层读取接下去的数据。所以即使stream3、stream4的内容已顺利抵达,应用层仍然无法读取,只能等待stream2中丢失的包进行重传
4.2 QUIC的无队头阻塞
QUIC使用Packet Number来代替TCP中的Seq,并且每个Packet Number都严格递增,也就是说就算Pac N丢失了,重传的Pact N的Packet Number已经不是N,而是一个比N大的值,比如Pac N+M
这样,QUIC就不必有序确认,而是可以乱序确认,就不会因为丢包重传将当前窗口阻塞在原地。同时,QUIC使用Stream ID来标识当前数据流属于哪个资源请求,这同时也是数据包多路复用传输到接收端后能正常组装的依据
5 协议加密
TCP协议头部没有经过任何加密和认证,所以在传输过程中很容易被中间网络设备篡改,注入和窃听。比如修改序列号、滑动窗口。这些行为有可能是出于性能优化,也有可能是主动攻击
但是QUIC的packet可以说是武装到了牙齿。除了个别报文,所有报文头部都是经过认证的,报文Body都是经过加密的
这样只要对QUIC报文任何修改,接收端都能够及时发现,有效地降低了安全风险
参考
Web技术(六):QUIC 是如何解决TCP 性能瓶颈的?_流云-CSDN博客
科普:QUIC协议原理分析 - 知乎 (zhihu.com)