欢迎您访问 最编程 本站为您分享编程语言代码,编程技术文章!
您现在的位置是: 首页

QUIC协议详解 - 了解QUIC的基础与入门指南

最编程 2024-07-31 16:26:48
...

1.1 QUIC是什么

QUIC :Quick UDP Internet Connections;是一种新的默认加密的互联网通信协议,它提供了许多改进,旨在加速HTTP通信,同时使其变得更加安全,其最终目的是在web上代替TCP和TLS协议。QUIC 协议也是整合了 TCP 协议的可靠性和 UDP 协议的速度和效率。

1.2 QUIC出现的背景

传统的传输层协议是TCP和UDP,但是TCP和UDP存在很多问题,因而促进了QUIC的诞生。

1.2.1 TCP协议的不足

(1) TCP的不足

  • 握手导致的连接成本
  • 队头阻塞
  • 连接不能迁移(手机以及Wifi的场景)
  • 协议历史悠久导致中间设备僵化
  • 依赖于操作系统的实现导致协议本身僵化。

(2) UDP的不足

  • 不稳定
  • 不可靠

(3) 具体缺点的分析

a) 握手导致的连接成本

不管是 HTTP1.0/1.1 还是 HTTPS,HTTP2,都使用了 TCP 进行传输。HTTPS 和 HTTP2 还需要使用 TLS 协议来进行安全传输,因此TCP的缺点HTTP及TLS都有。

两个握手延迟:

  • TCP 三次握手导致的 TCP 连接建立的延迟,TCP的三次握手至少需要1个RTT;
  • TLS 完全握手需要至少 2 个 RTT 才能建立,简化握手需要 1 个 RTT。

对于很多短连接场景,这样的握手延迟影响很大,且无法消除。

b) TCP的可靠成本

在数据传递时,确认机制、重传机制、拥塞控制机制等都会消耗大量的时间,而且要在每台设备上维护所有的传输连接,事实上,每个连接都会占用系统的CPU、内存等硬件资源。

c) 中间设备的僵化

TCP 协议使用得太久,也非常可靠。所以我们很多中间设备,包括防火墙、NAT 网关,整流器等出现了一些约定俗成的动作。

比如有些防火墙只允许通过 80 和 443,不放通其他端口。NAT 网关在转换网络地址时重写传输层的头部,有可能导致双方无法使用新的传输格式。整流器和中间代理有时候出于安全的需要,会删除一些它们不认识的选项字段。

TCP 协议本来是支持端口、选项及特性的增加和修改。但是由于 TCP 协议和知名端口及选项使用的历史太悠久,中间设备已经依赖于这些潜规则,所以对这些内容的修改很容易遭到中间环节的干扰而失败。而这些干扰,也导致很多在 TCP 协议上的优化变得小心谨慎,步履维艰。

d) 依赖于操作系统的实现导致协议僵化

TCP 是由操作系统在内核系统层面实现的,应用程序只能使用,不能直接修改。虽然应用程序的更新迭代非常快速和简单。但是 TCP 的迭代却非常缓慢,原因就是操作系统升级很麻烦。

服务端系统不依赖用户升级,但是由于操作系统升级涉及到底层软件和运行库的更新,所以也比较保守和缓慢。这也就意味着即使 TCP 有比较好的特性更新,也很难快速推广。比如 TCP Fast Open。它虽然 2013 年就被提出了,但是 Windows 很多系统版本依然不支持它。

e) 队头阻塞

队头阻塞主要是 TCP 协议的可靠性机制引入的。TCP 使用序列号来标识数据的顺序,数据必须按照顺序处理,如果前面的数据丢失,后面的数据就算到达了也不会通知应用层来处理。

另外 TLS 协议层面也有一个队头阻塞,因为 TLS 协议都是按照 record 来处理数据的,如果一个 record 中丢失了数据,也会导致整个 record 无法正确处理。

概括来讲,TCP 和 TLS1.2 之前的协议存在着结构性的问题,如果继续在现有的 TCP、TLS 协议之上实现一个全新的应用层协议,依赖于操作系统、中间设备还有用户的支持。部署成本非常高,阻力非常大。

由于TCP存在以上的缺陷,促进了QUIC的诞生。QUIC是基于 UDP实现的一种协议,因为 UDP 本身没有连接的概念,不需要三次握手,优化了连接建立的握手延迟。同时QUIC在应用程序层面实现了 TCP 的可靠性,TLS 的安全性和 HTTP2 的并发性,只需要用户端和服务端的应用程序支持 QUIC 协议,完全避开了操作系统和中间设备的限制。

