QUIC协议测试与源代码分析

ustczb 2022-01-17 20:15:04

一、QUIC协议的产生背景和基本原理

QUIC 的全称是:Quick UDP Internet Connections;是一种新的默认加密的互联网通信协议,

目的是为了加速HTTP通信,同时使其变得更加安全,其最终目的是在web上代替TCP和TLS协议。QUIC 协议也是整合了 TCP 协议的可靠性和 UDP 协议的速度和效率。

为了解决TCP在实际应用中存在的问题,QUIC协议应运而生。QUIC是基于 UDP实现的一种协议,因为 UDP 本身没有连接的概念,不需要三次握手,优化了连接建立的握手延迟。同时QUIC在应用程序层面实现了 TCP 的可靠性,TLS 的安全性和 HTTP2 的并发性,只需要用户端和服务端的应用程序支持 QUIC 协议,完全避开了操作系统和中间设备的限制。

QUIC是一种面向连接的协议,它在客户机和服务器之间创建有状态的交互。  

端点通过交换QUIC数据包在QUIC中进行通信。 大多数数据包包含帧,这些帧在端点之间携带控制信息和应用程序数据。 QUIC验证每个包的完整性,并在实际情况下尽可能多地加密每个包。 QUIC报文以UDP数据报的形式携带,以便于在现有系统和网络中更好地部署。  

应用协议通过流在QUIC连接上交换信息,流是字节的有序序列。 可以创建两种类型的流:双向流,允许两个端点发送数据; 还有单向流,它允许单个端点发送数据。 基于信用的方案用于限制流的创建和可发送的数据量。  

QUIC连接并不严格绑定到单个网络路径。 连接迁移使用连接标识符允许连接传输到新的网络路径。 只有客户端能够在这个版本的QUIC中迁移。 这种设计还允许在网络拓扑或地址映射发生变化(比如NAT重新绑定可能引起的变化)后继续连接。  

建立连接后,会提供多个终止连接的选项。 应用程序可以管理一个安全的关闭,端点可以协商一个超时时间,错误可以导致立即断开连接,并且在一个端点失去状态后提供了一种无状态机制来终止连接。  

QUIC的优势

1.连接建立延时低:

通信双方发起通信连接时,第一个数据包便可以携带有效的业务数据(0RTT)

2.改进的拥塞控制:

TCP 的拥塞控制实际上包含了四个算法:慢启动,拥塞避免,快速重传,快速恢复。

QUIC 协议当前默认使用了 TCP 协议的 Cubic 拥塞控制算法,同时也支持 CubicBytes, Reno, RenoBytes, BBR, PCC 等拥塞控制算法

3.QUIC的NACK比TCP的延迟确认机制高效:

TCP 为了保证可靠性,使用了基于字节序号的 Sequence Number 及 Ack 来确认消息的有序到达。

QUIC 同样是一个可靠的协议,它使用 Packet Number 代替了 TCP 的 sequence number,并且每个 Packet Number 都严格递增,也就是说就算 Packet N 丢失了,重传的 Packet N 的 Packet Number 已经不是 N,而是一个比 N 大的值。而 TCP 呢,重传 segment 的 sequence number 和原始的 segment 的 Sequence Number 保持不变,也正是由于这个特性,引入了 Tcp 重传的歧义问题。

4.更安全的传输协议:

CP 协议头部没有经过任何加密和认证,所以在传输过程中很容易被中间网络设备篡改,注入和窃听。比如修改序列号、滑动窗口。这些行为有可能是出于性能优化,也有可能是主动攻击。

但是 QUIC 的 packet 可以说是武装到了牙齿。除了个别报文比如 PUBLIC_RESET 和 CHLO,所有报文头部都是经过认证的,报文 Body 都是经过加密的。

这样只要对 QUIC 报文任何修改,接收端都能够及时发现,有效地降低了安全风险。

如下图所示,红色部分是 Stream Frame 的报文头部,有认证。绿色部分是报文内容,全部经过加密。

 二、QUIC的环境配置和部署

本文实现的是quic-go,源代码采用自https://github.com/lucas-clemente/quic-go,在一台ubuntu系统中开启两个终端模拟客户端与服务端的交互过程

