其他分享
首页 > 其他分享> > QUIC协议和HTTP3.0技术研究

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

img

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

img

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

img

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

2. QUIC(Quick UDP Internet Connections)

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

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

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

img

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

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;

我们可以看到服务端响应为 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

标签:协议,return,err,nil,TCP,HTTP3.0,QUIC,config
来源: https://www.cnblogs.com/panrenhua/p/14349305.html