前言

随着移动互联网的发展,我们可以随时随地在更多的地方享受到互联网带来的便利,但由于各个地方的信号强度并不相同,在一些网络条件不好的地方,也就是弱网的地方,丢包会很严重,在以TCP作为传输层协议的网络传输中,由于丢包导致的队头阻塞,会使传输性能受到很大的影响。

为了提升传输性能,2012年Google设计出QUIC协议。QUIC全称Quick UDP Internet Connection(快速UDP互联网连接),是一个基于UDP的传输协议,在2018年的时候IETF的HTTP工作组和QUIC工作组共同决定将QUIC在HTTP上映射为HTTP/3,也就是既HTTP 0.9、1.0、1.1、2之后,HTTP/3出现了。

在聊QUIC之前,我们先来看看HTTP的发展史,这样能够更好地理解QUIC出现的缘由。

HTTP发展史

HTTP/0.9

HTTP是基于TCP/IP协议(HTTP/3之前基于TCP)的应用层协议,它不涉及数据包的传输(传输由TCP及以下的协议负责),主要规定了客户端和服务端的通信格式。
最早版本是1991年发布的0.9版本,该版本极其简单,只支持GET命令。

HTTP/1.0

1996年5月,HTTP/1.0版本发布,功能完善了许多:

  • 支持丰富的格式,不仅支持文字,还能传输图像、视频、二进制文件等,为互联网的大发展奠定了基础
  • 除GET外,引入了POST、HEAD命令,丰富了浏览器与服务器的互动手段
  • 引入状态码、缓存等功能

HTTP/1.0的主要缺点是,每个TCP连接只能发送一个请求,连接无法复用。

HTTP/1.1

1997年1月,HTTP/1.1版本发布,进一步完善了HTTP协议:

  • 支持持久连接,默认不关闭,可以被多个请求复用
  • 引入管道机制,在同一个TCP连接里,客户端可以同时发送多个请求,但接收的时候是按发送顺序接收
  • 支持分块传输,服务器发送回应不需要等待请求全部完成,可以将回应的数据分块传输,产生一块发送一块

虽然1.1版本支持复用TCP连接,但是同一个TCP连接里面,所有的数据通信是按次序进行的。服务器只有处理完一个回应,才会进行下一个回应。要是前面的回应特别慢,后面就会有许多请求排队等着。

HTTP/2

2009年,Google公开了自研的SPDY协议,主要解决HTTP/1.1效率不高的问题,后来作为HTTP/2的基础。

2015年,HTTP/2发布,特性如下:

  • 采用二进制协议,头信息和数据题都是二进制,为将来的高级应用打好了基础,如果是之前的HTTP版本,头都是采用文本(ASCII编码)传输,解析起来就变得麻烦,二进制解析则方便很多
  • 支持多路复用,在一个连接里,客户端和服务端可以同时处理多个发送请求和响应,不用按照顺序一一对应,解决HTTP/1.1的阻塞问题。
  • 头信息压缩,减少请求头部的冗余数据,降低开销
  • 服务端可以主动推送,允许服务端主动向客户端发送资源,这样就可以减少一点延迟时间。常见场景是客户端请求一个网页,这个网页里面包含很多静态资源。服务器可以预期到客户端请求网页后,很可能会再请求静态资源,所以就主动把这些静态资源随着网页一起发给客户端了

HTTP2中,多个请求在一个TCP管道中,出现丢包时,由于TCP要保证可靠传输,丢失的包必须要等待重新传输确认,这样会导致整个TCP连接都要开始等待重传,这样会阻塞对已经达到的请求处理。

HTTP/3

HTTP/3是即将到来的第三个主要版本的HTTP协议,截止至2021年3月,它还处于草案阶段。

HTTP/3使用QUIC协议实现,QUIC主要为了解决HTTP/2中存在的TCP丢包导致阻塞的问题而诞生。下面我们来看看QUIC的优势所在。

QUIC优势

无队头阻塞

TCP的队头阻塞

