一、QUIC是什么?
QUIC是快速UDP网络连接(Quick UDP Internet Connections)的缩写, 是Google公司研发的一种基于 UDP 协议的低时延互联网传输协议。在过去,QUIC用在HTTP上,后来IETF(国际互联网工程任务组)发现QUIC是个好东西,于是希望把QUIC从HTTP中分离出来(如 : IP / UDP / QUIC / HTTP)。在2016年IETF会议中,HTTP-over-QUIC协议被重命名为HTTP/3,并成为 HTTP 协议的第三个正式版本。
社区中的人们已经使用非正式名称如iQUIC和gQUIC来指代这些不同版本的协议,以将QUIC协议与IETF和Google分开,在wireshark中可以通过输入gquic或quic过滤quic报文。
QUIC是个很大的协议,位于传输层,使用TLS 1.3(RFC 8446),在UDP之上实现类似于TCP + TLS + HTTP/2。
QUIC相关时间:
2013 年由Google提出
2015 年被提议作为 IETF 的标准草案
2016年11月IETF召开了第一次QUIC工作组会议
二、为什么是UDP,而不是TCP?
吐槽历代HTTP
1、HTTP0.9 只有Get方法
2、HTTP1.0 使用短连接,每次请求都要经过三次握手,四次挥手。需要使用keep-alive参数来告知服务器端要建立一个长连接,而HTTP1.1默认支持长连接。
3、HTTP 1.1 队头阻塞。
4、HTTP2.0 IETF借鉴了SPDY,虽然消除了以请求为单元的队头阻塞,但是请求内是以TCP同步发送,没能根治队头阻塞。
2、吐槽TCP:
a. TCP由操作系统内核实现,如果要改变,那么所有操作系统都得改变,不现实。
b. TCP对报文控制太精细,很难有改进的空间,数据包经过严格控制,不能来一场说走就走的旅行。曾经Google有人提出TFO (tcp fast open)对TCP扩展,也提交了RFC,但是没普及。
c. UDP报文结构简单,在此基础上做文章很方便。而且QUIC并非基于系统内核修改,可以进行快速迭代更新。
3、吐槽完:
UDP是不可靠传输,IP也是不可靠传输。为啥QUIC不直接建立在IP上呢?
世界上有很多终端设备通过NAT通信,所有的NAT产品都支持TCP/UDP,根据IP+PORT与终端的会话一一对应。如果QUIC建立在IP之上,并没有端口号,意味着每一个NAT设备只能记忆一个终端的会话,一个全球IP 与一个私有IP的一一映射。那将意味着NAT设备后只能有一个会话可以访问同一个服务器的页面。那还不如赶紧普及IPv6。
再退一步,即使在IP层之上给到QUIC端口号,那也得所有的NAT设备都得修改可以兼容QUIC的规则。所以QUIC建立在UDP上是唯一出路。
4、QUIC相比现在广泛应用的TCP + TLS + HTTP/2协议有如下优势:
(1)通过减少往返次数,以缩短连接建立时间
(2)多路复用,解决HTTP/2队头阻塞问题
QUIC 最基本的传输单元是 Packet,不会超过 MTU 的大小,整个加密和认证过程都是基于 Packet 的,不会跨越多个 Packet。这样就能避免 TLS 协议存在的队头阻塞。Stream 之间相互独立,比如 Stream2 丢了一个 Pakcet,不会影响 Stream3 和 Stream4。不存在 TCP 队头阻塞。
QUIC 的流量控制类似 HTTP2,即在 Connection 和 Stream 级别提供了两种流量控制。为什么需要两类流量控制呢?主要是因为 QUIC 支持多路复用。Stream 可以认为就是一条 HTTP 请求。Connection 可以类比一条 TCP 连接。多路复用意味着在一条 Connetion 上会同时存在多条 Stream。既需要对单个 Stream 进行控制,又需要针对所有 Stream 进行总体控制。相当于rabbitmq的channel和Connection的关系。
简单来说,http2解决http1.1在应用层中队头阻塞的问题,但是没有解决传输层TCP的单个报文的重传阻塞(传输层的队头阻塞没有解决)。而UDP没有报文阻塞这个问题,所以基于UDP的quic协议,可以更深一层的解决阻塞问题。
(3)使用FEC(前向纠错)恢复丢失的包,以减少超时重传
前向纠错是一种差错控制方式,它是指信号在被送入传输信道之前预先按一定的算法进行编码处理,加入带有信号本身特征的冗码,在接收端按照相应算法对接收到的信号进行解码,从而找出在传输过程中产生的错误码并将其纠正的技术。QUIC用的是一阶冗余,如果有一个Group内有其中一条QUIC报文出错可以修正。如果有超过一个QUIC报文出错,则NACK。
(4)使用一个随机数(CID)标志一个连接,取代传统IP + PORT的方式,使得切换网络环境如从4G到wifi仍然能使用之前的连接。
当使用手机在 WIFI 和 4G 移动网络切换时,客户端的 IP 肯定会发生变化,需要重新建立和服务端的 TCP 连接。而任何一条 QUIC 连接不再以 IP 及端口四元组标识,而是以一个 64 位的随机数作为 ID 来标识( 由于这个 ID 是客户端随机产生的,并且长度有 64 位,所以冲突概率非常低),这样就算 IP 或者端口发生变化时,只要 ID 不变,这条连接依然维持着,上层业务逻辑感知不到变化,不会中断,也就不需要重连。
三、QUIC报文
参考 https://tools.ietf.org/html/draft-tsvwg-quic-protocol-02#ref-3
第一字节为:Public Flags:
Public Flag的8位如下所示,左边为高位,右边为低位
public flags
如果Bit0被置上(0x01 = PUBLIC_FLAG_VERSION = 0x01),该字段的含义取决于报文是客户端还是服务端发送。如果是客户端发送,这一位被置上表示QUIC Version字段不为空,且QUIC Version字段被填充为客户端的QUIC版本信息。在客户端收到服务端同意建立该版本的连接报文抵达之前,客户端发送的所有报文的这一位必须都要被设置。如果服务端同意建立该版本的连接,那么这一位不用设置。如果这一位被服务端置上,那么表示该报文是版本协商报文(Version Negotiation Packet)。
如果Bit1被置上(0x02 = PUBLIC_FLAG_RESET),表明该报文是公共重置报文(Public Reset packet)
Bit2、Bit3两位表示报文中的Connection ID的长度。直至协商另外一个值之前,在所有的报文中,这两位必须设置被设置为相同(客户端可能请求更少的数据,所以ConnectID的长度需要变化)
0x0C(Bit3及Bit2均为1)表示Connection ID长度是8字节
0x08(Bit3位1,Bit2为0)表示Connection ID长度是4字节
0x04(Bit3位0,Bit2为1)表示Connection ID长度是1字节
0x00(Bit3及Bit2均为0)表示无Connection ID
Bit4、Bit5两位表示每个数据包中存在的数据包编号的字节数。对于帧数据,这两位才使用,对于Public Reset报文及Version Negotiation报文,这两位必须为0
0x30(Bit5及Bit4均为1)表示:包序号是6个字节
0x20(Bit5为1,Bit4为0)表示:包序号是4个字节
0x10(Bit5为0,Bit4为1)表示:包序号是2个字节
0x00(Bit5及Bit4均为0)表示:包序号是1个字节
Bit6:预留给多路径使用
Bit7:未使用,必须为0
Connection ID:客户端随机选择的最大长度为64位的无符号整数。但是,长度可以协商。
QUIC Version:QUIC协议的版本号,32位的可选字段。如果Public Flag & FLAG_VERSION != 0,这个字段必填。客户端设置Public Flag中的Bit0为1,并且填写期望的版本号。如果客户端期望的版本号服务端不支持,服务端设置Public Flag中的Bit0为1,并且在该字段中列出服务端支持的协议版本(0或者多个),并且该字段后不能有任何报文。
Packet Number:长度取决于Public Flag中Bit4及Bit5两位的值,最大长度6字节。发送端在每个普通报文中设置Packet Number。发送端发送的第一个包的序列号是1,随后的数据包中的序列号的都大于前一个包中的序列号。
QUIC报文分为:
1、特殊报文
版本协商报文(Version Negotiation Packets)
只由服务端发送。版本协议报文以1字节的Public Flag及8字节的Connection ID开始。必须设置PUBLIC_FLAG_VERSION且标识Connection ID长度为8字节,最后面就是服务端支持的协议版本(4字节)。
公共重置报文(Public Reset Packets)
Public Reset报文以1字节的Public Flag及8字节的Connection ID开始。必须设置PUBLIC_FLAG_RESET且标识Connection ID长度为8字节,剩余的部分是QUIC Tag。
2、普通报文
普通的报文被验证(authenticated)且被加密。公有头被验证但是没有加密,从Private Flags字段开始的报文被加密。普通的报文包含AEAD(authenticated encryption and associated data)报文。普通报文必须被解密,并且密文被解密后,明文以Private Header开始。
帧报文
除了私有头外,帧包有一系列的基于类型的帧数据的负载,通用的帧包的格式
FEC(Forward Error Correction)报文
FEC包(FLAG_FEC标志被置为1)的负载只包含位于FEC组中的每个数据包的空填充负载的XOR值。每个FEC包的FLAG_FEC_GROUP标志也必须被置为1
四、浏览器中查看QUIC
有豆瓣,google,Facebook,YouTube,QQ空间。以下是F12豆瓣网的,可以发现豆瓣有在用HTTP1.1,HTTP2.0(h2),QUIC。
在Google新版的Chrome浏览器中,支持QUIC协议,在Chrome浏览器中打开“实验性功能”页面(chrome://flags/),把Experimental QUIC protocol设置enabled。
五、抓包验证
抓不到正常通信的包。将就看个版本匹配失败的例子 - -
客户端发送一个CHLO报文,报文里Public Flags的bit0(Version)设置1,而且设置Version值Q039。创建CID连接号。
服务端收到客户端的连接请求,返回协商报文,Public Flags的bit0(Version)设置1,并返回可支持的QUIC Version版本列表。
客户端发现服务端返回的版本列表里,并没有符合的,于是发送CONNECTION_CLOSE消息,关闭通信。虽然看到连接关闭的字样,但实际上我是可以访问到豆瓣的,因为此时TCP会有替补,走基于TCP的HTTP通信。
六、支持QUIC的开源项目
Caddy:Go 写的 Web 服务器 ,QUIC只是附属功能,但用它的人更多是用来做QUIC实验 :D, 勉强能用在生产环境。
quic-go :是完全用 go 写的 QUIC 协议栈,开发很活跃,已在 Caddy 中使用,MIT 许可。
Chromium :Google 官方维护基本没有坑,随时可以跟随 chrome 更新到最新版本。不过编译 Chromium 比较麻烦。
七、附录:
1、header压缩
在HTTP1.x中,头部元数据都是以纯文本的形式发送的,通常会给每个请求增加500~800字节的负荷。
HTTP2.0使用HPACK算法来减少需要传输的header大小,通讯双方各自cache一份header fields表,既避免了重复header的传输,又减小了需要传输的大小。高效的压缩算法可以很大的压缩header,减少发送包的数量从而降低延迟。
2、参考文档 https://tools.ietf.org/html/draft-ietf-quic-http-17
3、FEC算法 https://blog.csdn.net/u010178611/article/details/82656838