首先在ubuntu系统中安装最新版本的golang编译器

安装完成后使用go version查看go版本

接着使用git clone https://github.com/lucas-clemente/quic-go.git下载库

 新建终端,定义为服务端,找到本地quic-go文件并进入example文件夹

 使用go build main.go,对main.go文件进行编译,生成名为main的可执行文件

在服务端运行./main命令运行,等待客户端发送消息。

新建终端,定义为客户端,进入quic-go/example/client目录,开始编译go build main.go,接着使用./main -v -insecure -keylog ssl.log 即可访问支持quic协议的网站。

在client下运行./main https://localhost:6121/demo/tile,可以看到客户端成功访问了服务端并返回了一个png图片,错误代码为0x100,表示未发生错误

 在服务端显示此次通信的具体信息

 在firefox中打开about:config,搜索HTTP3,将值设为True以打开HTTP3的实验特性。在地址栏中输入https://localhost:6121/demo/tile,可以查看图片的具体信息。

 使用wireshark对QUIC协议进行抓包,使用前要确保wireshark更新到最新版本,以便能够识别QUIC协议,将过滤器设置为udp.port==6121,进行捕获。

可以看到捕获到的Connection ID与终端使用的链接ID相同,均为38598ebf9a75f4827304c696ec748e。以及一些其他的链接信息,如第一个包的类型为Initial,进行了0RTT的初始化。

再次对客户端进行测试,访问https://quic.rocks:4433/,可以看到网页的html内容,“You have successfully loaded quic.rocks using QUIC!”,以及网页的相关信息,Status:”200 OK”  StatusCode:200  状态码为200表示访问成功。Proto:”HTTP/3”表示协议为http3.0。错误代码为0x100,表示未发生错误。

浏览器地址栏中输入https://quic.rocks:4433/,查看网页的具体内容。

 

三、QUIC源代码分析

整个过程的时序图如下:

 

发送数据包

在client/main.go代码中,通过http3.RoundTripper实现roundTripper接口。从而能够建立http客户端,发送http请求,查看回复信息。

func main() {
	verbose := flag.Bool("v", false, "verbose")
	quiet := flag.Bool("q", false, "don't print the data")
	keyLogFile := flag.String("keylog", "", "key log file")
	insecure := flag.Bool("insecure", false, "skip certificate verification")
	enableQlog := flag.Bool("qlog", false, "output a qlog (in the same directory)")
	flag.Parse()
	urls := flag.Args()

	logger := utils.DefaultLogger

	if *verbose {
		logger.SetLogLevel(utils.LogLevelDebug)
	} else {
		logger.SetLogLevel(utils.LogLevelInfo)
	}
	logger.SetLogTimeFormat("")

	var keyLog io.Writer
	if len(*keyLogFile) > 0 {
		f, err := os.Create(*keyLogFile)
		if err != nil {
			log.Fatal(err)
		}
		defer f.Close()
		keyLog = f
	}

	pool, err := x509.SystemCertPool()
	if err != nil {
		log.Fatal(err)
	}
	testdata.AddRootCA(pool)

	var qconf quic.Config
	if *enableQlog {
		qconf.Versions = []protocol.VersionNumber{protocol.VersionDraft29}
		qconf.Tracer = qlog.NewTracer(func(_ logging.Perspective, connID []byte) io.WriteCloser {
			filename := fmt.Sprintf("client_%x.qlog", connID)
			f, err := os.Create(filename)
			if err != nil {
				log.Fatal(err)
			}
			log.Printf("Creating qlog file %s.\n", filename)
			return utils.NewBufferedWriteCloser(bufio.NewWriter(f), f)
		})
	}
	roundTripper := &http3.RoundTripper{
		TLSClientConfig: &tls.Config{
			RootCAs:            pool,
			InsecureSkipVerify: *insecure,
			KeyLogWriter:       keyLog,
		},
		QuicConfig: &qconf,
	}
	defer roundTripper.Close()
	hclient := &http.Client{
		Transport: roundTripper,
	}

	var wg sync.WaitGroup
	wg.Add(len(urls))
	for _, addr := range urls {
		logger.Infof("GET %s", addr)
		go func(addr string) {
			rsp, err := hclient.Get(addr)
			if err != nil {
				log.Fatal(err)
			}
			logger.Infof("Got response for %s: %#v", addr, rsp)

			body := &bytes.Buffer{}
			_, err = io.Copy(body, rsp.Body)
			if err != nil {
				log.Fatal(err)
			}
			if *quiet {
				logger.Infof("Response Body: %d bytes", body.Len())
			} else {
				logger.Infof("Response Body:")
				logger.Infof("%s", body.Bytes())
			}
			wg.Done()
		}(addr)
	}
	wg.Wait()
}

