QUIC协议的原理与分析

weixin_45713621 2022-01-18 21:34:51

一、QUIC的基本原理

1 QUIC简介

QUIC(Quick UDP Internet Connection)是谷歌制定的一种基于UDP的低时延的互联网传输层协议。在2016年11月国际互联网工程任务组(IETF)召开了第一次QUIC工作组会议,受到了业界的广泛关注。这也意味着QUIC开始了它的标准化过程,成为新一代传输层协议。
QUIC很好地解决了当今传输层和应用层面临的各种需求,包括处理更多的连接,安全性,和低延迟。QUIC基于UDP传输,融合了包括TCP,TLS,HTTP/2等协议的特性。

2 QUIC原理

2.1 HTTP 历史
HTTP是一种基于TCP的协议
1991 年 HTTP(HyperText Transfer Protocol,超文本传输协议)0.9发布。协议定义了客户端发起请求、服务端响应请求的通信模式。请求报文内容只有 1 行:GET + 请求的文件路径
1996年5月,HTTP/1.0 版本发布。可以传输更多格式的内容:图像、视频、二进制文件等。更多的命令:POST命令和HEAD命令。引入了 HTTP Header(头部)的概念,让 HTTP 处理请求和响应更加灵活。新增了多字符集支持、多部分发送(multi-part type)、权限(authorization)、缓存(cache)、内容编码(content encoding)等。
1997年1月,HTTP/1.1 版本发布。引入了持久连接(persistent connection),TCP连接默认不关闭,可以被多个请求复用。引入管道机制,一个 TCP 连接,可以同时发送多个请求。客户端请求的头信息新增了Host字段。
2015 年发布 HTTP/2。数据以二进制传输。同一个连接里面发送多个请求不再需要按照顺序来。头信息压缩、服务器推送。

HTTPS
HTTPS (全称:Hyper Text Transfer Protocol over SecureSocket Layer),是以安全为目标的 HTTP 通道。HTTPS 在HTTP 的基础下加入SSL/TSL,用于安全的 HTTP 数据传输,加密的详细内容需要 SSL/TSL。

2.2 HTTPS协议的问题
从上个世纪 90 年代互联网开始兴起一直到现在,大部分的互联网流量传输只使用了几个网络协议。使用 IPv4 进行路由,使用 TCP 进行连接层面的流量控制,使用 SSL/TLS 协议实现传输安全,使用 DNS 进行域名解析,使用 HTTP 进行应用数据的传输。随着移动互联网快速发展以及物联网的逐步兴起,网络交互的场景越来越丰富,网络传输的内容也越来越庞大,用户对网络传输效率和 WEB 响应速度的要求也越来越高。由来已久的问题和矛盾就变得越来越突出: 协议历史悠久导致中间设备僵化;依赖于操作系统的实现导致协议本身僵化;建立连接的握手延迟大;队头阻塞。

2.3 QUIC的优势
相比现在广泛应用的 HTTP2+TCP+TLS 协议,QUIC的优势在于:
1.减少了 TCP 三次握手及 TLS 握手时间;
HTTPS建立连接需要三个RTT。TCP握手需要一个RTT,TLS握手需要两个RTT。
第一个RTT完成TCP三次握手。
第二个RTT,客户端client hello,将自己支持的TLS版本、加密套件发给服务端。然后服务端server hello,服务端确认支持的TLS版本和选择的加密套件,以及证书发送给客户端。
第三个RTT,客户端生成对称密钥,并用服务器的公钥进行加密,发给服务端。服务端用自己的私钥解密出对称密钥。

HTTPS建立连接需要三个RTT,显然时延太高无法接受,因此QUIC提出一个新的建立连接机制,通过1-RTT或0-RTT建立连接。
把传输和加密握手合并成一个,首次连接需要1RTT。后续的连接可以通过缓存的信息使用0-RTT传输数据,这极大减小了建立连接的时延。

 