1.2.2 基于UDP实现的QUIC协议

QUIC协议是基于UDP协议实现。和与TCP 相反,UDP 协议是无连接协议。客户端发出 UDP 数据包后,只能“假设”这个数据包已经被服务端接收。这样的好处是在网络传输层无需对数据包进行确认,但存在的问题就是为了确保数据传输的可靠性,应用层协议需要自己完成包传输情况的确认。

QUIC 非常类似于在 UDP 上实现的 TCP + TLS + HTTP/2。由于 TCP 是在操作系统内核和中间件固件中实现的,因此对 TCP 进行重大更改几乎是不可能的(TCP 协议栈通常由操作系统实现)。但是,由于 QUIC 建立在 UDP 之上,因此没有这种限制。QUIC 可以实现可靠传输,而且相比于 TCP,它的流控功能在用户空间而不在内核空间,那么使用者就不受限于 CUBIC 或是 BBR,而是可以*选择,甚至根据应用场景*调整优化。

QUIC 与现有 TCP + TLS + HTTP/2 方案相比,有以下几点主要特征:
1)利用缓存,显著减少连接建立时间;
2)改善拥塞控制,拥塞控制从内核空间到用户空间;
3)没有队头阻塞的多路复用;
4)前向纠错,减少重传;
5)连接平滑迁移,网络状态的变更不会影响连接断线。
在这里插入图片描述
从图上可以看出,QUIC 底层通过 UDP 协议替代了 TCP,上层只需要一层用于和远程服务器交互的 HTTP/2 API。这是因为 QUIC 协议已经包含了多路复用和连接管理,HTTP API 只需要完成 HTTP 协议的解析即可。

1.2.3 QUIC的基本特征

  • 低延迟的连接建立
  • 更加灵活的拥塞控制
  • 没有队头阻塞的多路复用
  • 流和连接的流量控制
  • 前项纠错
  • 连接迁移
  • 身份认证和加密的头部和负载

1.3 QUIC重要特征详解

1.3.1 低延迟的连接建立

  • QUIC协议是如何做到0RTT加密传输的(addons)
  • QUIC协议和HTTP3.0技术研究-对QUIC的特征介绍的非常详细清楚

有在HTTPS协议中,由于TCP+TLS 需要4~5个RTT,导致连接建立过程较为复杂和耗时,降低了HTTPS的效率。QUIC在握手过程中使用Diffie-Hellman算法协商初始密钥,初始密钥依赖于服务器存储的一组配置参数,该参数会周期性的更新。初始密钥协商成功后,服务器会提供一个临时随机数,双方根据这个数再生成会话密钥。
具体握手过程如下:
(1) 客户端判断本地是否已有服务器的全部配置参数,如果有则直接跳转到(5),否则继续
(2) 客户端向服务器发送inchoate client hello(CHLO)消息,请求服务器传输配置参数
(3) 服务器收到CHLO,回复rejection(REJ)消息,其中包含服务器的部分配置参数
(4) 客户端收到REJ,提取并存储服务器配置参数,跳回到(1)
(5) 客户端向服务器发送full client hello消息,开始正式握手,消息中包括客户端选择的公开数。此时客户端根据获取的服务器配置参数和自己选择的公开数,可以计算出初始密钥。
(6) 服务器收到full client hello,如果不同意连接就回复REJ,同(3);如果同意连接,根据客户端的公开数计算出初始密钥,回复server hello(SHLO)消息,SHLO用初始密钥加密,并且其中包含服务器选择的一个临时公开数。
(7) 客户端收到服务器的回复,如果是REJ则情况同(4);如果是SHLO,则尝试用初始密钥解密,提取出临时公开数
(8) 客户端和服务器根据临时公开数和初始密钥,各自基于SHA-256算法推导出会话密钥
(9) 双方更换为使用会话密钥通信,初始密钥此时已无用,QUIC握手过程完毕。之后会话密钥更新的流程与以上过程类似,只是数据包中的某些字段略有不同。
在这里插入图片描述
如上所述, DH密钥协商需要通行双方各自生成自己的非对称公私钥对。server端与客户端的关系是1对N的关系,明显server端生成一份公私钥对, 让N个客户端公用, 能明显减少生成开销, 降低管理成本。server端的这份公私钥对就是专门用于握手使用的,客户端一经获取,就可以缓存下来后续建连时继续使用, 这个就是达成0-RTT握手的关键, 因此server生成的这份公钥称为0-RTT握手公钥。client端首次握手时对server一无所知,需要1个RTT来询问server端的握手公钥(实际的握手交互还会发送诸如版本等其他数据)并缓存下来,本步骤只在首次建连时发生(0-RTT握手公钥的过期也会导致需要重走这一步),但这种情况很少发生。

