HTTP/2 是目前最新的网络传输协议(如上图左),主要由 TCP + TLS 1.2 + HTTP 所组成。随着时间的演进,越来越多的网络流量都往移动端移动,手机的无线网络环境会遇到的问题像是 (1) 丢包率较高、(2) 较长的往返时间(RTT)和 (3) 连接迁移问题等等,都让主要是为了有线网络设计的 HTTP/TCP 协议遇到瓶颈。
因此 Google 在 2013 年发表了一个新的传输协议 QUIC(如上图右),全名为 Quick UDP Internet Connection。不同于 HTTP/2,QUIC 采用的是较不可靠的 UDP 作为传输层,再另外在 QUIC 层上实现丢包恢复和拥塞控制,并引入新的设计以支持多路复用、降低连接握手的延迟、解决重传歧义和支持连接迁移等等。之所以不直接修改 TCP 协议的原因,主要是因为 TCP 和 UDP 大部分都是在操作系统的核心实现,无法快速的升级并广泛地被采用,所以才直接在目前系统都已经支持的 UDP 的上层动手脚。
IETF 的 QUIC 工作组在 2018 年把 QUIC 重新命名为 HTTP/3,准备把 QUIC 确立为下一代传输协议的标准。其中 IETF 对 QUIC 做了一些改动,像是将 QUIC 改成较通用的传输协议,除了支持 HTTP,也支持 SMTP、DNS 和 SSH 等等。也将原本 QUIC 里面的 QUIC Crypto 加密机制改用 TLS 1.3 取代。
在 Google 关于 QUIC 的文档中提到,与 HTTP/2 相比,QUIC 主要具有下列优势:
下面将对 QUIC 的下列五个机制分别做介绍:
原本的 TCP/IP 协议在每次连接时都会进行有名的三次握手(Three-Way Handshake),总共会耗时 1.5 个 RTT。如果再加上 TLS 的传输时间,整个连接的建立每次都需要花上 3 个 RTT 的时间,如上图左。在过去传输速度较慢的时代,光数据传输所花的时间就非常长,建立连接所花时间的占比就显得微不足道。但是在传输速度越来越快的现在,每次连接中,传输数据所花的时间也变得越来越短,使得连接时握手所花的时间,对传输效能的影响占比也越来越大。
QUIC 因此提出一个新的连接建立机制,初始的连接握手和密钥的交换,只需要花 1 个 RTT 的时间,如上图右和下图左。通过将初始连接握手的密钥缓存在客户端,从第二次连接开始,之后的每一次连接都可以直接开始传输数据,如下图中,达到零握手延迟(0-RTT Handshake Latency)的优势,直接在一个经过认证且加密的通道内传输数据。
QUIC 连接的建立主要分为两个步骤:(1) 初始握手和 (2) 最终与重复握手,下面分别对这两个步骤做详细的介绍。
在连接初始化阶段,客户端首先发送一个“Client Hello”(CHLO)消息至服务器,以此启动握手流程。随后,服务器会回应一个REJ(Reject)数据包,该REJ数据包封装了四项关键信息:
在客户端收到 REJ 后,依照 QUIC 的定义,双方就已经完成了握手,可以开始安全地传输数据。这是为什麼呢?让我们看看从 REJ 中客户端得到了哪些信息:
有了初始密钥后(暂时性的密钥),客户端就可以用这把密钥对想要传输的数据(request)进行加密,安全地传送给服务器。除了对数据进行加密外,客户端也必须将自己刚产生的短期 DH 密钥所对应的公钥,放入一个叫做 Complete CHLO 的包中,和 request 数据一并用初始密钥加密后回传给服务器,如上图左。因此,拿到 Complete CHLO 包的服务器,就同时拥有了客户端的短期 DH 公钥和自己保存的长期 DH 密钥,服务器便可以同样通过运算,拿到与客户端一模一样的那一把初始密钥,用来对数据进行加密与解密。
如果一切都顺利,服务器便会产生一个叫做 SHLO (Server Hello) 的包,包中包含了服务器新产生的短期 DH 公钥。之后,服务器便会将要求回传的 response 数据和产生的 SHLO 包,都用初始密钥进行加密,再一并回传给客户端,如上图左。此时,客户端与服务器已经使用初始密钥进行了一次数据交换,并且在初始密钥的加密保护下,交换了彼此的短期 DH 公钥。这边要特别提到两边的短期 DH 公钥和密钥都是专门为这次的连接新产生的,每一次建立连接都会产生新的短期 DH 公钥和密钥。接下来要进行的,就是更换初始密钥。
初始密钥毕竟是基於服务器的长期 DH 公钥所产生的,在公钥失效前,几乎所有的连接使用的都是同一把公钥,具有一定程度被 compromise 的可能。为了达到前向保密(Forward Secrecy)的安全性,客户端与服务器便会再用彼此交换而来的短期 DH 公钥,与自己保存的短期 DH 密钥做运算,产生一个仅限于这次连接使用的前向保密密钥(Forward-Secure Key),后续数据的加密和解密,就都会改用这把新的密钥,达到前向保密安全性。这样就完成了最终密钥的交换、连接的握手和 QUIC 连接的建立。
当下一次要重复建立连接时,客户端会使用自己之前 cache 的服务器长期公钥,加上自己新择定的短期密钥,重新产生一把与之前不同的初始密钥,直接在初始密钥的保护下传送 request 给服务器,达到零握手延迟连接(0-RTT Handshake Latency),如上图中。当服务器的长期公钥失效时,服务器会重新回传一个新的 REJ 数据包,重新与客户端进行握手,总共一样只会花费 1 个 RTT 的时间,如上图右。
当 TCP 连接传输的一个包丢失时,在发送端主动发现并且重新发送前,整个连接的传输都会被卡住,这就是 TCP 的队头阻塞(Head of Line Blocking)问题。
因为 QUIC 支持在同一个连接中进行多个 Stream 的数据传输,所以当某一个 Stream 中的包丢失时,只有这一个 Stream 的传输会受到影呴,其他 Stream 可以完全不受影呴的继续进行数据传输,避免 HOL Blocking。QUIC 的 Stream Multiplexing 具有以下几点特性:
过去TCP在数据包丢失恢复策略所用的做法是,在发送端为每一个数据包标记一个编号(sequence number),接收端在收到数据包时,就会回传一个带有对应编号的ACK数据包给发送端,告知发送端数据包已经确实收到。当发送端在超过一定时间(Retransmit Timeout, RTO)之后还没有收到回传的ACK,就会认为数据包已经丢失,启动重新发送的机制,复制与原来相同的编号重新发送一次数据包,确保在接收端这边没有任何数据包遗漏。
这样的机制会有什么弊端呢?假设发送端总共对同一个数据包发送了两次(初始+重传),使用的都是同一个sequence number:编号N。之后发送端在拿到编号N数据包的回传ACK时,将无法判断这个带有编号N的ACK,是接收端在收到初始数据包后回传的ACK(较长RTT),还是接收端在收到重传数据包后回传的ACK(较短RTT),这就是TCP重传歧义问题(TCP retransmission ambiguity problem)。ACK是属于初始数据包还是重传数据包如果判断错误,会造成TCP算法对连接通道内实际RTT(Round Trip Time)采样和预测的误差,影响后续拥塞控制(congestion control)算法的判断。RTT如果被不真实的放大,RTO就会随着RTT的增加呈现指数型增长,严重拉长数据包重传的反应时间。
QUIC为了避免重传歧义问题,发送端在传送数据包时,初始与重传的每一个数据包都改用一个新的编号,unique packet number,每一个编号都唯一且严格递增,这样每次在收到ACK时,就可以依据编号明确的判断这个ACK是来自初始数据包或者是重传数据包。接收端则是借由数据包内的Stream ID和Stream Offset的值,辨认数据包是属于哪一个Stream,再依照每个数据包的Offset将数据照顺序重组。简单来说就是将原本由sequence number一手包办的数据包传输顺序和数据包数据位置(offset)的信息,拆分成由unique packet number和Stream Offset两个数字分别记录,如此就同时解决了重传歧义和数据包重组的问题,大幅提升RTT的采样与预测的准确性,尽可能的降低数据包重传的反应时间。
流量控制 (Flow Control)
通过流量控制可以限制客户端传输数据量的大小,有了流量控制后,接收端就可以只保留相对应大小的接收buffer,优化内存被占用的空间。但是如果存在一个流量极慢的Stream,光一个Stream就有可能占用掉接收端所有的buffer。QUIC为了避免这个潜在的HOL Blocking,采用了连接层(connection flow control)和Stream层的(stream flow control)流量控制,限制单一Stream可以占用的最大buffer size。
接收端可以根据自己的需求设置Stream的max flow control receive window(max receive window),max receive window越大,接收端所需要保留的buffer size也就越大,接收端可以根据自己的内存的大小和服务的繁忙程度做出适当的设置。新建立的Stream,如图,因为还没有传送任何的数据,receive window的大小会与max receive window的大小一致。Flow Control限制发送端只能发送offset介于receive window内的数据,也就是只能传送offset从0到flow control receive offset(receive offset)之间的数据,确保发送端不会传送大于接收端buffer所能承受的数据量。
在发送端传送了一些数据后,如图,我们可以看到receive window的大小随着highest received byte offset的右移而逐渐缩小。接收端在这样的情况,只有可能接收到offset位于receive window中或是在gaps区域内的数据。造成gaps的原因可能有两个,一个是因传输速率的不同,造成数据包抵达接收端的先后顺序不同。另一个则是数据包在传输的过程中丢失了,必须等待发送端重传。
如图,随着consumed bytes(没有gaps的绿色区域)越来越多,当满足下列条件时:(flow control receive offset - consumed bytes) < (max receive window / 2)
接收端就会发送一个WINDOW_UPDATE的信号给发送端,更新flow control receive offset = (consumed bytes + max receive window),如图,允许发送端传送更多后续的数据。可以注意到receive window仍然小于max receive window,确保发送端不会传送大于接收端buffer可承受的数据量。
连接层的流量控制采用的是一模一样的机制,但在byte consumed和highest received byte offset的计算则是将所有Stream的数据加总,如下两张图。
目前要识别TCP的连接,需要用(1)源IP、(2)源port、(3)目的IP和(4)目的port共四个参数来区分收到的数据包是属于哪一个TCP连接。这个机制的缺点就是当客户端的IP变动时,例如手机从WIFI连接转移到4G网络连接,原本的连接就全部失效了。这在频繁于WIFI网络与不同的3G和4G网络中做切换的手机上使用情境上,TCP协议就显得非常的不友善。
因此QUIC为了可以很平顺的处理传输过程中的Connection Migration问题,它连接的识别采用的是一个64 bits的独立Connection ID,ID由客户端在建立连接时随机产生。当客户端的IP改变时,只要客户端继续使用旧的Connection ID传送数据包,即使新发送数据包的源IP地址不同,接收端也可以通过Connection ID顺利的识别新数据包所属的连接。而原本正在传递中,存有旧IP的数据包,也一样可以通过Connection ID来识别,正确的被接收端接收。
以上就是对QUIC协议的简介,分别介绍了它的 Connection Establishment、Stream Multiplexing、Loss Recovery、Flow Control 和 Connection Migration 的机制。
我正在参与2024腾讯技术创作特训营最新征文,快来和我瓜分大奖!
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。