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

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位随机数作为连接标识。当网络、端口发生改变或中断时,只要连接标识不改变,连接就不会中断。
改进拥塞控制
1.QUIC在应用层即可实现不同的拥塞控制算法,不需要改操作系统和内核。
2.单个程序的不同连接也能支持配置不同的拥塞控制。这样我们就可以给不同的用户提供更好的拥塞控制。
3.应用程序变更拥塞控制,甚至不需要停机和升级。
4.QUIC还有带宽预测,RTT监控,发送速率调整等高级算法支持。
双级别流控
TCP通过滑动窗口机制来保证可靠性以及进行流量控制。QUIC更新了其滑动窗口机制,并在Connection和Stream两个级别分别进行流控。 用公式表示就是: connection可用窗口 = stream1可用窗口 + stream2可用窗口 + streamN可用窗口

没有队头阻塞的多路复用


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配置


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 != 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