QUIC协议和HTTP3.0技术研究

QUIC协议和HTTP3.0技术研究

1. 现状

1.1 HTTP 1.0

? 我们可以自己打开浏览器的控制台就可以发现目前主流web服务的http协议都基本是1.1版本了。HTTP/1.0最初实现了可用性。对每个请求都需要TCP三次握手建立单独链路。HTTP/1.1优化了传输效率。新增keep-alive特性使多个请求可以复用同一条TCP链路(TCP keep-alive是传输层特性,防止NAT路由断开连接);它支持持续连接.通过这种连接,就有可能在建立一个TCP连接后,发送请求并得到回应,然后发送更多的请求并得到更多的回应.通过把建立和释放TCP连接的开销分摊到多个请求上,则对于每个请求而言,由于TCP而造成的相对开销被大大地降低了

存在的缺陷

  • 队头堵塞导致:虽然通过持久性连接得到改善,但是每一个请求的服务端响应依然需要按照顺序排队,如果前面的响应处理较为耗费时间,那么同样非常耗费性能。

1.2 HTTP 2.0

  • 在 HTTP/1.1 协议中浏览器客户端在同一时间,针对同一域名下的请求有一定数量限制。超过限制数目的请求会被阻塞。
  • 而 HTTP/2 的多路复用(Multiplexing) 则允许同时通过单一的 HTTP/2 连接发起多重的请求-响应消息。因此 HTTP/2 可以很容易的去实现多流并行而不用依赖建立多个 TCP 连接,HTTP/2 把 HTTP 协议通信的基本单位缩小为一个一个的帧,这些帧对应着逻辑流中的消息。并行地在同一个 TCP 连接上双向交换消息。

QUIC协议和HTTP3.0技术研究

  • HTTP/2在 应用层(HTTP/2)和传输层(TCP or UDP)之间增加一个二进制分帧层。在不改动 HTTP/1.x 的语义、方法、状态码、URI 以及首部字段的情况下, 解决了HTTP1.1 的性能限制,改进传输性能,实现低延迟和高吞吐量。在二进制分帧层中, HTTP/2 会将所有传输的信息分割为更小的消息和帧(frame),并对它们采用二进制格式的编码 ,其中 HTTP1.x 的首部信息会被封装到 HEADER frame,而相应的 Request Body 则封装到 DATA frame 里面。

假设一个页面要发送三个独立的请求,一个获取css,一个获取js,一个获取图片jpg。如果使用HTTP1.1就是串行的,但是如果使用HTTP2.0,就可以在一个连接里,客户端和服务端都可以同时发送多个请求或回应,而且不用按照顺序一对一对应

QUIC协议和HTTP3.0技术研究

HTTP2.0的缺陷 因为还是基于TCP协议的原因,基于连接的TCP协议在往返时百延(RTT)上仍是一个问题(如图是TCP三次握手的过程)

QUIC协议和HTTP3.0技术研究

当其中一个数据包遇到问题,TCP连接需要等待整个包完成重传之后才能继续进行,虽然HTTP2.0通过多个stream,使得逻辑上一个tcp连接上的并行内容,进行多路数据的传输,然而这中间没有关联的数据,一前一后,前面stream2的帧没有收到,后面stream1的帧也会因此堵塞

2. QUIC(Quick UDP Internet Connections)

是由Google提出的一种基于UDP改进的低时延的互联网传输层(其实有疑义,QUIC基于UDP,其实更像应用层协议)协议。

因为TCP的重传机制,只要一个包丢失就得判断丢包并且重传,导致发生队头阻塞的问题,但是UDP没有这个限制。除此之外,它还有如下特点:

  • 实现了自己的加密协议,通过类似TCP的TFO机制实现0-RTT,当然TLS1.3已经实现了0-RTT。支持重传和纠错机制,在只丢失一个包的情况下不需要重传,使用纠错机制恢复丢失的包。
  • 纠错机制:通过异或的方式,算出发出去的数据的异或值并单独发出一个包,服务端在发现有一个包丢失的情况下,通过其他数据包的异或值包算出丢失包。 在丢失两个包及以上的情况就是用重传机制,因为算不出来了。

基于UDP,就可以在QUIC自己的逻辑里面维护连接的机制,不再以四元组标识,而是以一个64 位的随机数作为ID来标识,而且UDP是无连接的,所以当ip或者端口变化的时候,只要ID不变,就不需要重新建立连接

QUIC协议和HTTP3.0技术研究

TCP的流量控制是通过滑动窗口协议。QUIC的流量控制也是通过window_update,来告诉对端它可以接受的字节数。但是QUIC的窗口是适应自己的多路复用机制的,不但在一个连接上控制窗口,还在一个连接中的每个steam控制窗口。

QUIC协议和HTTP3.0技术研究

3. QUIC源码分析

? 由于谷歌chromium源码太过庞大,我们这里采用github上使用go实现的quic-go来分析quic的实现过程.

3.1 客户端部分

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

? 客户端采用 DialAddrEarly来向服务端创建一个quic连接

addr 存储服务器的地址

tlsConf tls加密相关配置

config 链接相关配置