http3.RoundTripper将请求交给了RoundTripOpt函数进行处理,RoundTripOpt函数在roundtrip.go文件内。

func (r *RoundTripper) RoundTripOpt(req *http.Request, opt RoundTripOpt) (*http.Response, error) {
	if req.URL == nil {
		closeRequestBody(req)
		return nil, errors.New("http3: nil Request.URL")
	}
	if req.URL.Host == "" {
		closeRequestBody(req)
		return nil, errors.New("http3: no Host in request URL")
	}
	if req.Header == nil {
		closeRequestBody(req)
		return nil, errors.New("http3: nil Request.Header")
	}

	if req.URL.Scheme == "https" {
		for k, vv := range req.Header {
			if !httpguts.ValidHeaderFieldName(k) {
				return nil, fmt.Errorf("http3: invalid http header field name %q", k)
			}
			for _, v := range vv {
				if !httpguts.ValidHeaderFieldValue(v) {
					return nil, fmt.Errorf("http3: invalid http header field value %q for key %v", v, k)
				}
			}
		}
	} else if !opt.SkipSchemeCheck {
		closeRequestBody(req)
		return nil, fmt.Errorf("http3: unsupported protocol scheme: %s", req.URL.Scheme)
	}

	if req.Method != "" && !validMethod(req.Method) {
		closeRequestBody(req)
		return nil, fmt.Errorf("http3: invalid method %q", req.Method)
	}

	hostname := authorityAddr("https", hostnameFromRequest(req))
	cl, err := r.getClient(hostname, opt.OnlyCachedConn)
	if err != nil {
		return nil, err
	}
	return cl.RoundTrip(req)
}

函数先对请求URL的合法性进行判断,若不合法,则根据不同情况,返回"http3: nil Request.URL","http3: no Host in request URL","http3: nil Request.Header"。若合法,则通过getClient函数来获取客户端,cl, err := r.getClient(hostname, opt.OnlyCachedConn)。

当第一次发送请求时会调用dial函数进行握手,Dial使用net.PacketConn建立一个新的QUIC连接到服务器。 如果  PacketConn满足OOBCapablePacketConn接口(作为一个网络。 UDPConn does), ECN和包信息支持将被启用。

func Dial(
	pconn net.PacketConn,
	remoteAddr net.Addr,
	host string,
	tlsConf *tls.Config,
	config *Config,
) (Session, error) {
	return dialContext(context.Background(), pconn, remoteAddr, host, tlsConf, config, false, false)
}

DialAddr与服务器建立新的QUIC连接,它使用一个新的UDP连接,并在QUIC会话关闭时关闭该连接。

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

DialAddr只是对DialAddrContext进行了封装,下面看DialAddrContext函数:

func DialAddrContext(
	ctx context.Context,
	addr string,
	tlsConf *tls.Config,
	config *Config,
) (Session, error) {
	return dialAddrContext(ctx, addr, tlsConf, config, false)
}

DialAddrContext使用提供的上下文建立一个新的QUIC连接到服务器。 

相比于DialAddr,唯一的不同在于多加了一个context.Context类型。上下文context.Context是用来设置截止日期、同步信号,传递请求相关值的结构体。可以理解为一个同步信号,即对信号同步以减少资源浪费。常用的context.Background()返回一个预定义的类型。

DialAddrContext依然是一个封装函数,下面看dialAddrContext函数:

func dialAddrContext(
	ctx context.Context,
	addr string,
	tlsConf *tls.Config,
	config *Config,
	use0RTT bool,
) (quicSession, error) {
	udpAddr, err := net.ResolveUDPAddr("udp", addr)
	if err != nil {
		return nil, err
	}
	udpConn, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.IPv4zero, Port: 0})
	if err != nil {
		return nil, err
	}
	return dialContext(ctx, udpConn, udpAddr, addr, tlsConf, config, use0RTT, true)
}

dialAddrContext调用net.ResolveUDPAddr("udp", addr),接着在UDP上听net.ListenUDP("udp", &net.UDPAddr{IP: net.IPv4zero, Port: 0}),最后根据返回的结果调用dialContext函数统一处理。

Dial函数和dialAddrContext函数都会调用dialContext函数:

unc dialContext(
	ctx context.Context,
	pconn net.PacketConn,
	remoteAddr net.Addr,
	host string,
	tlsConf *tls.Config,
	config *Config,
	use0RTT bool,
	createdPacketConn bool,
) (quicSession, error) {
	if tlsConf == nil {
		return nil, errors.New("quic: tls.Config not set")
	}
	if err := validateConfig(config); err != nil {
		return nil, err
	}
	config = populateClientConfig(config, createdPacketConn)
	packetHandlers, err := getMultiplexer().AddConn(pconn, config.ConnectionIDLength, config.StatelessResetKey, config.Tracer)
	if err != nil {
		return nil, err
	}
	c, err := newClient(pconn, remoteAddr, config, tlsConf, host, use0RTT, createdPacketConn)
	if err != nil {
		return nil, err
	}
	c.packetHandlers = packetHandlers

	c.tracingID = nextSessionTracingID()
	if c.config.Tracer != nil {
		c.tracer = c.config.Tracer.TracerForConnection(
			context.WithValue(ctx, SessionTracingKey, c.tracingID),
			protocol.PerspectiveClient,
			c.destConnID,
		)
	}
	if c.tracer != nil {
		c.tracer.StartedConnection(c.conn.LocalAddr(), c.conn.RemoteAddr(), c.srcConnID, c.destConnID)
	}
	if err := c.dial(ctx); err != nil {
		return nil, err
	}
	return c.session, nil
}

getMultiplexer建立一个多路复用。接着返回一个新的客户端结构newClient。

func newClient(
	hostname string,
	tlsConf *tls.Config,
	opts *roundTripperOpts,
	quicConfig *quic.Config,
	dialer func(network, addr string, tlsCfg *tls.Config, cfg *quic.Config) (quic.EarlySession, error),
) (*client, error) {
	if quicConfig == nil {
		quicConfig = defaultQuicConfig.Clone()
	} else if len(quicConfig.Versions) == 0 {
		quicConfig = quicConfig.Clone()
		quicConfig.Versions = []quic.VersionNumber{defaultQuicConfig.Versions[0]}
	}
	if len(quicConfig.Versions) != 1 {
		return nil, errors.New("can only use a single QUIC version for dialing a HTTP/3 connection")
	}
	quicConfig.MaxIncomingStreams = -1 // don't allow any bidirectional streams
	quicConfig.EnableDatagrams = opts.EnableDatagram
	logger := utils.DefaultLogger.WithPrefix("h3 client")

	if tlsConf == nil {
		tlsConf = &tls.Config{}
	} else {
		tlsConf = tlsConf.Clone()
	}
	// Replace existing ALPNs by H3
	tlsConf.NextProtos = []string{versionToALPN(quicConfig.Versions[0])}

	return &client{
		hostname:      authorityAddr("https", hostname),
		tlsConf:       tlsConf,
		requestWriter: newRequestWriter(logger),
		decoder:       qpack.NewDecoder(func(hf qpack.HeaderField) {}),
		config:        quicConfig,
		opts:          opts,
		dialer:        dialer,
		logger:        logger,
	}, nil
}

在newClient函数中,通过generateConnectionID和generateConnectionIDForInitial对srcConnID和destConnID进行了生成。

最后在RunHandshake函数中,完成握手包的发送。并能够查看握手包的传输时间。