我们先来看什么是队头阻塞,队头阻塞是TCP为了追求可靠引入的。TCP使用序列号来标识数据的顺序,数据必须按照顺序处理,也就是说前面的数据丢失,如发生丢包,后面的数据就算达到了也不会通知应用层来处理。
而对于HTTP2,虽然实现了多路复用,但由于是基于TCP,一旦丢包,还是会影响多路复用下的所有请求流。
image.png
图1 TCP丢包时的队头阻塞

在弱网环境下,TCP的队头阻塞问题会让用户在体验上非常糟糕

QUIC无队头阻塞实现原理

QUIC基于UDP,UDP本身不保证可靠,所以不存在队头阻塞的问题,而QUIC可以将可靠性放在了应用层去实现。
之所以不在传输层去优化TCP,是由于TCP是在内核层实现的,如果要升级TCP,就必须依赖操作系统的升级,但操作系统的升级很麻烦,如Windows XP还有大量用户在使用,所以即使TCP有比较好的特性,也很难快速推广。
image.jpeg
图2 QUIC无队头阻塞
QUIC的传输单位是packet,packet number是单调递增,如果Packet N丢失了,重传的时候是Packet N+M,这样就不需要像TCP那样有序确认,而可以乱序确认。当数据包Packet N丢失后,只要有新的已接收数据包确认,当前窗口会继续向右滑动。待发送端获知数据包Packet N丢失后,会将需要重传的数据包放到待发送队列,重新编号为Packet N+M后发送到接收端。通过这样的机制,窗口就不会被阻塞在原地,从而解决了队头阻塞的问题。
那怎么确定两个包一样呢?QUIC使用stream ID标识当前数据流属于哪个资源请求,另外还有会Stream Offset来标识当前数据包在当前Stream ID中的字节偏移量。有了Stream Offset后,只要两个数据包的Stream ID与Stream Offset都一致,就说明这两个数据包的内容一致。

实现连接迁移

TCP的连接重连问题

一条 TCP 连接是由四元组标识的(源 IP,源端口,目的 IP,目的端口)。什么叫连接迁移呢?就是当其中任何一个元素发生变化时,这条连接依然维持着,能够保持业务逻辑不中断。当然这里面主要关注的是客户端的变化,因为客户端不可控并且网络环境经常发生变化,而服务端的 IP 和端口一般都是固定的。

比如大家使用手机在 WIFI 和 4G 移动网络切换时,客户端的 IP 肯定会发生变化,这时就需要重新建立和服务端的 TCP 连接。又比如大家使用公共 NAT 出口时,有些连接竞争时需要重新绑定端口,导致客户端的端口发生变化,同样需要重新建立 TCP 连接。

QUIC连接迁移

那 QUIC 是如何做到连接迁移呢?很简单,任何一条 QUIC 连接不再以 IP 及端口四元组标识,而是以一个 64 位的随机数作为 ID 来标识,这样就算 IP 或者端口发生变化时,只要 ID 不变,这条连接依然维持着,上层业务逻辑感知不到变化,不会中断,也就不需要重连。
由于这个 ID 是客户端随机产生的,并且长度有 64 位,所以冲突概率非常低。
image.png
图3 TCP和QUIC在WiFi和Cellular切换时,连接唯一标识的变化区别

改进的拥塞控制

TCP的拥塞控制算法Cubic包含了四个算法:慢启动,拥塞避免,快速重传,快速恢复。

QUIC协议当前默认使用了TCP协议的拥塞控制算法,但使用哪种拥塞控制算法是可插拔的,也就是能够非常灵活地修改拥塞控制算法,可以支持修改成如BBR等拥塞控制算法。这意味着我们可以根据不同的业务场景,实现和配置不同的拥塞控制算法。如Google提出的BBR与Cubic是思路完全不同的算法,在弱网和一定丢包场景下,BBR比Cubic更不敏感,性能也更好。

QUIC对于拥塞控制算法的使用变更,不需要操作系统、内核支持。由于操作系统、内核的部署成本非常高,升级周期很长,在产品快速迭代的今天,QUIC的可拔插式更能满足需求。