另外,前面提到在初始密钥后,还会再协商出一个最终的会话密钥,这么做的目的是为了获取所谓的前向安全特性: 因为server端的后面生成的这份公私钥是临时生成的,不会保存下来,也就杜绝了密钥泄漏导致会话数据被恶意收集后的被解密掉的风险。
QUIC握手过程
由上图可以看出,在C和S首次通信的1RTT后就发送了应用数据,这是实现1RTT建立链接的真正原因。在C和S进行非首次通信时,C端已经缓存了S端的基本配置,即可以直接进行业务数据的发送。QUIC使用的加密算法是DH算法。

QUIC建立链接的过程
(1) step0: 配置服务器S密钥对
在S生成一个素数 p p p和一个整数 g g g,生成一个随机数 K p r i K_{pri} Kpri。根据上述三元组生成 K p u b K_{pub} Kpub
K p u b = ( g K p r i ) m o d ( p ) K_{pub}=(g^{K_{pri}})mod(p) Kpub=(gKpri)mod(p)
根据生成的 K p u b K_{pub} Kpub打包 { p , g , K p u b } \{p,g,K_{pub}\} {p,g,Kpub}为服务端的config。

(2) step1: C端首次发起链接
C发送简单的Client Hello到S。
(3) step2: S首次响应C
S把config封装称一个数据包回复给C,内部含有 { p , g , K p u b } \{p,g,K_{pub}\} {p,g,Kpub}元组。
(4) step3: C发送加密数据
C收到 { p , g , K p u b } \{p,g,K_{pub}\} {p,g,Kpub}后随机生成一个数 K c _ p r i K_{c\_pri} Kc_pri做如下计算:

  • 计算公钥: K c _ p u b = ( g K c _ p r i ) m o d ( p ) K_{c\_pub} = (g^{K_{c\_pri}})mod(p) Kc_pub=(gKc_pri)mod(p)
  • 计算对称密钥: K 1 = ( K p u b K c _ p r i ) m o d ( p ) K1=(K^{Kc\_pri}_{pub} )mod (p) K1=(KpubKc_pri)mod(p)

准备业务数据payload1,设加密函数为 E n c ( k e y , d a t a ) Enc(key,data) Enc(key,data),将下列元组D1发送给S:
D 1 = { K c _ p u b , E n c ( K 1 , p a y l o a d 1 ) } D_1=\{K_{c\_pub},Enc(K_1,payload1)\} D1={Kc_pub,Enc(K1,payload1)}
该阶段开始,payload便是加密传输的。