func (h *cryptoSetup) RunHandshake() {
	// Handle errors that might occur when HandleData() is called.
	handshakeComplete := make(chan struct{})
	handshakeErrChan := make(chan error, 1)
	go func() {
		defer close(h.handshakeDone)
		if err := h.conn.Handshake(); err != nil {
			handshakeErrChan <- err
			return
		}
		close(handshakeComplete)
	}()

	select {
	case <-handshakeComplete: // return when the handshake is done
		h.mutex.Lock()
		h.handshakeCompleteTime = time.Now()
		h.mutex.Unlock()
		h.runner.OnHandshakeComplete()
	case <-h.closeChan:
		// wait until the Handshake() go routine has returned
		<-h.handshakeDone
	case alert := <-h.alertChan:
		handshakeErr := <-handshakeErrChan
		h.onError(alert, handshakeErr.Error())
	}
}

 

 

接收数据包

在session.run中的runloop中,通过select对接收通道进行监听,当收到数据包时,就会调用handlePacketImpl ,handleSinglePacket , handleUnpackedPacket函数进行处理。

func (s *session) run() error {
	defer s.ctxCancel()

	s.timer = utils.NewTimer()

	go s.cryptoStreamHandler.RunHandshake()
	go func() {
		if err := s.sendQueue.Run(); err != nil {
			s.destroyImpl(err)
		}
	}()

	if s.perspective == protocol.PerspectiveClient {
		select {
		case zeroRTTParams := <-s.clientHelloWritten:
			s.scheduleSending()
			if zeroRTTParams != nil {
				s.restoreTransportParameters(zeroRTTParams)
				close(s.earlySessionReadyChan)
			}
		case closeErr := <-s.closeChan:
			// put the close error back into the channel, so that the run loop can receive it
			s.closeChan <- closeErr
		}
	}

	var (
		closeErr           closeError
		sendQueueAvailable <-chan struct{}
	)

runLoop:
	for {
		// Close immediately if requested
		select {
		case closeErr = <-s.closeChan:
			break runLoop
		case <-s.handshakeCompleteChan:
			s.handleHandshakeComplete()
		default:
		}

		s.maybeResetTimer()

		var processedUndecryptablePacket bool
		if len(s.undecryptablePacketsToProcess) > 0 {
			queue := s.undecryptablePacketsToProcess
			s.undecryptablePacketsToProcess = nil
			for _, p := range queue {
				if processed := s.handlePacketImpl(p); processed {
					processedUndecryptablePacket = true
				}
				// Don't set timers and send packets if the packet made us close the session.
				select {
				case closeErr = <-s.closeChan:
					break runLoop
				default:
				}
			}
		}
		// If we processed any undecryptable packets, jump to the resetting of the timers directly.
		if !processedUndecryptablePacket {
			select {
			case closeErr = <-s.closeChan:
				break runLoop
			case <-s.timer.Chan():
				s.timer.SetRead()
				// We do all the interesting stuff after the switch statement, so
				// nothing to see here.
			case <-s.sendingScheduled:
				// We do all the interesting stuff after the switch statement, so
				// nothing to see here.
			case <-sendQueueAvailable:
			case firstPacket := <-s.receivedPackets:
				wasProcessed := s.handlePacketImpl(firstPacket)
				// Don't set timers and send packets if the packet made us close the session.
				select {
				case closeErr = <-s.closeChan:
					break runLoop
				default:
				}
				if s.handshakeComplete {
					// Now process all packets in the receivedPackets channel.
					// Limit the number of packets to the length of the receivedPackets channel,
					// so we eventually get a chance to send out an ACK when receiving a lot of packets.
					numPackets := len(s.receivedPackets)
				receiveLoop:
					for i := 0; i < numPackets; i++ {
						select {
						case p := <-s.receivedPackets:
							if processed := s.handlePacketImpl(p); processed {
								wasProcessed = true
							}
							select {
							case closeErr = <-s.closeChan:
								break runLoop
							default:
							}
						default:
							break receiveLoop
						}
					}
				}
				// Only reset the timers if this packet was actually processed.
				// This avoids modifying any state when handling undecryptable packets,
				// which could be injected by an attacker.
				if !wasProcessed {
					continue
				}
			case <-s.handshakeCompleteChan:
				s.handleHandshakeComplete()
			}
		}

		now := time.Now()
		if timeout := s.sentPacketHandler.GetLossDetectionTimeout(); !timeout.IsZero() && timeout.Before(now) {
			// This could cause packets to be retransmitted.
			// Check it before trying to send packets.
			if err := s.sentPacketHandler.OnLossDetectionTimeout(); err != nil {
				s.closeLocal(err)
			}
		}

		if keepAliveTime := s.nextKeepAliveTime(); !keepAliveTime.IsZero() && !now.Before(keepAliveTime) {
			// send a PING frame since there is no activity in the session
			s.logger.Debugf("Sending a keep-alive PING to keep the connection alive.")
			s.framer.QueueControlFrame(&wire.PingFrame{})
			s.keepAlivePingSent = true
		} else if !s.handshakeComplete && now.Sub(s.sessionCreationTime) >= s.config.handshakeTimeout() {
			s.destroyImpl(qerr.ErrHandshakeTimeout)
			continue
		} else {
			idleTimeoutStartTime := s.idleTimeoutStartTime()
			if (!s.handshakeComplete && now.Sub(idleTimeoutStartTime) >= s.config.HandshakeIdleTimeout) ||
				(s.handshakeComplete && now.Sub(idleTimeoutStartTime) >= s.idleTimeout) {
				s.destroyImpl(qerr.ErrIdleTimeout)
				continue
			}
		}

		if s.sendQueue.WouldBlock() {
			// The send queue is still busy sending out packets.
			// Wait until there's space to enqueue new packets.
			sendQueueAvailable = s.sendQueue.Available()
			continue
		}
		if err := s.sendPackets(); err != nil {
			s.closeLocal(err)
		}
		if s.sendQueue.WouldBlock() {
			sendQueueAvailable = s.sendQueue.Available()
		} else {
			sendQueueAvailable = nil
		}
	}

	s.handleCloseError(&closeErr)
	if e := (&errCloseForRecreating{}); !errors.As(closeErr.err, &e) && s.tracer != nil {
		s.tracer.Close()
	}
	s.logger.Infof("Connection %s closed.", s.logID)
	s.cryptoStreamHandler.Close()
	s.sendQueue.Close()
	s.timer.Stop()
	return closeErr.err
}