QUIC提供了更丰富的拥塞控制信息,比如不管是原始包还是重传包,都有一个新的序列化(packet number),这使得QUIC能够区分ACK是重传包还是原始包,从而避免了TCP重传模糊的问题
image.png
图4 TCP重传模糊
如图4,经过RTO(Retransmission TimeOut 超时重传时间)后,客户端发起了重传,然后收到了ACK数据。由于序列号一样,这个ACK到底是原始请求还是重传请求呢?不好判断
如果算成原始请求的相应,但实际上是重传请求的响应(图左),会导致采样的RTT(Round-trip time 来回时间)变大。如果算成重传请求的响应,但实际上是原始请求的响应,又很容易导致采样的RTT过小。

由于QUIC重传的Packet和原始的Packet Number是严格递增的,就很容易解决这个问题
image.png
图5 QUIC重传没有歧义性
如图5,重传事件发生后,根据重传的Packet Number就能确定精确的RTT。RTT的精确,就能让拥塞控制更合理,从而提升整体的吞吐量。

此外,QUIC的ACK Frame支持256个NACK区间,相比TCP的SACK(Selective Acknowledgment)提供的3个NACK区间更弹性,在丢包率比较高的网络下,更多的NACK区间可以提升网络的恢复速度,减少重传量。

0RTT握手延时

HTTPS的连接延时问题

以一次简单的HTTPS请求(https://www.yi.com)为例,为获取请求资源,需要经过以下4个步骤

  1. DNS查询www.yi.com获取IP
  2. TCP握手,我们熟悉的三次握手需要1个RTT(通过捎带可以减少掉第三次握手)
  3. TLS握手,以目前应用最广泛的TLS 1.2而言,需要2个RTT。对于非首次建立连接,可以选择启用会话重用(Session Resumption),则可以将握手时间缩小到1个RTT
  4. HTTP业务数据交互,假设www.yi.com的数据在一次交互就能取回来,那么业务数据的交互需要1个RTT

经过上面的过程分析可知,要完成一次简短的HTTPS业务数据交互,新连接需要 4RTT + DNS,会话重用 3RTT + DNS。不算第四步,只考虑建立连接的情况,新连接需要 3RTT + DNS,会话重用需要 2RTT + DNS。
所以对于数据量小的请求而言,单一次的请求握手就占用了大量的时间,如果网络不好,RTT延时大的话,将极大影响用户体验。

image.png
图6 TLS各版本与场景下的握手耗时
以上可以看到,即使用上了TLS 1.3,精简了握手,也是需要1RTT握手,最快能做到0RTT握手(非首次),但还需要再加上1RTT的TCP握手开销,因此至少需要1RTT。

Google有提出Fastopen的方案来使得TCP非首次握手就能附带用户数据, 但是由于TCP实现僵化, 无法升级应用, 相关RFC(Request for Comments 请求意见稿)到现今都是experimental状态。这种分层设计带来的延时,有没有办法进一步降低呢? QUIC通过合并连接与加密管理解决了这个问题,我们来看看其是如何实现真正意义上的0-RTT的握手, 让与server进行第一个数据包的交互就能带上用户数据。

0RTT的QUIC握手

QUIC由于基于UDP,没有TCP的建立连接,在非首次连接情况下,最好可以做到0RTT建立连接开启数据传输。

对于TCP+TLS之所以至少需要1RTT,究其原因是TCP和TLS分层设计导致,分层的设计需要每个逻辑层次分别建立自己的连接状态,另一方面是TLS的握手阶段复杂的密钥协商机制导致的。

而QUIC是合并了连接和加密管理。QUIC的握手使用了DH密钥协商算法来协商一个对称密钥,简单来说需要通信双方各自生成自己的非对称公私钥对,双方各自保留自己的私钥,将共钥发送给对方,利用对方的共钥和自己的私钥可以运算出同一个对称密钥。
在非首次会话下,可以利用提前缓存好的服务端证书等配置信息,既可直接加密应用数据传输给服务端,这样就能够实现0RTT的握手延时。

image.png
图7 QUIC 0RTT握手

总结

本文从HTTP的发展历史出发,阐述了QUIC协议出现的缘由。QUIC基于UDP,在保证像TCP同样安全与可靠的同时,解决了TCP中的一些缺点,提升了弱网下的传输性能,大大提升了用户的使用体验。

参考链接