2.改进的拥塞控制
QUIC协议使用Cubic拥塞控制算法。QUIC协议在拥塞算法上做的改进:
1.可插拔
2.单调递增的Packet Numbe
3.更多的ACK块
4.精确计算RTT时间

3.避免队头阻塞
由于http是基于TCP协议的,无法避免TCP队头阻塞的问题,即队头如果发生延迟或者丢失,队尾必须等待发送端重新发送并接收到数据。
而QUIC弃用TCP,改用UDP协议,避免了队头阻塞问题。

4.连接迁移
TCP 的连接标识是通过(源IP,源Port,目标IP,目标Port)四元组组成的,一旦其中一个参数发生变化,则需要重新创建新的 TCP 连接。在现在的生活中,手机网络连接会经常在 Wi-Fi 和 蜂窝网络中进行切换。客户端的 IP 会发生变化,需要重新建立和服务端的 TCP 连接。而QUIC 连接标识是一个64位的连接ID,使用一个唯一的连接ID 可以确保用户的 IP 变化时业务请求依然能够被继续处理,不用重新连接。

5.前向纠错
QUIC使用了FEC(前向纠错码)来恢复数据,FEC采用简单异或的方式,每发送一组数据,包括若干个数据包后,并对这些数据包依次做异或运算,最后的结果作为一个FEC包再发送出去。接收方收到一组数据后,根据数据包和FEC包即可以进行校验和纠错。

二、QUIC的配置与测试

目前有许多开源的 QUIC 实现,如Microsoft's MsQuic、Google's Chromium、nginx-quic等。本文选用基于go语言的quic-go。
1.下载安装go
quic-go目前支持Go 1.16.x 和 Go 1.17.x。
下载最新版本的1.17.6
解压下载的文件

tar -C /usr/local -xzf go1.17.6.linux-amd64.tar.gz

在/etc/profile中添加
export PATH=$PATH:/usr/local/go/bin

验证go是否安装并查看版本

 

2.下载quic-go源码​​​​​​​

 

3.编译运行example

服务端:

编译、运行服务端

go build main.go
./main


使用firefox浏览器访问https://localhost:6121/demo/tiles,可以看到协议是HTTP/3


对访问https://localhost:6121/demo/tile使用wireshark进行抓包

 

 

客户端:
编译、运行访问https://quic.rocks:4433/

go build main.go
./main -v https://quic.rocks:4433/

通过访问https://quic.rocks:4433/ 用来测试是否使用QUIC

可以看到response中:
You have successfully loaded quic.rocks using QUIC!
成功的使用QUIC访问了https://quic.rocks:4433/

 

 

三、QUIC源码分析

1.server

Config 包含 QUIC 服务器或客户端所需的所有配置数据。

type Config struct {
	// 可以协商的 QUIC 版本。
	Versions []VersionNumber
	// 连接 ID 的长度(以字节为单位)。
	ConnectionIDLength int
	// 握手完成前的Timeout
	HandshakeIdleTimeout time.Duration
	// 在没有任何网络活动的情况下的最大持续时间。
	MaxIdleTimeout time.Duration
	AcceptToken func(clientAddr net.Addr, token *Token) bool
	TokenStore TokenStore
	InitialStreamReceiveWindow uint64
	MaxStreamReceiveWindow uint64
	InitialConnectionReceiveWindow uint64
	MaxConnectionReceiveWindow uint64
	MaxIncomingStreams int64
	MaxIncomingUniStreams int64
	// 是否保持连接状态。
	KeepAlive bool
	DisablePathMTUDiscovery bool
	Negotiation packets.
	DisableVersionNegotiationPackets bool
	EnableDatagrams bool
	Tracer          logging.Tracer
}
server := http3.Server{
					Server:     &http.Server{Handler: handler, Addr: bCap},
					QuicConfig: quicConf,
				}
err = server.ListenAndServeTLS(testdata.GetCertificatePaths())

ListenAndServeTLS 监听 UDP 地址 s.Addr 并调用 s.Handler 来处理传入连接上的 HTTP/3 请求