handleUnpackedPacket函数读取数据包的内容

func (s *session) handleUnpackedPacket(
	packet *unpackedPacket,
	ecn protocol.ECN,
	rcvTime time.Time,
	packetSize protocol.ByteCount, // only for logging
) error {
	if len(packet.data) == 0 {
		return &qerr.TransportError{
			ErrorCode:    qerr.ProtocolViolation,
			ErrorMessage: "empty packet",
		}
	}

	if !s.receivedFirstPacket {
		s.receivedFirstPacket = true
		if !s.versionNegotiated && s.tracer != nil {
			var clientVersions, serverVersions []protocol.VersionNumber
			switch s.perspective {
			case protocol.PerspectiveClient:
				clientVersions = s.config.Versions
			case protocol.PerspectiveServer:
				serverVersions = s.config.Versions
			}
			s.tracer.NegotiatedVersion(s.version, clientVersions, serverVersions)
		}
		// The server can change the source connection ID with the first Handshake packet.
		if s.perspective == protocol.PerspectiveClient && packet.hdr.IsLongHeader && !packet.hdr.SrcConnectionID.Equal(s.handshakeDestConnID) {
			cid := packet.hdr.SrcConnectionID
			s.logger.Debugf("Received first packet. Switching destination connection ID to: %s", cid)
			s.handshakeDestConnID = cid
			s.connIDManager.ChangeInitialConnID(cid)
		}
		// We create the session as soon as we receive the first packet from the client.
		// We do that before authenticating the packet.
		// That means that if the source connection ID was corrupted,
		// we might have create a session with an incorrect source connection ID.
		// Once we authenticate the first packet, we need to update it.
		if s.perspective == protocol.PerspectiveServer {
			if !packet.hdr.SrcConnectionID.Equal(s.handshakeDestConnID) {
				s.handshakeDestConnID = packet.hdr.SrcConnectionID
				s.connIDManager.ChangeInitialConnID(packet.hdr.SrcConnectionID)
			}
			if s.tracer != nil {
				s.tracer.StartedConnection(
					s.conn.LocalAddr(),
					s.conn.RemoteAddr(),
					packet.hdr.SrcConnectionID,
					packet.hdr.DestConnectionID,
				)
			}
		}
	}

	s.lastPacketReceivedTime = rcvTime
	s.firstAckElicitingPacketAfterIdleSentTime = time.Time{}
	s.keepAlivePingSent = false

	// Only used for tracing.
	// If we're not tracing, this slice will always remain empty.
	var frames []wire.Frame
	r := bytes.NewReader(packet.data)
	var isAckEliciting bool
	for {
		frame, err := s.frameParser.ParseNext(r, packet.encryptionLevel)
		if err != nil {
			return err
		}
		if frame == nil {
			break
		}
		if ackhandler.IsFrameAckEliciting(frame) {
			isAckEliciting = true
		}
		// Only process frames now if we're not logging.
		// If we're logging, we need to make sure that the packet_received event is logged first.
		if s.tracer == nil {
			if err := s.handleFrame(frame, packet.encryptionLevel, packet.hdr.DestConnectionID); err != nil {
				return err
			}
		} else {
			frames = append(frames, frame)
		}
	}

	if s.tracer != nil {
		fs := make([]logging.Frame, len(frames))
		for i, frame := range frames {
			fs[i] = logutils.ConvertFrame(frame)
		}
		s.tracer.ReceivedPacket(packet.hdr, packetSize, fs)
		for _, frame := range frames {
			if err := s.handleFrame(frame, packet.encryptionLevel, packet.hdr.DestConnectionID); err != nil {
				return err
			}
		}
	}

	return s.receivedPacketHandler.ReceivedPacket(packet.packetNumber, ecn, packet.encryptionLevel, rcvTime, isAckEliciting)
}

