QUIC协议分析-基于quic-go
quic协议分析
QUIC是由谷歌设计的一种基于UDP的传输层网络协议,并且已经成为IETF草案。HTTP/3就是基于QUIC协议的。QUIC只是一个协议,可以通过多种方法来实现,目前常见的实现有Google的quiche,微软的msquic,mozilla的neqo,以及基于go语言的quic-go等。
由于go语言的简洁性以及编译的便捷性,本文将选用quic-go进行quic协议的分析,该库是完全基于go语言实现,可以用于构建客户端或服务端。
源码编译与测试
下载
- 从https://golang.org/dl/下载golang编译器,要求go版本为1.14+。
- 使用
git clone https://github.com/lucas-clemente/quic-go.git下载库
编译
服务端
cd example
go build main.go
之后使用./main -qlog -v -tcp运行即可。
必须带上-tcp参数是因为浏览器第一次访问时仍然是要通过TCP进行的,如果不带浏览器将无法访问。
客户端
先修改example/client/main.go,在60行之后加上qconf.Versions = []protocol.VersionNumber{protocol.VersionDraft29},选择quic版本为draft-29。
cd example/client
go build main.go
之后使用./main -v -insecure -keylog ssl.log https://quic.rocks:4433/即可访问支持quic协议的网站。
服务端测试
浏览器访问
在firefox中打开about:config,搜索HTTP3,将值设为True以打开HTTP3的实验特性。
打开https://localhost:6121/demo/tile网页,通过调试工具查看请求,当第一次请求该网页时,会通过TCP协议进行:

而在响应头中会带上Alt-Svc,以告诉浏览器该服务器支持HTTP3协议:

之后刷新页面,浏览器就会以HTTP3协议来访问:

抓包
使用wireshark对loopback进行抓包,过滤器设置为udp.port==6121,此时wireshark只显示为UDP协议,并未解析为quic,需要右键Decode As解析为quic。


可以看到,第一个包的类型为Initial,进行了0-RTT的初始化。
问题解决
当访问时,服务器可能会报错Client offered version draft-29, sending Version Negotiation,这是因为当使用-tcp选项后,将使用默认设置,而在默认设置中未开启draft-29版本的支持,因此需要修改源码,将internal/protocol/version.go:30中的var SupportedVersions = []VersionNumber{VersionTLS}修改为var SupportedVersions = []VersionNumber{VersionTLS, VersionDraft29}即可。
客户端测试
使用./main -v -insecure -keylog key.log https://quic.rocks:4433/访问测试网站,可以看见最后成功输出了网页的内容 “You have successfully loaded quic.rocks using QUIC!”,使用的协议为HTTP/3,并且错误代码为0x100,即未发生错误。

抓包
在wireshark中的首选项-protocol-tls-(pre)-master-secret log filename设置为上面输出的key.log文件,用来对quic的payload进行解密,之后可以看到客户端的完整的请求过程,包括1-RTT的握手,HTTP3数据发送,断开连接等:

协议分析
数据包
quic的数据包是通过UDP数据报进行传输的,一个数据报中可以包含一个或多个quic数据包。quic数据包编号被分为三个空间:
- Initial:所有初始包
- Handshake:所有握手包
- Application data:所有 0-RTT 和 1-RTT 加密的数据包
从上图的抓包中可以看见三种类型的包:Initial,Handshake以及Protected payload即Application data。
首部
quic首部分为两种:Long header 和 Short Header,通过第一个有效字节的最高位来区分。首部当中有部分字段是于版本有关的,本文将以quic-29为基础进行分析。
Long header的定义如下:
Long Header Packet {
Header Form (1) = 1,
Fixed Bit (1) = 1,
Long Packet Type (2),
Type-Specific Bits (4),
Version (32),
Destination Connection ID Length (8),
Destination Connection ID (0..160),
Source Connection ID Length (8),
Source Connection ID (0..160),
}
Long Header Packets的类型包括四种:Initial,0-RTT,Handshake,Retry。
Short Header的定义如下:
Short Header Packet {
Header Form (1) = 0,
Fixed Bit (1) = 1,
Spin Bit (1),
Reserved Bits (2),
Key Phase (1),
Packet Number Length (2),
Destination Connection ID (0..160),
Packet Number (8..32),
Packet Payload (..),
}
在版本协商以及1-RTT密钥传输完成后,quic就会使用Short Header Packet来传输数据。
连接迁移 Connection Migration
quic通过在首部携带Connection ID来保证在底层协议(UPD、IP等)寻址发生变化时也能够将数据包分发到正确的端点上。在TCP协议中,是通过四元组(源 IP,源端口,目的 IP,目的端口)来标识连接的,而当网络发生切换时,IP就会发生变化,使得连接需要重新建立,浪费大量时间;而quic通过Connection ID来对连接进行标识,只要ID不变,这条连接就可以保持,这就给quic协议带来了连接迁移的特性。
握手
quic加密握手提供以下属性:
- 认证密钥交换,其中
- 服务端总是经过身份验证
- 客户端可以选择性进行身份验证
- 每个连接都会产生不同并且不相关的密钥
- 密钥材料(keying material)可用于 0-RTT 和 1-RTT 数据包的保护
- 两个端点(both endpoints)传输参数的认证值,以及服务端传输参数的保密保护
- 应用协议的认证协商(TLS 使用 ALPN)
1-rtt的握手流程如下所示:
Client Server
Initial[0]: CRYPTO[CH] ->
Initial[0]: CRYPTO[SH] ACK[0]
Handshake[0]: CRYPTO[EE, CERT, CV, FIN]
<- 1-RTT[0]: STREAM[1, "..."]
Initial[1]: ACK[0]
Handshake[0]: CRYPTO[FIN], ACK[0]
1-RTT[0]: STREAM[0, "..."], ACK[0] ->
Handshake[1]: ACK[0]
<- 1-RTT[1]: HANDSHAKE_DONE, STREAM[3, "..."], ACK[0]
0-rtt的握手流程如下所示:
Client Server
Initial[0]: CRYPTO[CH]
0-RTT[0]: STREAM[0, "..."] ->
Initial[0]: CRYPTO[SH] ACK[0]
Handshake[0] CRYPTO[EE, FIN]
<- 1-RTT[0]: STREAM[1, "..."] ACK[0]
Initial[1]: ACK[0]
Handshake[0]: CRYPTO[FIN], ACK[0]
1-RTT[1]: STREAM[0, "..."] ACK[0] ->
Handshake[1]: ACK[0]
<- 1-RTT[1]: HANDSHAKE_DONE, STREAM[3, "..."], ACK[1]
源码分析
在example的client代码中,通过http3.RoundTripper建立了一个中间件,之后将roundTripper传递给http.Client建立了一个http客户端,并以此来发起http请求。
roundTripper := &http3.RoundTripper{
TLSClientConfig: &tls.Config{
RootCAs: pool,
InsecureSkipVerify: *insecure,
KeyLogWriter: keyLog,
},
QuicConfig: &qconf,
}
defer roundTripper.Close()
hclient := &http.Client{
Transport: roundTripper,
}
rsp, err := hclient.Get(addr)
http3.RoundTripper实现了net.RoundTripper接口,使http客户端将发起请求的过程交由该中间件来处理。该接口定义如下,只有一个函数RoundTrip接受一个http请求,返回http响应。
type RoundTripper interface {
RoundTrip(*Request) (*Response, error)
}
在http3.RoundTripper的实现中,将请求又交给了RoundTripOpt函数来处理。该函数中首先判断请求是否合法,如果不合法就关闭请求,合法就会通过cl, err := r.getClient(hostname, opt.OnlyCachedConn)来获取quic客户端。
而在getClient函数中,通过hash表来获取quic client,如果不存在就会通过newClient函数建立新client。
当获取到client之后,就会通过client.RoundTrip函数发起请求。
而在client.RoundTrip中,在发起请求之前,会调用authorityAddr来确保源地址不是伪造的。当第一次发送请求时会调用dial函数进行握手,如果使用0rtt请求,就立即发送请求,否在当握手完成后通过doRequest发出请求。
QUIC请求流程分析
时序图
整个过程的时序图如下所示,忽略了部分ACK帧:

可以看出在1-RTT时,就开始了数据的传输,在2RTT时数据传输完成并准备关闭连接。这也就是QUIC协议快于TCP协议的一个主要原因。
数据包的发送
握手的函数调用栈为dial -> dialAddr -> DialAddrEarly -> DialAddrEarlyContext -> dialAddrContext -> dialContext -> newClient -> client.dial -> newClientSession -> session.run -> RunHandshake -> conn.Handshake -> clientHandshake。最终在Conn.clientHandshake函数中完成了握手的设置,之后通过clientHandshakeState.handshark函数完成了发送等工作。
在newClient函数中,通过generateConnectionID和generateConnectionIDForInitial对srcConnID和destConnID进行了生成。
在handshark函数中,调用establishKeys函数,完成了密钥的生成,之后调用sendFinished函数,将Client Hello帧写入到TLS Record层,完成握手包的发送。
数据包的接收
在session.run中的runloop中,通过select对接收通道进行监听,当收到数据包时,就会调用handlePacketImpl -> handleSinglePacket -> handleUnpackedPacket函数进行处理。
在handleUnpackedPacket函数中,如果是第一个包,就会读取其SrcConnectionID,将其设置为该连接的destination connection ID;之后对包中的帧依次进行读取,并使用parseFrame函数进行判断,并调用对应函数进行解析,最后调用handleFrame函数中调用相关函数进行处理。
在握手过程中,接收的第一个Initial包为合并包(coalesced packet),其第一个帧为ACK帧,通过parseAckFrame进行解析,使用handleAckFrame函数进行处理;第二个帧为Crypto帧,消息为Server Hello,通过parseCryptoFrame函数解析,handleCryptoFrame函数进行处理,该函数会通过session.cryptoStreamManager对密钥信息进行处理。之后第二个Handshake包中只有一个Crypto帧,消息类型为Encrypted Extensions。第三个quic包中包含了一个Stream帧,stream id为3,这个帧会通过handleStreamFrameImpl进行处理,在该函数中会将数据push到frameQueue队列中去,之后通过signalRead函数来通知数据包的到达。该帧的内容为HTTP3的SETTINGS帧。
连接建立及HTTP3数据传输
在第二个RTT中,client先通过Initial包发送ACK帧对收到的包进行确认,之后再通过Handshake包发送了CRYPTO帧和ACK帧,此CRYPTO帧的消息为Handshark protocol: Finished。最后再分别发送了Stream id为0和2的HTTP3 HEADERS帧和SETTINGS帧。
Stream id为0的HEADERS包即为http请求,该包使用了QPACK方法进行压缩,该方法与http2的HPACK类似,而根据QPACK的定义,id为2和3的stream分别为encoder stream和decoder stream,即上文中提及的两个SETTINGS帧。
之后client接收到了Handshark包,其中包含一个ACK帧。此时,1-RTT的握手过程已经结束,因此接下来收到的包的类型就变为了Short header packet,收到的第一个包的类型为HANDSHARK_DONE,说明握手完成。
最后,服务端返回了一个HTTP3的DATA帧,该帧中即包含了请求的响应数据,如下图,可以看到数据的对应文本即为html文档。