(5) step4: S发送加密数据
S收到 D 1 D_1 D1后,做如下计算:

  • 计算对称密钥: K 1 ′ = ( K c _ p u b K p r i ) m o d ( p ) K_{1}^{'}=(K_{c\_pub}^{K_{pri}})mod(p) K1=(Kc_pubKpri)mod(p)

利用计算计算得到的 K 1 ′ K_{1}^{'} K1可以解密 E n c ( K 1 , p a y l o a d 1 ) Enc(K_1,payload1) Enc(K1,payload1)

S在发送自己的payload2之前,随机生成一个数 K n _ p r i K_{n\_pri} Kn_pri,做如下计算:

  • 计算新的通信公钥: K n _ p u b = ( g K n _ p r i ) m o d ( p ) K_{n\_pub}=(g^{K_{n\_pri}})mod(p) Kn_pub=(gKn_pri)mod(p)
  • 计算新的通信对称密钥: K 2 = ( K c _ p u b K n _ p r i ) m o d ( p ) K_{2}=(K_{c\_pub}^{K_{n\_pri}})mod(p) K2=(Kc_pubKn_pri)mod(p)

计算出 K 2 K_2 K2后,可以把 D 2 = { K n _ p u b , E n c ( K 2 , p a y l o a d 2 ) } D_2=\{ K_{n\_pub},Enc(K_2,payload2)\} D2={Kn_pub,Enc(K2,payload2)}发送到C。
(6) step5: C收到S的 D 2 D_2 D2
C收到S发来的 D 2 D_2 D2后,解出其中的 K n _ p u b K_{n\_pub} Kn_pub,做如下运算:

  • 计算新的通信密钥: K 2 ′ = ( K c _ p r i n _ p u b ) m o d ( p ) K_2^{′}=(K_{c\_pri}^{n\_pub} )mod(p) K2=(Kc_prin_pub)mod(p)

K 2 ′ K_2^{′} K2可以正确解密出payload2。其中 K 2 ′ K_2^{′} K2 K 2 K_2 K2等价。
此后的通讯,S和C便可以用K2做通信对称密钥了。
(7) step 6:C和S断开连接
S和C之间通信一会儿后,断开连接。

(8) step 7:C直接发送加密数据
一段时间后,C和S再次通信。此时C已经有了S的config元组 p , g , K p u b {p,g,K_{pub}} p,g,Kpub,直接从Step 3开始数据传输。

(9) step 8:S发送加密数据
这里在S收到C的加密数据后,重复step 4重新计算出一个新的“安全对称密钥”作为之后数据传输的对称密钥。

几个问题:
Q1. 为什么QUIC可以实现0RTT建立连接
0 RTT 的效果是因为QUIC的客户端会缓存服务器端发的令牌和证书,当有数据需要再次发送的时候,客户端可以直接使用旧的令牌和证书,这样子就实现了 0 RTT 了。对于没有缓存的情况,服务器端会直接拒绝请求,并且返回新生产的令牌和证书。 所以当令牌失效或者没有缓存的情况下,QUIC还是需要一次握手才能开始传输数据。

Q2 密钥的生成以及DH加密算法是什么?

Tips:
首先明确一个概念是 RTT(round-trip time),顾名思义,就是服务器和终端一次交互需要的时间。RTT一般用于衡量网络延迟。传统的TCP协议,我们需要进行3次握手,也就是1.5 RTT,才开始传输数据。TCP的三次握手的时间也可以是1RTT,因为客户端发送ACK同时,就可以直接下发业务报文数据。

1.3.2 更灵活的拥塞控制

QUIC协议当前默认使用TCP协议的Cubic拥塞控制算法。看似QUIC协议只是把TCP的拥塞算法重新实现了一遍,其实不然。QUIC协议在TCP拥塞算法基础上做了些改进:

(1) 可插拔

什么叫可插拔呢?就是能够非常灵活地生效,变更和停止。

  • 应用程序层面就能实现不同的拥塞控制算法,不需要操作系统,不需要内核支持。这是一个飞跃,因为传统的 TCP 拥塞控制,必须要端到端的网络协议栈支持,才能实现控制效果。而内核和操作系统的部署成本非常高,升级周期很长,这在产品快速迭代,网络爆炸式增长的今天,显然有点满足不了需求。
  • 单个应用程序的不同连接也能支持配置不同的拥塞控制。就算是一台服务器,接入的用户网络环境也千差万别,结合大数据及人工智能处理,我们能为各个用户提供不同的但又更加精准更加有效的拥塞控制。比如 BBR 适合,Cubic 适合。
  • 应用程序不需要停机和升级就能实现拥塞控制的变更。我们在服务端只需要修改一下配置,reload 一下,完全不需要停止服务就能实现拥塞控制的切换。

(2) 单调递增的Packet Number

  • QUIC并没有使用TCP的基于字节序号及ACK来确认消息的有序到达,QUIC使用的是Packet Number,每个Packet Number严格递增,所以如果Packet N丢失了,重传Packet N的Packet Number已不是N,而是一个大于N的值。 这样就很容易解决TCP的重传歧义问题。

(3) 更多的ACK块

  • QUIC ACK帧支持256个ACK块,相比TCP的SACK在TCP选项中实现,有长度限制,最多只支持3个ACK块

(4) 精确计算RTT时间

  • QUIC ACK包同时携带了从收到包到回复ACK的延时,这样结合递增的包序号,能够精确的计算RTT。

1.3.3 无队头阻塞的多路复用

基于TCP的HTTP2的一个较大问题是队头阻塞问题。应用程序将TCP连接看作字节流,当TCP数据包丢失时,HTTP2连接上的流不能继续进行,直到数据包被重传并被远端接收——甚至当这些流的数据包已经到达并在缓冲区中等待时也不能。

因为QUIC是专门为多路复用操作而设计的,丢失的数据包携带单个流的数据通常只影响那个特定的流。每个流帧可以在到达时立即分配到该流,因此没有丢失的流可以继续重新组装并在应用程序中继续进行。
在这里插入图片描述
在这里插入图片描述

补充:
(1) TCP的流量控制策略
TCP 保证了数据的有序和可达性,所以原则上是数据按照序号依次发送和接收,下一个包的发送需要等到上一个包 ACK 到达。这样的话,在相邻两个包的发送间隙存在很长时间的空闲等待,好在 TCP 采用了滑动窗口机制来减少了排队等待时间,双方约定一定大小的窗口,在这个窗口内的包都可以同步发送,接收方收到一个 packet 时会回复 ACK 给发送方,发送方收到 ACK 后移动发送窗口,发送后续数据。
但是如果某个 packet 丢失或者其对应的 ACK 包丢失,同样会出现一方不必要的等待。如下图情况,packet 5的 ACK 包丢失,导致发送方无法移动发送窗口,但接收方已经在等待后面的包了。必须等到接收方超时重传这个 ACK 包,接收方超收到这个 ACK 包后,发送窗口才会移动,继续后面的发送行为。
在这里插入图片描述
在这里插入图片描述

1.3.4前向冗余纠错

为了在不等待重传的情况下从丢失的数据包中恢复,QUIC可以用FEC数据包补充一组数据包。很像RAID-4, FEC包包含FEC组中包的奇偶校验。如果组中的一个包丢失,则可以从FEC包和组中的其余包中恢复该包的内容。发送方可以决定是否发送FEC数据包来优化特定场景(例如,请求的开始和结束)。但是这种机制可能会造成传输数据包的冗余,目前已经舍弃

1.3.5 连接迁移

QUIC连接由客户端随机生成的64位连接ID标识。相比之下,TCP连接由源地址、源端口、目的地址和目的端口的4元组标识。这意味着,如果客户端更改了IP地址(例如,从Wi-Fi范围转移到蜂窝网络)或端口(如果NAT映射丢失并重新绑定端口关联),那么任何活动的TCP连接都不再有效。当QUIC客户端更改IP地址时,它可以继续使用来自新IP地址的旧连接ID,而不会中断任何正在运行的请求。

1.3.6 流量控制

首先分两个定义,Stream和Connections:

  • Stream 可以认为就是一条 HTTP 请求。
  • Connection 可以类比一条 TCP 连接。多路复用意味着在一条 Connetion 上会同时存在多条 Stream。既需要对单个 Stream 进行控制,又需要针对所有 Stream 进行总体控制。

(1) 博客一种对QUIC流量控制的解释

  • QUIC中的流量控制

当客户端与服务端进行发送数据的时候,有可能因为发送者发送的速度太快,导致接收者来不及接收,因此会出现分组的丢失,因此为了解决这个问题,解决的根本应该是控制发送者的速度,因此服务端在进行TCP通信时,使用滑动窗口协议。

quic的流量控制是在此基础上的改进,分为了两类,第一类是连接上的流量控制,可以类比成TCP连接。第二类是逻辑流上的流量控制,可以类比为HTTP请求,quic对这两种类型分别进行流量控制。
在这里插入图片描述
如上图,对于quic中的可用窗口也会分为两类:

  • 针对Connection:可用窗口 = 最大窗口数 - 接收到的最大偏移数;

  • 针对流:可用窗口 = stream1可用 + stream2 + … + streamN

(2) 博客二对流量控制的解释

  • 跟坚哥学 QUIC 系列:流量控制(Flow Control)

QUIC 是基于 UDP 传输的,而 UDP 没有流量控制,因此 QUIC 实现了自己的流量控制机制,分为 Stream 和 Connection 两种级别:

1) Stream级别的流控

通过限制 stream 可以发送的数据量,防止单个 stream 消耗连接(connection)的全部缓冲区。与 TCP 不同,就算此前有 packet 没有接收到,它的滑动只取决于接收到的最大偏移字节数(highest received byte offset)。只要还有可用窗口,发送方可以继续发送数据。

  • 在握手时,接收方通过传输参数(transport parameters)设置 stream 的初始限制。
  • 发送方根据这个值进行流量控制,大致过程跟 TCP 相似。
  • 如果发送方达到限制,(可选)则可以发送STREAM_DATA_BLOCKED帧给接收方,以告示它有数据要发送,但被流量控制限制阻止。
  • 接收方如果有更大的窗口值,可以发送MAX_STREAM_DATA帧通知发送方增加。
  • 如果发送方违反流量控制的限制,接收方可以关闭连接并返回FLOW_CONTROL_ERROR错误。

可用窗口 = 最大窗口数 - 接收到的最大偏移数

在这里插入图片描述

2) Connections级别的流量控制
Connections级别的流量控制:对connection 中所有 streams 相加起来的总字节数进行限制,防止发送方超过 connection 的缓冲(buffer)容量。

  • 在握手时,接收方通过传输参数(transport parameters)设置 connection 的初始限制。

  • 发送方根据计算 connection 中所有 streams 的可用窗口,与这个连接窗口值对比进行流量控制。

  • 如果发送方达到限制,(可选)则可以发送STREAM_BLOCKED帧给接收方,以告示它有数据要发送,但被流量控制限制阻止。

  • 接收方如果有更大的窗口值,可以发送MAX_DATA帧通知发送方增加。

  • 如果发送方违反流量控制的限制,接收方可以关闭连接并返回FLOW_CONTROL_ERROR错误。
    下图所示的例子,所有 streams 的最大窗口数为 120,其中:

      stream 1 的最大接收偏移为 100,可用窗口 = 120 - 100 = 20
      stream 2 的最大接收偏移为 90,可用窗口 = 120 - 90 = 30
      stream 3 的最大接收偏移为 110,可用窗口 = 120 - 110 = 10
      那么整个 Connection 的可用窗口 = 20 + 30 + 10 = 60
    

