基于quic-go的quic测试和示例代码分析

断桨 2022-01-18 23:54:09

1. QUIC协议概述

QUIC是由谷歌设计的一种基于UDP的传输层网络协议,并且已经成为IETF草案。HTTP/3就是基于QUIC协议的。QUIC只是一个协议,可以通过多种方法来实现,目前常见的实现有Google的quiche,微软的msquic,mozilla的neqo,以及基于go语言的quic-go等。 由于go语言的简洁性以及编译的便捷性,本文将选用quic-go进行quic协议的分析,该库是完全基于go语言实现,可以用于构建客户端或服务端。

image.png

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了。这已最大限度的减少握手延迟带来的影响。这个特性在连接延迟较大的移动网络上有较好的性能提升。

    image.png

  • 连接迁移
    TCP下一个连接是以四元组标识的,即(SrcIp,SrcPort,DstIp,DstPort)。而QUIC连接是以客户端产生的一个64位随机数作为连接标识。当网络、端口发生改变或中断时,只要连接标识不改变,连接就不会中断。

  • 改进拥塞控制
    1.QUIC在应用层即可实现不同的拥塞控制算法,不需要改操作系统和内核。
    2.单个程序的不同连接也能支持配置不同的拥塞控制。这样我们就可以给不同的用户提供更好的拥塞控制。
    3.应用程序变更拥塞控制,甚至不需要停机和升级。
    4.QUIC还有带宽预测,RTT监控,发送速率调整等高级算法支持。

  • 双级别流控
    TCP通过滑动窗口机制来保证可靠性以及进行流量控制。QUIC更新了其滑动窗口机制,并在Connection和Stream两个级别分别进行流控。 用公式表示就是: connection可用窗口 = stream1可用窗口 + stream2可用窗口 + streamN可用窗口

    image.png

  • 没有队头阻塞的多路复用

image.png


SDPY和HTTP2协议的多路复用,是让所有请求在一个TCP连接上传输。前面说过,TCP协议有队头阻塞问题,如果某个资源的某个包丢失了,由于TCP是保证时序的,就会在接收端形成队头阻塞。TCP协议无法区分各个资源的包是否关联,因此会停止处理所有资源,直到丢包恢复。

image.png


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

image.png


./main -qlog -v -tcp运行服务器

image.png

客户端

cd example/client打开客户端文件夹
go build main.go编译
./main -v -insecure -keylog ssl.log [https://quic.rocks:4433/](https://quic.rocks:4433/)客户端访问

image.png

浏览器访问

配置火狐浏览器

image.png


浏览器访问[https://localhost:6121/demo/tile](https://localhost:6121/demo/tile)

image.png

抓包

wireshark配置

image.png


使用wireshark对loopback抓包,可以看到抓包信息与服务器获取信息是相匹配的。

image.png

3. QUIC示例代码分析

echo

echo程序实现了一个简单的回显功能。

image.png

服务器端

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 != nil {
        return err
    }
    stream, err := sess.AcceptStream(context.Background())
    if err != nil {
        panic(err)
    }
    // Echo through the loggingWriter
    _, err = io.Copy(loggingWriter{stream}, stream)
    return err
}

ListenAddr()方法创建一个监听给定地址的QUIC服务器,其中的tls.config参数不能为nil,必须包含证书配置。这里使用了generateTLSConfig()方法配置

func ListenAddr(addr string, tlsConf *tls.Config, config *Config) (Listener, error) {
    return listenAddr(addr, tlsConf, config, false)
}

generateTLSConfig()为服务器设置简单的TLS配置,这里使用的是rsa算法,对rsa公私钥的配置。

QUIC 实现 0RTT 的前提是 ServerConfig 这个内容签名和校验都没有问题。由于 ServerConfig 涉及到 RSA 签名或者 ECDSA 签名,非常消耗我们的 CPU 资源。根据之前的测试数据,RSA 私钥签名计算会降低 90% 的性能。

func generateTLSConfig() *tls.Config {
    key, err := rsa.GenerateKey(rand.Reader, 1024)
    if err != nil {
        panic(err)
    }
    template := x509.Certificate{SerialNumber: big.NewInt(1)}
    certDER, err := x509.CreateCertificate(rand.Reader, &template, &template, &key.PublicKey, key)
    if err != nil {
        panic(err)
    }
    keyPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)})
    certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER})

    tlsCert, err := tls.X509KeyPair(certPEM, keyPEM)
    if err != nil {
        panic(err)
    }
    return &tls.Config{
        Certificates: []tls.Certificate{tlsCert},
        NextProtos:   []string{"quic-echo-example"},
    }
}

ListenAddr()方法返回Listener是传入QUIC连接的侦听器,Accept返回新的sessions,它应该在循环中调用。

type Listener interface {
    // Close the server. All active sessions will be closed.
    Close() error
    // Addr returns the local network addr that the server is listening on.
    Addr() net.Addr
    // Accept returns new sessions. It should be called in a loop.
    Accept(context.Context) (Session, error)
}

