1. QUIC协议概述
QUIC是由谷歌设计的一种基于UDP的传输层网络协议,并且已经成为IETF草案。HTTP/3就是基于QUIC协议的。QUIC只是一个协议,可以通过多种方法来实现,目前常见的实现有Google的quiche,微软的msquic,mozilla的neqo,以及基于go语言的quic-go等。 由于go语言的简洁性以及编译的便捷性,本文将选用quic-go进行quic协议的分析,该库是完全基于go语言实现,可以用于构建客户端或服务端。
QUIC的优势(参考)
- 0RTT快速连接
TCP最少需要花费1RTT的时间来建立连接。下图列分别描述了TLS1.2、TLS1.3和QUIC建立连接的成本。每1列分为上下两个图,上图表示首次建立连接所需成本,下图表示再次建立连接所需成本。
可以看到,TLS1.2下,首次建立连接,首先需要1次RTT建立连接(蓝色线),2次RTT交换密钥和加密策略(黑色线),然后开始通信。再次建立连接时,由于已缓存了密钥,因此少1次RTT。 TLS1.3和QUIC都采用了Diffie-Hellman密钥交换算法来交换密钥。该算法的优点是交换密钥只需要1次RTT。在QUIC下,只有首次建立连接交换密钥时消耗1RTT时间,再次连接时就是0RTT了。这已最大限度的减少握手延迟带来的影响。这个特性在连接延迟较大的移动网络上有较好的性能提升。
- 连接迁移
TCP下一个连接是以四元组标识的,即(SrcIp,SrcPort,DstIp,DstPort)。而QUIC连接是以客户端产生的一个64位随机数作为连接标识。当网络、端口发生改变或中断时,只要连接标识不改变,连接就不会中断。
- 改进拥塞控制
- QUIC在应用层即可实现不同的拥塞控制算法,不需要改操作系统和内核。
- 单个程序的不同连接也能支持配置不同的拥塞控制。这样我们就可以给不同的用户提供更好的拥塞控制。
- 应用程序变更拥塞控制,甚至不需要停机和升级。
- QUIC还有带宽预测,RTT监控,发送速率调整等高级算法支持。
- 双级别流控
TCP通过滑动窗口机制来保证可靠性以及进行流量控制。QUIC更新了其滑动窗口机制,并在Connection和Stream两个级别分别进行流控。
用公式表示就是: connection可用窗口 = stream1可用窗口 + stream2可用窗口 + streamN可用窗口
- 没有队头阻塞的多路复用
SDPY和HTTP2协议的多路复用,是让所有请求在一个TCP连接上传输。前面说过,TCP协议有队头阻塞问题,如果某个资源的某个包丢失了,由于TCP是保证时序的,就会在接收端形成队头阻塞。TCP协议无法区分各个资源的包是否关联,因此会停止处理所有资源,直到丢包恢复。
QUIC是基于UDP的,UDP不需要保证包的时序,因而不存在等待丢包恢复,不存在队头阻塞问题。如果某个资源的某个包丢失了,只会影响单个资源,其他资源会继续传输。
- 实现与升级更灵活
TCP协议栈是写在操作系统内核以及中间设备固件上的,对其更新升级,耗费的时间是以年为周期。 基于UDP协议栈的QUIC协议在应用层实现。应用软件的更新较为轻量,因此协议新特性的升级迭代周期是以月或周来计算。
2. 基于quic-go抓包
实验环境
- MacOS10.13.6
- go version go1.16.6 darwin/amd64
- Wireshark Version 3.6.1
- FireFox 96.0.1 (64 位)
项目下载配置
git clone [https://github.com/lucas-clemente/quic-go.git](https://github.com/lucas-clemente/quic-go.git)
下载项目
cd example
打开实例文件夹
服务器端
go build main.go
./main -qlog -v -tcp
运行服务器
客户端
cd example/client
打开客户端文件夹
go build main.go
编译
./main -v -insecure -keylog ssl.log [https://quic.rocks:4433/](https://quic.rocks:4433/)
客户端访问
浏览器访问
配置火狐浏览器
浏览器访问[https://localhost:6121/demo/tile](https://localhost:6121/demo/tile)
抓包
wireshark配置
使用wireshark对loopback抓包,可以看到抓包信息与服务器获取信息是相匹配的。
3. QUIC源代码分析
echo
echo程序实现了一个简单的回显功能。
服务器端
echoServer()
方法启动一个服务器,回显客户端的所有数据。
// Start a server that echos all data on the first stream opened by the client
func echoServer() error {
listener, err := quic.ListenAddr(addr, generateTLSConfig(), nil)
if err != nil {
return err
}
sess, err := listener.Accept(context.Background())
if err