QUIC是什么?QUIC是谷歌推出的一套基于UDP的传输协议,它实现了TCP + HTTPS + HTTP/2的功能,目的是保证可靠性的同时降低网络延迟。因为UDP是一个简单传输协议,基于UDP可以摆脱TCP传输确认、重传慢启动等因素,建立安全连接只需要一的个往返时间,它还实现了HTTP/2多路复用、头部压缩等功能。它是一个很大的协议,全称是Quick UDP Internet Connection.
为什么要使用UDP呢?因为TCP是系统内核实现的,如果升级TCP协议,就得让用户升级系统,这个的门槛比较高,而QUIC在UPD基础上由客户端自由发挥,只要有服务器能对接就可以、比较遗憾的是现在主流的代理服务Nginx/Apache都没有实现QUIC,一些比较小众的代理服务如Caddy就实现了。
另外,笔者参加了5月份在上海的WebRTConf,很多讲师都认为QUIC并没有达到谷歌宣传的效果,并且国内运营商有些是屏蔽掉UDP传输的。不过使用UDP本身不是大问题,因为很多游戏如农药、吃鸡的传输协议也是基于UDP封装的,运营商应该只是屏蔽掉了UDP的部分端口如500/4500。
至于QUIC的性能怎么样,我们不妨自已实验一下。怎么在自己的网站开启QUIC呢?
一、开启QUIC
大部分人的网站应该是使用Nginx或者Apache做的服务代理,作用是可以让一台服务器开启多个网站,并支持http/https等协议,业务服务如PHP等就不用去关心类似于怎么建立https连接、加密数据、怎么添加gzip压缩之类的问题了。但是它们都不支持QUIC,可能它们认为这个协议现在还不是特别成熟,迭代比较快,整个协议比较复杂,实现起来比较累。。。
所以需要再加一个Caddy,它支持QUIC,那是不是说我们得抛弃nginx,改换Caddy。并不用,我们用它来处理QUIC服务就行了,其它的还是走nginx。因为nginx是监听在TCP的443和80端口,我们可以让Caddy只监听在UDP的443端口,两者不冲突。如下图,用netstat命令输出http/https端口占用情况:
问题在于,你启动Caddy之后,如果给它指定了https的域名,它必定要占用tcp:443端口,而tcp:443已经被nginx占用了,所以它就启动不了了:
1 2 3 |
$ sudo ./caddy -quic -conf ./conf Activating privacy features... done. 2018/06/09 10:03:25 listen tcp :443: bind: address already in use |
所以这个解决方法在网上搜了一下,有些人是使用一个docker容器,把Caddy跑在里面和nginx的环境隔离开,只把udp:443端口给docker使用,这样就没问题了。但是新开一个docker成本有点高,如果之前没玩过的话。
所以我就想到有没有办法去改它源码,不要让它监听tcp:443,然后重新编译一个呢?试了一下,果然可以,这个方法相应比较简单。
首先,需要安装go和github,如果之前没有装过的话,因为它是使用Golang写的,使用CentOS系统可以这样:
1 2 |
yum install go yum install github |
然后下载源码:
1 2 |
go get github.com/mholt/caddy/caddy go get github.com/caddyserver/builds |
编译安装:
1 2 |
cd ~/go/src/github.com/mholt/caddy/caddy go run build.go |
就会在当前目录下生成一个可执行文件caddy,编译速度非常快,半分钟不到就好了。然后改下源码,在:
caddy/caddyhttp/httpserver/server.go
在这个文件的Listen这个函数里面,把其中一行注释掉,换成:
1 2 |
// ln, err := net.Listen("tcp", s.Server.Addr) ln, err := net.Listen("tcp", "127.0.0.1:61234") |
就是把原本的443地址换成一个没用的地址,然后重新编译,就能在已经开了nginx的情况下正常启动Caddy了,它需要一个配置文件,如下conf文件所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
https://www.yinchengli.com tls /etc/letsencrypt/live/www.yinchengli.com/fullchain.pem /etc/letsencrypt/live/www.yinchengli.com/privkey.pem root /home/blog/ gzip fastcgi / 127.0.0.1:9000 php { root /home/blog/ } rewrite { r .* ext / to /index.php?{query} } |
指定tls的证书,通过fastcgi指令把服务代理到本地的9000端口的php服务,并指定wordpress所在的目录,还可以开启gzip/server push这些东西。然后启动Caddy:
1 2 3 |
$ sudo ./caddy -quic -conf ./conf Activating privacy features... done. https://www.yinchengli.com |
为了能让Caddy变成一个守护进程运行在后台,可以使用nohup命令:
1 |
nohup sudo ./caddy -quic -conf ./conf >/dev/null 2>&1 & |
通过netstat可以看到,已经成功在udp开启443服务了:
浏览器怎么知道网站支持QUIC呢?需要我们给nginx添加一个http头部:
1 2 3 |
location / { add_header alt-svc 'quic=":443"; ma=2592000; v="39"'; } |
浏览器还是先用TCP发起http请求,如果检测到这个头部:
就会切换到QUIC,但是它要是发现QUIC连接不可建立,就还是会继续使用TCP连接,所以不用担心QUIC不可用的问题。我们可以装一个HTTP/2 and SPDY inspector,如果是网站是使用QUIC协议的话,闪电就会变成绿色(普通的HTTP/2是蓝色):
或者是打开chrome://net-internals/#quic也是会显示使用QUIC的网站,如下图所示:
可以看到,我们已经成功开启了QUIC。并且发现谷歌搜索、谷歌邮箱等谷歌的主要服务都使用了QUIC,国内的主要有腾讯云提供支持,QQ空间就是用的QUIC.
现在比较一下QUIC和TCP方式的加载速度,加载同一个网页,禁掉缓存,连续刷新5次,如下图所示:
我们发现,似乎在网络比较好的情况下,QUIC似乎并没有太大的提升。那是不是说只有在网络比较差,丢包比较厉害的情况下QUIC才能发挥它的优势?实际上QUIC并不是专门针对抗丢包设计的,它的目的是做为一种通用传输协议。当然,丢包在网络传输也是一个很重要的话题。
二、QUIC协议
Chromium文档里介绍了使用QUIC的优点,有以下5点:
- 通过减少往返次数,以缩短连接建立时间
- 使用一种新的ACK确认机制(包含了NACK),达到更好的拥塞控制
- 多路复用,并解决HTTP/2队头阻塞问题,即一个流的TCP包丢失导致所有流都暂停组装。在QUIC里面,一个流的包丢失只会影响当前流,不会影响其它流。
- 使用FEC(前向纠错)恢复丢失的包,以减少超时重传
- 使用一个随机数标志一个连接,取代传统IP + 端口号的方式,使得切换网络环境如从4G到wifi仍然能使用之前的连接。
我们将围绕这几个点研究一下QUIC协议。
1. 建立连接
QUIC只需要一次往返就能建立HTTPS连接,如下图所示:
左一表示TCP连接的3次握手需要100ms,左二表示建立HTTPS连接,首次建立需要300ms,已经建立过的需要200ms(算上TCP的三次握手),左三使用QUIC第一次建立只需要100ms,如果建立过了不需要再确认直接0ms.
我们再一次实验一下,QUIC建立连接时间是否会比使用TCP的短,首先是使用TCP的时间,如下图所示:
可以看到初始化建立连接的平均时间为60ms左右,建立SSL连接时间为30ms左右,这个SSL时间是包含在了初始化建立时间里面。
再看一下开启QUIC的:
初始化建立连接时间和SSL握手时间是一样的,在这一组实验里面平均时间仅为23ms,所以可以看到这个QUIC的连接建立延迟还是有比较大的提升。
那么QUIC连接建立的过程是怎么样的呢?怎么实现一次往返就要包含TCP的三次握手以及建立HTTPS连接的功能?
我们先回忆一下HTTPS建立握手的过程,如下图所示:
这个过程我在《https连接的前几毫秒发生了什么》做了详细介绍。这个过程最少需要3个RTT时间,上图省略了一些ACK的确认。QUIC的一个RTT是怎么做到的呢?如下图所示:
QUIC的报文是使用key/value的键值对表示的,key是32位字符串,例如Client Hello用CHLO表示,这4个字母在ASCII表分别为:0x43、0x48、0x4c、0x4f,所以key值为4348 4c4f,共4个字节32位。如果key值只有3个字母,最后一位就为NULL(0)。
第1步客户端发送一个CHLO报文,这个报文里面包含了想要连接的域名和客户端支持的加密套装,还有一个STK,它是Source Address Token的缩写,这个是服务端下发的,第一次连接客户端是没有这个的。
第2步服务端收到Client Hello之后,会发一个Reject响应,由于缺少必要的信息所以拒绝连接,并且会下发一个第1步提到的STK,这个用来区分不同的IP,主要是做为防止DDOS攻击的一个手段,因为DDOS攻击通常会使用假的IP地址,导致TCP连接第三次握手失败,服务端会一直等待最后一个ACK报文,但是由于源IP是一个假的IP,永远等不到,所以就消耗了连接池的资源,当黑客通过病毒等方式控制了很多台计算机之后,每台计算机在同一个时间段不断地伪造源IP发起握手请求,就会导致服务端处理不过来直接瘫痪了。但是在QUIC里面由于服务端根据不同的IP下发一个token,客户端在发起连接请求的时候需要带上这个token,如果token不对的话,服务端可直接拒绝连接,这样DDOS的不断地变换IP就没效了。
除了下发STK,还有一个Server Config,这个Server Config里面包含了服务端支持的密钥交换算法列表和每个算法对应的公钥,客户端决定它想要用的密钥交换算法后取出相应的公钥,再加上服务端的随机数Server Nonce和客户端自己生成的随机数,使用这个3个数生成一把加密数据的主密钥。所以在这个RTT之后,客户端就可以开始加密和发送数据了,就达到了一个RTT就能发数据的目的。在以后建立的连接,只要有这个Server Config就能直接发数据,达到再次连接0RTT的目的。这个Server Config有一个效期,超过有效期需要重新获取。但是这样会有一个问题,就是每次交换的公钥是一样的,就会有重放攻击(Replay Attack)的危险,所以这个虽然可以发数据了,但是需要有第3步确保后续的操作更加安全。Chrome限制了在这个阶段发送的数据类型只能是GET类型的。
第3步客户端发送一个完整的CHLO,除了第一步有的key/value之外,还新加了客户端的随机数nonce、密钥交换算法和公钥。
第4步服务端收到之后再返回一个实时生成的公钥给客户端,客户端收到之后重新生成主密钥,用新的主密钥加密数据。QUIC里面的公钥是可以动态变换的,例如1分钟就换一次。
这一整个就是一个比较完整的QUIC的连接建立过程了,更详细的说明可以见谷歌文档。
在实际的传输中,观察到0s的连接建立:
这个应该就是QUIC的0RTT了。
2. 新的ACK确认机制
其中包括NACK的确认机制,NACK是Negative ACK的意思,意思是接收方告知发送方哪些包丢了。在普通的TCP里面,如果发送方收到三个重复的ACK就会触发快速重传,如果太久没收到ACK就会触发超时重传,而使用NACK可以直接告知发送方哪些包丢了,不用等到超时重传。TCP有一个SACK的选项,也具备NACK的功能,QUIC的NACK有一个区别它每次重传的报文序号都是新的。
QUIC的ACK帧包含了NACK区,如下图所示:
比较详细的QUIC ACK确认机制本篇不再深入讨论。
3. 解决多路复用队头阻塞问题
HTTP/2里的多路复用就是指多个HTTP请求共用一个TCP连接,以减少TCP连接数,达到复用高速信道的作用。但是由于它是基于TCP,会导致如果有一个包丢了,那么TCP的接收窗口就不会往右移动了,直到那个包重传补上了才能往右移动。由于QUIC是基于UDP自已实现的,所以想怎么搞就怎么搞,就可以手动解决这个问题,所有流都连续接收,丢的包的那个流不能够组装,但是其它流不会影响到。
4. FEC前向纠正拥塞控制
FEC是Forward Error Correction前向错误纠正的意思,就是通过多发一些冗余的包,当有些包丢失时,可以通过冗余的包恢复出来,而不用重传。这个算法在多媒体网关拥塞控制有重要的地位。QUIC的FEC是使用的XOR的方式,即发N + 1个包,多发一个冗余的包,在正常数据的N个包里面任意一个包丢了,可以通过这个冗余的包恢复出来,使用异或可以做到:
1 2 3 4 5 6 7 |
let E = XOR(A, B, C, D) then -A = XOR(B, C, D, E) -B = XOR(A, C, D, E) -C = XOR(A, B, D, E) -D = XOR(A, B, C, E) |
E是一个冗余的包,如果A、B、C、D任意一个包丢失都可以借助E计算恢复出来,这里面可能会涉及到一些矩阵运算,具体过程不深入探讨。这种XOR FEC的优点是计算相对简单,缺点是只能只有一个包丢的情况才能恢复,丢了两个就恢复不了了。
5. 切换网络操持连接
经常会有从4G切换到wifi网络或者是从wifi切换到4G网络的场景,由于网络的IP变了,导致需要重新建立连接,而QUIC使用一个ID来标志连接,即使切换网络也可以使用之前的建立连接的数据如交换的密钥,而不用再重新HTTPS握手,不过切换的过程可能会导致有些包丢了,需要利用FEC恢复或者重传。
三、小结
本文介绍了使用Caddy结合nginx提供QUIC服务,需要改一下Caddy的源码,让Caddy只监听在UDP的443端口不要的正常的nginx占用的tcp:443产生冲突。由Caddy支持QUIC服务,而Nginx还是提供普通的HTTP/2服务。
为什么要升级到QUIC呢,因为QUIC是改良版的TCP + HTTPS + HTTP/2,我们可以在实践中使用一下收集一些数据,做一些比较。QUIC是一个很大的协议,本文没有面面俱到,主要是介绍了QUIC握手的过程,重点分析了1个RTT是怎么实现的,还介绍了QUIC使用的NACK和FEC,以达到更好的拥塞控制。
更多关于QUIC的内容可以看Chromium的文档 :QUIC, a multiplexed stream transport over UDP.
目前chorme70.0.3538.102 支持的quic版本QUIC_VERSION_43,和贵站当前的quic39协商不起来,所以只能看到建立http2.0但是无法建立quic。
好像是的,按教程不能成功。