在这里插入图片描述
在这里插入图片描述
可用窗口 = stream 1 可用窗口 + stream 2 可用窗口 +… + stream N 可用窗口

1.4 QUIC协议包类型和格式

QUIC 具有特殊包和普通包。有两种类型特殊包:版本协商包 (Version Negotiation Packets) 和 公共复位包 (Public Reset Packets),普通包包含帧。
所有 QUIC 包的大小应该适配在路径的 MTU 以避免IP分片。路径 MTU 发现是正在进行中的工作,而当前 QUIC 实现为 IPv6 使用 1350 字节的最大QUIC包大小,IPv4 使用1370字节。两个大小都没有 IP 和 UDP 过载。

1.4.1 QUIC公共包头

公共包头的格式如下:

--- src
     0        1        2        3        4            8
+--------+--------+--------+--------+--------+---    ---+
| Public |    Connection ID (64)    ...                 | ->
|Flags(8)|      (optional)                              |
+--------+--------+--------+--------+--------+---    ---+

     9       10       11        12   
+--------+--------+--------+--------+
|      QUIC Version (32)            | ->
|         (optional)                |                           
+--------+--------+--------+--------+


    13       14       15        16      17       18       19       20
+--------+--------+--------+--------+--------+--------+--------+--------+
|                        Diversification Nonce                          | ->
|                              (optional)                               |
+--------+--------+--------+--------+--------+--------+--------+--------+

    21       22       23        24      25       26       27       28
