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