在handleUnpackedPacket函数中,如果是第一个包,就会读取其SrcConnectionID,将其设置为该连接的destination connection ID;之后对包中的帧依次进行读取,并使用parseFrame函数进行判断,并调用对应函数进行解析,最后调用handleFrame函数中调用相关函数进行处理。

Listen监听给定网络上的QUIC连接。 如果 PacketConn满足OOBCapablePacketConn接口(作为一个网络。 UDPConn   does), ECN和包信息支持将被启用。

func Listen(conn net.PacketConn, tlsConf *tls.Config, config *Config) (Listener, error) {
	return listen(conn, tlsConf, config, false)
}

func listen(conn net.PacketConn, tlsConf *tls.Config, config *Config, acceptEarly bool) (*baseServer, error) {
	if tlsConf == nil {
		return nil, errors.New("quic: tls.Config not set")
	}
	if err := validateConfig(config); err != nil {
		return nil, err
	}
	config = populateServerConfig(config)
	for _, v := range config.Versions {
		if !protocol.IsValidVersion(v) {
			return nil, fmt.Errorf("%s is not a valid QUIC version", v)
		}
	}

	sessionHandler, err := getMultiplexer().AddConn(conn, config.ConnectionIDLength, config.StatelessResetKey, config.Tracer)
	if err != nil {
		return nil, err
	}
	tokenGenerator, err := handshake.NewTokenGenerator(rand.Reader)
	if err != nil {
		return nil, err
	}
	c, err := wrapConn(conn)
	if err != nil {
		return nil, err
	}
	s := &baseServer{
		conn:                c,
		tlsConf:             tlsConf,
		config:              config,
		tokenGenerator:      tokenGenerator,
		sessionHandler:      sessionHandler,
		sessionQueue:        make(chan quicSession),
		errorChan:           make(chan struct{}),
		running:             make(chan struct{}),
		receivedPackets:     make(chan *receivedPacket, protocol.MaxServerUnprocessedPackets),
		newSession:          newSession,
		logger:              utils.DefaultLogger.WithPrefix("server"),
		acceptEarlySessions: acceptEarly,
	}
	go s.run()
	sessionHandler.SetServer(s)
	s.logger.Debugf("Listening for %s connections on %s", conn.LocalAddr().Network(), conn.LocalAddr().String())
	return s, nil
}