+--------+--------+--------+--------+--------+--------+--------+--------+
|                   Diversification Nonce Continued                     | ->
|                              (optional)                               |
+--------+--------+--------+--------+--------+--------+--------+--------+

    29       30       31        32      33       34       35       36
+--------+--------+--------+--------+--------+--------+--------+--------+
|                   Diversification Nonce Continued                     | ->
|                              (optional)                               |
+--------+--------+--------+--------+--------+--------+--------+--------+

    37       38       39        40      41       42       43       44
+--------+--------+--------+--------+--------+--------+--------+--------+
|                   Diversification Nonce Continued                     | ->
|                              (optional)                               |
+--------+--------+--------+--------+--------+--------+--------+--------+


    45      46       47        48       49       50
+--------+--------+--------+--------+--------+--------+
|           Packet Number (8, 16, 32, or 48)          |
|                  (variable length)                  |
+--------+--------+--------+--------+--------+--------+

---

(1) 公共标记(Public Flags)

  • 0x01 = PUBLIC_FLAG_VERSION。这个标记的含义与包是由服务器还是客户端发送的有关。当由客户端发送时,设置它表示头部包含 QUIC 版本 (参考下面的说明)。客户端必须在所有的包中设置这个位,直到客户端收到来自服务器的确认,同意所提议的版本。服务器通过发送不设置该位的包来表示同意版本。当这个位由服务器设置时,包是版本协商包。版本协商在后面更详细地描述。
  • 0x02 = PUBLIC_FLAG_RESET。设置来表示包是公共复位包。
  • 0x04 = 表明在头部中存在 32字节的多样化随机数。
  • 0x08 = 表明包中存在完整的8字节连接ID。必须为所有包设置该位,直到为给定方向协商出不同的值 (比如,客户端可以请求包含更少字节的连接ID)。
  • 0x30 处的两位表示每个包中存在的数据包编号的低位字节数。这些位只用于帧包。没有包号的公共复位和版本协商包 (由服务器发送) ,不使用这些位,且必须被设置为0。这2位的掩码:
    • 0x30 表示包号占用6个字节。
    • 0x20 表示包号占用4个字节。
    • 0x10 表示包号占用2个字节。
    • 0x00 表示包号占用1个字节。
    • 0x40 为多路径使用保留。
    • 0x80 当前未使用,且必须被设置为0。

