0x01 Quic

QUIC协议于2012年实现,2015年提交RFC草案,它是Goolge为了解决当今WEB应用常见的传输层和应用层问题而提出的,从分层结构上可以看做是TCP+TLS+HTTP2的集合,不过是在UDP的基础上实现的Google Quic协议-LMLPHP

主要解决了下面的几个问题

  1. Connection establishment latency(连接时延)
  2. Flexible congestion control (拥塞控制)
  3. Multiplexing without head-of-line blocking (HOLB问题)
  4. Authenticated and Encrypted Header and Payload (头部加密)
  5. Forward error correction (前向纠错)
  6. Connection migration(连接迁移)

0x02 HOLB问题

HOLB(Head of Line Blocking),当所有请求必须按序执行时,会造成后续请求即使已经达到处理的条件也会由于前面的请求阻塞而阻塞的现象。网络中大量存在着按序的原则,例如路由器转发队列需要按序转发、TCP报文需要按序到达等,HTTP请求在多个层次上也都存在该问题,

HTTP事务级别的 HOLB

在HTTP1.0时代,HTTP请求只能遵循请求-应答、请求-应答的模式,效率极低,一般浏览器都开启多条流来并行多个请求(Chrome 6条)。HTTP1.1引入了Pipelining机制,实现流水线请求,客户端可以一次性向服务端发送多个请求,但HTTP 1.X要求多个HTTP响应必须按序到达,例如请求r1和r2的响应各有几个报文,[r1p1, r1p2]和[r2p1, r2p2],HTTP1.X要求报文按序到达,后面的请求被前面的请求阻塞在服务器,造成了HTTP层面的HOLB问题,大多数浏览器因此并没有采用Pipelining而是沿用之前的多流机制。SPDY打破了HTTP1.X的交付顺序,允许响应报文乱序到达,例如按照[r2p1, r1p1, r1p2, r2p2]的顺序到达,从而解决了HTTP事务层面的HOLB问题。

Google Quic协议-LMLPHP

TCP级别的 HOLB

SPDY解决了HTTP事务层的HOL问题,但无法解决TCP层的HOL问题。TCP的可靠性要求所有数据按序交付应用层,发生丢包时接收端会收到大量乱序(Out of Order)的报文,这些报文只能暂时缓存在TCP接收缓冲区中,并不能交付应用层,造成后面请求响应报文由于前面请求响应报文的丢包而不能被应用层读取。UDP就不存在这个问题,应用层可以提前读取乱序但有价值的数据,在采取多路复用时,不会因为某个响应丢包而造成所有响应都被阻塞在内核缓冲区中,因此Quic采用UDP作为传输层协议。

Google Quic协议-LMLPHP

0x03 连接建立时间

基于传统TCP的WEB应用每次请求至少经过1-3个RTT才能建立连接,包括TCP三次握手和TLS交换秘钥的时间,在长连接中,这点时间不算什么,但在短流时,连接建立时间可能在响应时间内占有相当大的比重,Google也做了相当多的工作来优化连接建立时间。

Google Quic协议-LMLPHP

Google Quic协议-LMLPHP

早些年,Google还搞过TFO(TCP Fast Open )用来加速连接的建立,传统TCP其实可以在SYN报文内携带数据的,只不过应用层无法在三次握手前读取[rfc793],主要在可靠性与安全方面的考虑,在三次握手前就交付数据可能会带来几个问题,一个就是SYN-Flood攻击可以直接向服务器传输数据。TFO通过Cookie的方式防御这种攻击,服务器在初始连接中向客户端发送一个Cookie,之后客户端通过该Cookie向服务器证明自己,服务端在SYN-ACK报文内就可以发送数据了,减少了整整一个RTT的时间。

Google Quic协议-LMLPHPGoogle Quic协议-LMLPHP

QUIC同样采用了Cookie的机制来验证客户端的合法性,从而将整个连接建立过程减少到至多1个RTT,除了在初始连接中服务器需要发送证书和Cookie之外,客户端都可以直接用Cookie在建立连接的同时发送数据。

Google Quic协议-LMLPHP

0x04 其它特性

拥塞控制算法

TCP的拥塞控制算法一直被诟病,以至于UDT、KCP等都是基于UDP实现自己的可靠性与拥塞控制。Quic目前拥塞控制算法目前采用Cubic。Quic的ACK报文中携带这接收端在接收报文与发送ACK报文之间的时间间隔,发送端根据该间隔可以更精确地测量链路时延,TCP-Vegas等基于时延的拥塞控制算法在将来都有在其基础上实现的可能。

前向纠错(Forward Error Correction)

FEC机制下每隔几个报文就发送一个FEC报文,FEC报文为一组报文的异或,在发生丢包时,可以通过未丢失的报文和FEC报文将丢包恢复出来,减少了不必要的重传。QUIC若干个报文组成一个FEC组,这样增加了每个报文的负载,但减少了重传,是一种空间换时间的做法,报文冗余度是一个可控的参数,冗余度越大消耗的带宽越大,同时减少了重传数。QUIC类似于RAID4

连接迁移(Connection Migration)

一条TCP流由原宿IP、原宿端口和协议来标识,而一条Quic流由一个变长ID(0,8,32,64位)唯一标识,这样Quic流并不依赖于IP地址。一方面,当某个网卡崩溃后,QUIC并不需要重新建立连接,只需要切换到另外一个网卡,提高了可靠性;另一方面,QUIC天然具备了Multipath的特质,让多个网卡同时工作,通过添加调度器和多流拥塞控制算法,QUIC可以实现MPTCP的功能,而且是在一个更高的层次上。

0x05 搭建Quic服务器

google自己的chromium项目里已经支持了QUIC,不过不够方便,go语言编写的WEB服务器Caddy已经能够较好地支持Quic与HTTP2.0了,已经在个人服务器上搭建了Caddy-Server,地址为 https://m.codingmozart.com/ ,当然在Google的各大网站不断F5也是可以看到QUIC的。

Chrome浏览器已经支持QUIC,通过chrome://flags/#enable-quic开启QUIC功能,并在chrome://net-internals/#quic里可以看到所有活跃的QUIC流。

Google Quic协议-LMLPHP

Wireshark可以抓到QUIC的包,在报头部分可以看到连接号CID(Connection ID)和报文号Sequence类似于TCP的Seq,报文的主要内容被编码在了负载部分

Google Quic协议-LMLPHP

Google Quic协议-LMLPHP

QUIC有三种类型的报文

Frame Packet:携带一个个数据帧,数据帧类型包括ACK,STREAM,WINDOW_UPDATE等等,其中STRAM类型的Frame就是用来实现Multiplexing的,一个帧可以携带多条流的分片。

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

FEC Packet:携带一组报文的异或,用来前向纠错

 +-----...----+
| Redundancy |
+-----...----+

Public Reset Packet

      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)
+--------+--------+--------+--------+--------+--------+---

0x06 参考文献

[1] https://tools.ietf.org/html/draft-tsvwg-quic-protocol-00#ref-QUIC-CRYPTO

05-08 15:10