Accept()返回的Session是两个实体间的QUIC连接,AcceptStream ()返回对等方打开的下一个流,阻塞直到有一个可用为止。

type Session interface {
    // AcceptStream returns the next stream opened by the peer, blocking until one is available.
    // If the session was closed due to a timeout, the error satisfies
    // the net.Error interface, and Timeout() will be true.
    AcceptStream(context.Context) (Stream, error)
    // AcceptUniStream returns the next unidirectional stream opened by the peer, blocking until one is available.
    // If the session was closed due to a timeout, the error satisfies
    // the net.Error interface, and Timeout() will be true.
    AcceptUniStream(context.Context) (ReceiveStream, error)
    // OpenStream opens a new bidirectional QUIC stream.
    // There is no signaling to the peer about new streams:
    // The peer can only accept the stream after data has been sent on the stream.
    // If the error is non-nil, it satisfies the net.Error interface.
    // When reaching the peer's stream limit, err.Temporary() will be true.
    // If the session was closed due to a timeout, Timeout() will be true.
    OpenStream() (Stream, error)
    // OpenStreamSync opens a new bidirectional QUIC stream.
    // It blocks until a new stream can be opened.
    // If the error is non-nil, it satisfies the net.Error interface.
    // If the session was closed due to a timeout, Timeout() will be true.
    OpenStreamSync(context.Context) (Stream, error)
    // OpenUniStream opens a new outgoing unidirectional QUIC stream.
    // If the error is non-nil, it satisfies the net.Error interface.
    // When reaching the peer's stream limit, Temporary() will be true.
    // If the session was closed due to a timeout, Timeout() will be true.
    OpenUniStream() (SendStream, error)
    // OpenUniStreamSync opens a new outgoing unidirectional QUIC stream.
    // It blocks until a new stream can be opened.
    // If the error is non-nil, it satisfies the net.Error interface.
    // If the session was closed due to a timeout, Timeout() will be true.
    OpenUniStreamSync(context.Context) (SendStream, error)
    // LocalAddr returns the local address.
    LocalAddr() net.Addr
    // RemoteAddr returns the address of the peer.
    RemoteAddr() net.Addr
    // CloseWithError closes the connection with an error.
    // The error string will be sent to the peer.
    CloseWithError(ApplicationErrorCode, string) error
    // The context is cancelled when the session is closed.
    // Warning: This API should not be considered stable and might change soon.
    Context() context.Context
    // ConnectionState returns basic details about the QUIC connection.
    // It blocks until the handshake completes.
    // Warning: This API should not be considered stable and might change soon.
    ConnectionState() ConnectionState

    // SendMessage sends a message as a datagram.
    // See https://datatracker.ietf.org/doc/draft-pauly-quic-datagram/.
    SendMessage([]byte) error
    // ReceiveMessage gets a message received in a datagram.
    // See https://datatracker.ietf.org/doc/draft-pauly-quic-datagram/.
    ReceiveMessage() ([]byte, error)
}

io.Copy(loggingWriter{stream}, stream)对这个流进行IO操作,即可实现回显效果。

客户端

func clientMain() error {
    tlsConf := &tls.Config{
        InsecureSkipVerify: true,
        NextProtos:         []string{"quic-echo-example"},
    }
    session, err := quic.DialAddr(addr, tlsConf, nil)
    if err != nil {
        return err
    }

    stream, err := session.OpenStreamSync(context.Background())
    if err != nil {
        return err
    }

    fmt.Printf("Client: Sending '%s'\n", message)
    _, err = stream.Write([]byte(message))
    if err != nil {
        return err
    }

    buf := make([]byte, len(message))
    _, err = io.ReadFull(stream, buf)
    if err != nil {
        return err
    }
    fmt.Printf("Client: Got '%s'\n", buf)

    return nil
}

客户端&tls.Config配置tls,然后使用quic.DialAddr(addr, tlsConf, nil)方法。DialAddr 与服务器建立新的 QUIC 连接,它使用一个新的 UDP 连接,并在 QUIC 会话关闭时关闭此连接。addr配置地址,此处是localhost:4242

func DialAddr(
    addr string,
    tlsConf *tls.Config,
    config *Config,
) (Session, error) {
    return DialAddrContext(context.Background(), addr, tlsConf, config)
}

建立新的连接后,获得会话Session,使用session.OpenStreamSync(context.Background()),OpenStreamSync 打开一个新的双向 QUIC stream。它会阻塞,直到可以打开一个新的stream。然后对流进行读写操作,即可实现回显功能。

    fmt.Printf("Client: Sending '%s'\n", message)
    _, err = stream.Write([]byte(message))
    if err != nil {
        return err
    }

    buf := make([]byte, len(message))
    _, err = io.ReadFull(stream, buf)
    if err != nil {
        return err
    }
    fmt.Printf("Client: Got '%s'\n", buf)

作者:NP 332

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

571

社区成员

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

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