ListenAddr创建一个QUIC服务器监听给定的地址。 Config可以为nil,在这种情况下将使用默认值。   

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

func listenAddr(addr string, tlsConf *tls.Config, config *Config, acceptEarly bool) (*baseServer, error) {
	udpAddr, err := net.ResolveUDPAddr("udp", addr)
	if err != nil {
		return nil, err
	}
	conn, err := net.ListenUDP("udp", udpAddr)
	if err != nil {
		return nil, err
	}
	serv, err := listen(conn, tlsConf, config, acceptEarly)
	if err != nil {
		return nil, err
	}
	serv.createdPacketConn = true
	return serv, nil
}

函数返回一个baseServer,这是一个QUIC的listener。接着新建一个线程,不断地监听端口,等待一个新的客户端连接请求。

Accept返回已经完成握手的会话,只有当acceptEarlySessions为false时才有效。

func (s *baseServer) Accept(ctx context.Context) (Session, error) {
	return s.accept(ctx)
}

func (s *baseServer) accept(ctx context.Context) (quicSession, error) {
	select {
	case <-ctx.Done():
		return nil, ctx.Err()
	case sess := <-s.sessionQueue:
		atomic.AddInt32(&s.sessionQueueLen, -1)
		return sess, nil
	case <-s.errorChan:
		return nil, s.serverError
	}
}

这里当收到一个新的请求的时候添加到sessionQueue中,返回一个客户端Session,用这个Session可以和客户端进行发送和接收数据。

AddConn函数实现了多路复用添加连接的过程。

func (m *connMultiplexer) AddConn(
	c net.PacketConn,
	connIDLen int,
	statelessResetKey []byte,
	tracer logging.Tracer,
) (packetHandlerManager, error) {
	m.mutex.Lock()
	defer m.mutex.Unlock()

	addr := c.LocalAddr()
	connIndex := addr.Network() + " " + addr.String()
	p, ok := m.conns[connIndex]
	if !ok {
		manager, err := m.newPacketHandlerManager(c, connIDLen, statelessResetKey, tracer, m.logger)
		if err != nil {
			return nil, err
		}
		p = connManager{
			connIDLen:         connIDLen,
			statelessResetKey: statelessResetKey,
			manager:           manager,
			tracer:            tracer,
		}
		m.conns[connIndex] = p
	} else {
		if p.connIDLen != connIDLen {
			return nil, fmt.Errorf("cannot use %d byte connection IDs on a connection that is already using %d byte connction IDs", connIDLen, p.connIDLen)
		}
		if statelessResetKey != nil && !bytes.Equal(p.statelessResetKey, statelessResetKey) {
			return nil, fmt.Errorf("cannot use different stateless reset keys on the same packet conn")
		}
		if tracer != p.tracer {
			return nil, fmt.Errorf("cannot use different tracers on the same packet conn")
		}
	}
	return p.manager, nil
}

connMultiplexer这个结构定义了互斥锁、连接map、新建函数和日志处理。AddConn函数首先加了互斥锁,然后将连接信息新建了一个连接管理器connManager,加入到conns这个map当中,map以ip地址(如:"udp xx.xx.xx.xx:80")作为key,连接管理器作为value。

四、总结

如今QUIC即将正式成为HTTP/3标准,但距离广泛应用还有很有一段时间,QUIC确实是一个完善的传输层解决方案,在Web访问上相对TCP确实具有一些优势,QUIC在大部分情况下的确能比 TCP 达到更低的传输延时,但是仍然有部分情况下 QUIC 的表现不如 TCP。这些 QUIC 性能表现较差的场景往往是拥塞算法的选择、服务器部署等外部因素造成的,而非QUIC本身的设计缺陷。

NP577

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

571

社区成员

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

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