(2) 连接ID
这是客户端选择的无符号64位统计随机数,该数字是连接的标识符。由于 QUIC 的连接被设计为,即使客户端漫游,连接依然保持建立状态,因而 IP 4元组(源IP,源端口,目标IP,目标端口)可能不足以标识连接。对每个传输方向,当4元组足以标识连接时,连接ID可以省略。

(3) QUIC版本
表示 QUIC 协议版本的32位不透明标记。只有在公共标记包含 FLAG_VERSION(比如 public_flags & FLAG_VERSION !=0) 时才存在。客户端可以设置这个标记,并 准确 包含一个提议版本,同时包含任意的数据(与该版本一致)。当客户端提议的版本不支持时,服务器可以设置这个标记,并可以提供一个可接受版本的列表(0或多个),但 一定不能(MUST not) 在版本信息之后包含任何数据。最近的实验版本的版本值示例包括 “Q025”,它对应于 byte 9 包含 ‘Q”,byte 10 包含 ‘0”,等等。[参考本文末尾的不同版本变化列表。]

(4) 包号
包号的低 8,16,32,或 48 位,基于公共标记的 FLAG_?BYTE_SEQUENCE_NUMBER 标记被设置为什么。每个普通包(与特别的公共复位和版本协商包相反)由发送者分配包号。由某一端发送的首包包号应该为1,后续每个包的包号应该比前一个的大1。

1.4.2 特殊包

(1) 版本协商包
只有服务器会发送版本协商包。版本协商包以8位的公共标记和64位的连接ID开始。公共标记必须设置PUBLIC_FLAG_VERSION,并指明64位的连接ID。版本协商包的其余部分是服务器支持的版本的4字节列表:

--- src
     0        1        2        3        4        5        6        7       8
+--------+--------+--------+--------+--------+--------+--------+--------+--------+
| Public |    Connection ID (64)                                                 | ->
|Flags(8)|                                                                       |
+--------+--------+--------+--------+--------+--------+--------+--------+--------+

     9       10       11        12       13      14       15       16       17
+--------+--------+--------+--------+--------+--------+--------+--------+---...--+
|      1st QUIC version supported   |     2nd QUIC version supported    |   ...
|      by server (32)               |     by server (32)                |             
+--------+--------+--------+--------+--------+--------+--------+--------+---...--+

---

(2) 公共复位包
公共复位包以8位的公共标记和64位的连接ID开始。公共标记必须设置 PUBLIC_FLAG_RESET,并表明64位的连接ID。公共复位包的其余部分像标记 PRST 的加密握手消息那样编码(参考[QUIC-CRYPTO]):


--- src
     0        1        2        3        4         8
+--------+--------+--------+--------+--------+--   --+
| Public |    Connection ID (64)                ...  | ->
|Flags(8)|                                           |
+--------+--------+--------+--------+--------+--   --+

     9       10       11        12       13      14       
+--------+--------+--------+--------+--------+--------+---
|      Quic Tag (32)                |  Tag value map      ... ->
|         (PRST)                    |  (variable length)                         
+--------+--------+--------+--------+--------+--------+---
---

标记值映射(Tag value map):标记值映射包含如下的标记值:

  • RNON (public reset nonce proof) - 一个64位的无符号整数。必须。
  • RSEQ (rejected packet number) - 一个64位的包号。必须。
  • CADR (client address) - 观察到的客户端IP地址和端口号。它当前只被用于调试,因而是可选的。
    (TODO:公共复位包应该包含认证的(目标)服务器 IP/端口。)

