? 我们可以自己打开浏览器的控制台就可以发现目前主流web服务的http协议都基本是1.1版本了。HTTP/1.0最初实现了可用性。对每个请求都需要TCP三次握手建立单独链路。HTTP/1.1优化了传输效率。新增keep-alive特性使多个请求可以复用同一条TCP链路(TCP keep-alive是传输层特性,防止NAT路由断开连接);它支持持续连接.通过这种连接,就有可能在建立一个TCP连接后,发送请求并得到回应,然后发送更多的请求并得到更多的回应.通过把建立和释放TCP连接的开销分摊到多个请求上,则对于每个请求而言,由于TCP而造成的相对开销被大大地降低了
存在的缺陷
假设一个页面要发送三个独立的请求,一个获取css,一个获取js,一个获取图片jpg。如果使用HTTP1.1就是串行的,但是如果使用HTTP2.0,就可以在一个连接里,客户端和服务端都可以同时发送多个请求或回应,而且不用按照顺序一对一对应
HTTP2.0的缺陷 因为还是基于TCP协议的原因,基于连接的TCP协议在往返时百延(RTT)上仍是一个问题(如图是TCP三次握手的过程)
当其中一个数据包遇到问题,TCP连接需要等待整个包完成重传之后才能继续进行,虽然HTTP2.0通过多个stream,使得逻辑上一个tcp连接上的并行内容,进行多路数据的传输,然而这中间没有关联的数据,一前一后,前面stream2的帧没有收到,后面stream1的帧也会因此堵塞
是由Google提出的一种基于UDP改进的低时延的互联网传输层(其实有疑义,QUIC基于UDP,其实更像应用层协议)协议。
因为TCP的重传机制,只要一个包丢失就得判断丢包并且重传,导致发生队头阻塞的问题,但是UDP没有这个限制。除此之外,它还有如下特点:
基于UDP,就可以在QUIC自己的逻辑里面维护连接的机制,不再以四元组标识,而是以一个64 位的随机数作为ID来标识,而且UDP是无连接的,所以当ip或者端口变化的时候,只要ID不变,就不需要重新建立连接
TCP的流量控制是通过滑动窗口协议。QUIC的流量控制也是通过window_update,来告诉对端它可以接受的字节数。但是QUIC的窗口是适应自己的多路复用机制的,不但在一个连接上控制窗口,还在一个连接中的每个steam控制窗口。
? 由于谷歌chromium源码太过庞大,我们这里采用github上使用go实现的quic-go来分析quic的实现过程.
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.
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函数中
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()
}
}
}
}
? 进入一个死循环,等待收到消息,进行处理,并释放缓存,以及错误处理返回.
? 首先我们进入到example文件夹下。运行go build main.go
,然后运行./main
;
随后进入到client文件夹下面,go build main.go
,然后运行 ./main https://localhost:6212/demo/echo
;
我们可以看到服务端响应为 200:OK, 说明服务端和客户端都运行从正常。
? 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
原文:https://www.cnblogs.com/panrenhua/p/14349305.html