在收到数据后,客户端就发送了一个CONNECTION_CLOSE的帧关闭连接,Error code为0x100说明正常关闭,未发生错误。
QUIC协议分析-基于quic-go的更多相关文章
- 一泡尿的时间,快速读懂QUIC协议
1.TCP协议到底怎么了? 现时的互联网应用中,Web平台(准确地说是基于HTTP及其延伸协议的客户端/服务器应用)的数据传输都基于 TCP 协议. 但TCP 协议在创建连接之前需要进行三次握手(如下 ...
- 网络编程懒人入门(十):一泡尿的时间,快速读懂QUIC协议
1.TCP协议到底怎么了? 现时的互联网应用中,Web平台(准确地说是基于HTTP及其延伸协议的客户端/服务器应用)的数据传输都基于 TCP 协议. 但TCP 协议在创建连接之前需要进行三次握手(如下 ...
- QUIC协议的分析,性能测试以及在QQ会员实践
WeTest 导读 你听过HTTPS.HTTP2.0.SPDY,但是这些应用层协议都是基于可靠的传输层协议TCP来实现的.那么,基于高效的UDP协议有没有一种相对可靠的应用层协议呢? Why QUIC ...
- QUIC协议原理分析(转)
之前深入了解了一下HTTP1.1.2.0.SPDY等协议,发现HTTP层怎么优化,始终要面对TCP本身的问题.于是了解到了QUIC,这里分享一篇之前找到的有意义的文章. 原创地址:https://mp ...
- Google将向IETF标准提交QUIC协议提案
Google近期宣布,他们将向IETF提交实验性传输层网络协议QUIC的提案.此外,Google已经给出了QUIC协议优化页面加载时间的第一手数据. 自从2013年引入QUIC以来,Google一直在 ...
- 让互联网更快:新一代QUIC协议在腾讯的技术实践分享
本文来自腾讯资深研发工程师罗成在InfoQ的技术分享. 1.前言 如果:你的 App,在不需要任何修改的情况下就能提升 15% 以上的访问速度,特别是弱网络的时候能够提升 20% 以上的访问速度. 如 ...
- QUIC协议详解之Initial包的处理
从服务器发起请求开始追踪,细说数据包在 QUIC 协议中经历的每一步.大量实例代码展示,简明易懂了解 QUIC. 前言 本文介绍了在 QUIC 服务器在收到 QUIC 客户端发起的第一个 UDP 请求 ...
- Google Quic协议
0x01 Quic QUIC协议于2012年实现,2015年提交RFC草案,它是Goolge为了解决当今WEB应用常见的传输层和应用层问题而提出的,从分层结构上可以看做是TCP+TLS+HTTP2的集 ...
- [译] QUIC Wire Layout Specification - Introduction & Overview | QUIC协议标准中文翻译(1) 简介和概述
本文同步发布于: https://www.pengrl.com/p/33330/ ,转载请注明出处,谢谢. 目录 Introduction | 简介 Conventions and Definitio ...
随机推荐
- vue原理之-神奇的Object.defineProperty
vue2.0通过defineProperty进行数据双向绑定 例如:(他接受三个参数,都是必填!) var a= {} Object.defineProperty(a,"b",{ ...
- 【python爬虫】一个简单的爬取百家号文章的小爬虫
需求 用"老龄智能"在百度百家号中搜索文章,爬取文章内容和相关信息. 观察网页 红色框框的地方可以选择资讯来源,我这里选择的是百家号,因为百家号聚合了来自多个平台的新闻报道.首先看 ...
- 微博爬虫,python微博用户主页小姐姐图片内容采集爬虫
python爬虫,微博爬虫,需要知晓微博用户id号,能够通过抓取微博用户主页内容来获取用户发表的内容,时间,点赞数,转发数等数据,当然以上都是本渣渣结合网上代码抄抄改改获取的! 要抓取的微博地址:ht ...
- 跳表(SkipList)设计与实现(Java)
微信搜一搜「bigsai」关注这个有趣的程序员 文章已收录在 我的Github bigsai-algorithm 欢迎star 前言 跳表是面试常问的一种数据结构,它在很多中间件和语言中得到应用,我们 ...
- 使用IDE练习插件【廖雪峰】
使用廖雪峰大神的插件,安装过程中,一直出现问题,然后在他的Java教程下面看大家的评论也有点晕了(很多人说的是jar包,结果其实是下的依旧是zip包) 最终解决方法: 将zip包解压到同名文件夹中,再 ...
- Spring源码深度解析之Spring MVC
Spring源码深度解析之Spring MVC Spring框架提供了构建Web应用程序的全功能MVC模块.通过策略接口,Spring框架是高度可配置的,而且支持多种视图技术,例如JavaServer ...
- [数据库]000 - 🍳Sysbench 数据库压力测试工具
000 - Sysbench 数据库压力测试工具 sysbench 是一个开源的.模块化的.跨平台的多线程性能测试工具,可以用来进行CPU.内存.磁盘I/O.线程.数据库的性能测试.目前支持的数据库有 ...
- springboot文件上传问题记录
最近做项目需要开发一个通过excel表格导入数据的功能,上传接口写好调试的时候遇到几个问题,记录一下. 报错1: 15:50:57.586 [[1;33mhttp-nio-8763-exec-8 [0 ...
- Redis学习之路(四)Redis-cluster java api操作
import redis.clients.jedis.HostAndPort;import redis.clients.jedis.JedisCluster;import java.util.Hash ...
- flume的一些使用
一.第一层采集通道的编写 1.第一层采集脚本Source的选择①Source: 数据源在日志文件中! 读取日志中的数据,可以使用以下Source ExecSource: 可以执行一个linux命令,例 ...