// Config contains all configuration data needed for a QUIC server or client.
type Config struct {
	// The QUIC versions that can be negotiated.
	Versions []VersionNumber
	// The length of the connection ID in bytes.
	ConnectionIDLength int
	// HandshakeIdleTimeout is the idle timeout before completion of the handshake.
	HandshakeIdleTimeout time.Duration
	// MaxIdleTimeout is the maximum duration that may pass without any incoming network activity.
	MaxIdleTimeout time.Duration
	// AcceptToken determines if a Token is accepted.
	AcceptToken func(clientAddr net.Addr, token *Token) bool
	// The TokenStore stores tokens received from the server.
	TokenStore TokenStore
	// MaxReceiveStreamFlowControlWindow is the maximum stream-level flow control window for receiving data.
	MaxReceiveStreamFlowControlWindow uint64
	// MaxReceiveConnectionFlowControlWindow is the connection-level flow control window for receiving data.
	MaxReceiveConnectionFlowControlWindow uint64
	// MaxIncomingStreams is the maximum number of concurrent bidirectional streams that a peer is allowed to open.
	MaxIncomingStreams int64
	// MaxIncomingUniStreams is the maximum number of concurrent unidirectional streams that a peer is allowed to open.
	MaxIncomingUniStreams int64
	// The StatelessResetKey is used to generate stateless reset tokens.
	StatelessResetKey []byte
	// KeepAlive defines whether this peer will periodically send a packet to keep the connection alive.
	KeepAlive bool
	// Datagrams will only be available when both peers enable datagram support.
	EnableDatagrams bool
	Tracer          logging.Tracer
}

? 以上是关于quic连接的相关配置。我们再来看 DialAddrEarlyContext(context.Background(), addr, tlsConf, config), 其中context是go中并发编程管理上下文切换的标准库。与本次的quic协议无关这里我们不多做叙述。

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)
}

? 这里新增了一个参数 use0RTT 我们知道tcp协议在通信之前需要先握手,这个rtt的时间内发送的帧是无法携带有效信息的,但是采用quic通信的双方可以使采用0RTT的方式通信(但是这种方式也是有前提的,如果双方是第一次建立通信,就不可以使用这种方式).下面我们来看一下diaiContext() 这个函数.

func dialContext(
	ctx context.Context,
	pconn net.PacketConn,
	remoteAddr net.Addr,
	host string,
	tlsConf *tls.Config,
	config *Config,
	use0RTT bool,
	createdPacketConn bool,
) (quicSession, error) {
    // ...(0)...
    if err := validateConfig(config); err != nil {
		return nil, err
	}
	config = populateClientConfig(config, createdPacketConn)
	packetHandlers, err := getMultiplexer().AddConn(pconn, config.ConnectionIDLength, config.StatelessResetKey, config.Tracer)
    // ...(1)...
	c, err := newClient(pconn, remoteAddr, config, tlsConf, host, use0RTT, createdPacketConn)
    // ...(2)...
    if err := c.dial(ctx); err != nil {
		return nil, err
	}
	return c.session, nil
}

? 这个函数体内部在(0)处进行相关配置工作 packetHandlers, err := getMultiplexer().AddConn(pconn, config.ConnectionIDLength, config.StatelessResetKey, config.Tracer) packet的处理方式是多路复用 , 在(1)处创建了一个新的client,在(2)处进行与服务端的连接.最后返回一个session.

3.2 服务端部分

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
}

? 首先也是解析创建udp地址,然后进入listen函数监听.

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
	}
	s := &baseServer{
		// ...相关配置
	}
	go s.run()
	sessionHandler.SetServer(s)
	s.logger.Debugf("Listening for %s connections on %s", conn.LocalAddr().Network(), conn.LocalAddr().String())
	return s, nil
}

? listen函数中

  1. sessionHandler, err := getMultiplexer().AddConn(conn, config.ConnectionIDLength, config.StatelessResetKey, config.Tracer)添加多路复用连接;
  2. tokenGenerator, err := handshake.NewTokenGenerator(rand.Reader)新建一个token,交给服务端方便客户端进行连接时,将token交给客户端;
  3. s := &baseServer{} 将相关配置加入到baseServer中. go s.run()运行一个协程;
func (s *baseServer) run() {
	defer close(s.running)
	for {
		select {
		case <-s.errorChan:
			return
		default:
		}
		select {
		case <-s.errorChan:
			return
		case p := <-s.receivedPackets:
			if bufferStillInUse := s.handlePacketImpl(p); !bufferStillInUse {
				p.buffer.Release()
			}
		}
	}
}

? 进入一个死循环,等待收到消息,进行处理,并释放缓存,以及错误处理返回.

4 QUIC-GO运行

? 首先我们进入到example文件夹下。运行go build main.go,然后运行./main

随后进入到client文件夹下面,go build main.go,然后运行 ./main https://localhost:6212/demo/echo;

QUIC协议和HTTP3.0技术研究

QUIC协议和HTTP3.0技术研究

我们可以看到服务端响应为 200:OK, 说明服务端和客户端都运行从正常。

5. 总结

? QUIC协议相比于之前的协议有了很大的进步,具备更低的延迟和更高的安全性。尤其是现在短视频,直播的兴起,非常适合QUIC协议的应用。但是QUIC协议目前还在草案阶段,相关网站应用的完善还有很远的路要走。本文在写作时参考了几位同学写的文章,对我完成此文提供了很大的帮助,在此感谢。

参考资料

[1]. https://www.cnblogs.com/CatYe/p/14179075.html
[2]. https://zhuanlan.zhihu.com/p/137073979
[3]. https://github.com/lucas-clemente/quic-go

QUIC协议和HTTP3.0技术研究

上一篇:BindsNET学习系列——BasePipeline


下一篇:MYSQL安装报错 -- 出现Failed to find valid data directory.