func (s *Server) ListenAndServeTLS(certFile, keyFile string) error {
	var err error
	certs := make([]tls.Certificate, 1)
	certs[0], err = tls.LoadX509KeyPair(certFile, keyFile)
	if err != nil {
		return err
	}
	config := &tls.Config{
		Certificates: certs,
	}
	return s.serveImpl(config, nil)
}


2.client

roundTripper := &http3.RoundTripper{
		TLSClientConfig: &tls.Config{
			RootCAs:            pool,
			InsecureSkipVerify: *insecure,
			KeyLogWriter:       keyLog,
		},
		QuicConfig: &qconf,
	}
hclient := &http.Client{
		Transport: roundTripper,
	}

用一个http3.RoundTripper作为http.Client中的Transport

http.Client

type Client struct {
	CheckRedirect func(req *Request, via []*Request) error
	Jar CookieJar
	Timeout time.Duration
}

Client表示一个HTTP客户端。
如果nil,则使用默认DefaultTransport。
Transport表示HTTP事务,用于处理客户端的请求并等待服务端的响应。

RoundTripper 实现了 http.RoundTripper 接口

type RoundTripper struct {
	mutex sync.Mutex
	DisableCompression bool
	TLSClientConfig *tls.Config
	QuicConfig *quic.Config
	EnableDatagrams bool
	Dial func(network, addr string, tlsCfg *tls.Config, cfg *quic.Config) (quic.EarlySession, error)
	MaxResponseHeaderBytes int64
	clients map[string]roundTripCloser
}

RoundTrip 执行请求并返回响应
如果这是一个 0-RTT 请求,立即发送此请求。
如果不是等待握手完成

func (c *client) RoundTrip(req *http.Request) (*http.Response, error) {
	if authorityAddr("https", hostnameFromRequest(req)) != c.hostname {
		return nil, fmt.Errorf("http3 client BUG: RoundTrip called for the wrong client (expected %s, got %s)", c.hostname, req.Host)
	}

	c.dialOnce.Do(func() {
		c.handshakeErr = c.dial()
	})

	if c.handshakeErr != nil {
		return nil, c.handshakeErr
	}

	if req.Method == MethodGet0RTT {
		req.Method = http.MethodGet
	} else {	
		select {
		case <-c.session.HandshakeComplete().Done():
		case <-req.Context().Done():
			return nil, req.Context().Err()
		}
	}
	str, err := c.session.OpenStreamSync(req.Context())
	if err != nil {
		return nil, err
	}
	reqDone := make(chan struct{})
	go func() {
		select {
		case <-req.Context().Done():
			str.CancelWrite(quic.StreamErrorCode(errorRequestCanceled))
			str.CancelRead(quic.StreamErrorCode(errorRequestCanceled))
		case <-reqDone:
		}
	}()

	rsp, rerr := c.doRequest(req, str, reqDone)
	if rerr.err != nil { // if any error occurred
		close(reqDone)
		if rerr.streamErr != 0 { // if it was a stream error
			str.CancelWrite(quic.StreamErrorCode(rerr.streamErr))
		}
		if rerr.connErr != 0 { // if it was a connection error
			var reason string
			if rerr.err != nil {
				reason = rerr.err.Error()
			}
			c.session.CloseWithError(quic.ApplicationErrorCode(rerr.connErr), reason)
		}
	}
	return rsp, rerr.err
}

作者:NP452 

...全文
373 回复 打赏 收藏 转发到动态 举报
写回复
用AI写文章
回复
切换为时间正序
请发表友善的回复…
发表回复

571

社区成员

发帖
与我相关
我的任务
社区描述
软件工程教学新范式,强化专项技能训练+基于项目的学习PBL。Git仓库:https://gitee.com/mengning997/se
软件工程 高校
社区管理员
  • 码农孟宁
加入社区
  • 近7日
  • 近30日
  • 至今

试试用AI创作助手写篇文章吧