1.4.3 普通包

普通包已经过认证和加密。公共头部已认证但未加密,从第一帧开始的包的其余部分已加密。紧随公共头部之后,普通包包含 AEAD(authenticated encryption and associated data)数据。要解释内容,这些数据必须先解密。解密之后,明文由一系列帧组成。

(1) 帧包
帧包具有一个载荷,它是一系列的类型前缀帧。帧类型的格式将在本文档的后面定义,但帧包的通用格式如下:

--- src
+--------+---...---+--------+---...---+
| Type   | Payload | Type   | Payload |
+--------+---...---+--------+---...---+
---

1.5 QUIC帧类型

QUIC帧包由帧填充。它具有一个帧类型字节,它本身具有一个依赖类型的解释,后面是依赖类型的帧首部字段。所有的帧被包含在单独的QUIC包中,且没有帧可以跨越QUIC包边界。QUIC帧类型字节有两种解释,导致两种帧类型:特殊帧类型和普通帧类型。特殊帧类型在帧类型字节中同时编码帧类型和对应的标记,而普通帧类型简单地使用帧类型字节。

特殊帧类型的定义如下:

--- src
   +------------------+-----------------------------+
   | Type-field value |     Control Frame-type      |
   +------------------+-----------------------------+
   |     1fdooossB    |  STREAM                     |
   |     01ntllmmB    |  ACK                        |
   |     001xxxxxB    |  CONGESTION_FEEDBACK        |
   +------------------+-----------------------------+
---

普通帧类型的定义如下:

--- src
   +------------------+-----------------------------+
   | Type-field value |     Control Frame-type      |
   +------------------+-----------------------------+
   | 00000000B (0x00) |  PADDING                    |
   | 00000001B (0x01) |  RST_STREAM                 |
   | 00000010B (0x02) |  CONNECTION_CLOSE           |
   | 00000011B (0x03) |  GOAWAY                     |
   | 00000100B (0x04) |  WINDOW_UPDATE              |
   | 00000101B (0x05) |  BLOCKED                    |
   | 00000110B (0x06) |  STOP_WAITING               |
   | 00000111B (0x07) |  PING                       |
   +------------------+-----------------------------+
---

(1) STREAM 帧
STREAM帧被隐式地创建流和在流上发送数据,他的格式如下:

--- src
     0        1       …               SLEN
+--------+--------+--------+--------+--------+
|Type (8)| Stream ID (8, 16, 24, or 32 bits) |
|        |    (Variable length SLEN bytes)   |
+--------+--------+--------+--------+--------+

  SLEN+1  SLEN+2     …                                         SLEN+OLEN   
+--------+--------+--------+--------+--------+--------+--------+--------+
|   Offset (0, 16, 24, 32, 40, 48, 56, or 64 bits) (variable length)    |
|                    (Variable length: OLEN  bytes)                     |
+--------+--------+--------+--------+--------+--------+--------+--------+

  SLEN+OLEN+1   SLEN+OLEN+2
+-------------+-------------+
| Data length (0 or 16 bits)|
|  Optional(maybe 0 bytes)  |
+------------+--------------+
---

STREAM帧首部中的字段如下:

  • 帧类型:帧类型字节是一个包含多种标记 (1fdooossB) 的8位值:
    • 最左边的位必须被设为 1 以指明这是一个STREAM帧。
    • ‘f’ 位是FIN位。当被设置为 1 时,这个位表明发送者已经完成在流上的发送并希望 “half-close(半关闭)”(稍后将详细描述)。本文档的后面将更详细地描述。
    • ‘d’ 位表明STREAM头部中是否包含数据长度。当设为0时,这个字段表明STREAM帧扩展至包的结尾。
    • 接下来的三个’ooo’位编码Offset头部字段的长度为0,16,24,32,40,48,56,或64位长。
    • 接下来的两个 ‘ss’ 位编码流 ID头部字段的长度为 8,16,24,或32位长。
  • 流 ID:一个大小可变的流唯一的无符号ID。
  • 偏移:一个大小可变的无符号数字指定流中这块数据的字节偏移。
  • 数据长度:一个可选的16位无符号数字指定这个流帧中数据的长度。只有当包是 “全大小(full-sized)” 包时,才应该省略长度,来避免填充破坏的风险。

一个流帧必须总是要么具有非零的数据长度,要么设置了FIN位。

(2) ACK帧

1.6 QUIC的传输参数

推荐阅读