一. QUIC 的基本特点

基于UDP的多路传输(单连接下);
极低的等待时延(相比于TCP的三次握手);
快速迭代更新;
开源于Chromium项目中。
首先,QUIC为 传输层 协议,与TCP、UDP、SCTP同级。所以肯定会 在一定范围内 同现有的传输层协议构成竞争关系。

二. 为什么不用TCP

TCP由于基于操作系统内核实现,发展速度极慢,现有的TCP Fast Open实现等等虽然早已存在于标准中但是实际应用情况及其落后,即便除非所有机器的操作系统都更新到最新,否则考虑到兼容性不太可能大范围采用新技术。
QUIC直接基于客户端实现,而非基于系统内核(这点有点像最新的.Net Core),可以进行快速迭代更新,不需要操作系统内核层面的更改。
(TCP的性能缺陷将在下文中说明)。
像SCTP这样的传输层协议也都存在了10多年了但由于支持的操作系统内核太少也完全没有办法普及应用,所以基于UDP是一个更有效的选择。(并非是上层协议)

三. QUIC的发展路线

QUIC成为一个独立的传输层方案,成为更多应用层的高性能选择;
QUIC的理念被TCP和TLS所采纳,使得TCP的性能得到充分发展,融合统一;
综上所述,Google并不是想取代TCP,但是确实想改TCP又改不了(内核实现的劣势),所以独立实现了QUIC协议作为替补方案。

四. 核心优势

  1. 多路复用
    类似于SCTP的 多流 设计,可以通过一个连接同时进行多个请求,不必等待上一个请求返回浪费时间,也不必同时建立若干个连接浪费资源。
    另外单流情况下若发生丢包则会有等待重传阻塞,影响整个连接的传输速度。
  2. 等待时延(Latency)
    Web访问的 用户体验 极大地取决于打开网站的等待时间,而TCP需要进行 三次握手 才能建立连接,具有一定的连接等待时延,如果用了TLS加密,还会有其他的步骤进一步增加时延。
    QUIC采用了类似于TCP Fast Open的设计,在之前已经连接过的情况下可以无需握手,直接开始传送数据,连接建立时延为0。
    为什么不直接用TCP Fast Open呢?因为TCP在内核呀,除非所有的服务器和客户端的操作系统都能支持并且都更新到能支持的版本才行。所以可能这辈子都不行,就像HTTP1也支持单连接承载多请求但还没有哪个浏览器支持的。
  3. 加密技术
    总之看起来比TLS性能好很多,也具有各种攻击防御策略,这方面不是很懂。可以直接看视频或者相关文档。
  4. 前向纠错
    QUIC和TCP一个主要的核心区别就是:TCP采用 重传 机制,而QUIC采用 纠错 机制。
    如果发生丢包的话,TCP首先需要一个等待延时来判断发生了丢包,然后再启动重传机制,在此期间会对连接造成一定的阻塞(并且TCP窗口是缓慢增大的,Web这种突发性快速连接情况下窗口会相对较小),从而影响传输时间。
    而QUIC采用了一种脑洞极大的前向纠错(FEC)方案,类似于RAID5,将N个包的校验和(异或)建立一个单独的数据包发送,这样如果在这N个包中丢了一个包可以直接恢复出来,完全不需要重传,有利于保证高速性,N可以根据网络状况动态调整。
  5. 速率控制而非拥塞控制
  6. 连接保持
    在IP地址和端口变化的情况下(比如从Wi-Fi切换到流量),可以无需重新建立连接,继续通信。对移动设备的用户体验较为友好。
    综上所述,QUIC确实是一个完善的传输层解决方案,在 Web 访问 上相对TCP确实具有一些优势。但是总的来说,不说取代,如果能达到和TCP相提并论的地步就已经非常不错了。

下面是golang基于quic协议实现的server代码

package main

import (
    "github.com/lucas-clemente/quic-go"
    "io"
    "fmt"
    "crypto/tls"
    "crypto/rsa"
    "crypto/x509"
    "math/big"
    "encoding/pem"
    "crypto/rand"
)
const saddr = "localhost:9999"
func main()  {
    listener, err := quic.ListenAddr(saddr, generateTLSConfig(), nil)
    if err != nil {
        fmt.Println(err)
    }
    for{
        sess, err := listener.Accept()
        if err != nil {
            fmt.Println(err)
        }else{
            go dealSession(sess)
        }
    }
}
func dealSession(sess quic.Session){
    stream, err := sess.AcceptStream()
    if err != nil {
        panic(err)
    }else{
        for{
            _, err = io.Copy(loggingWriter{stream}, stream)
        }
    }
}
type loggingWriter struct{ io.Writer }
func (w loggingWriter) Write(b []byte) (int, error) {
    fmt.Printf("Server: Got '%s'\n", string(b))
    return w.Writer.Write(b)
}
// Setup a bare-bones TLS config for the server
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}}
}

下面是golang基于quic协议实现的client代码

package main

import (
    "github.com/lucas-clemente/quic-go"
    "io"
    "crypto/tls"
    "fmt"
    "time"
)

const addr = "localhost:9999"
const message = "ccc"
func main()  {
     session, err := quic.DialAddr(addr, &tls.Config{InsecureSkipVerify: true}, nil)
    if err != nil {
        fmt.Println(err)
        return
    }
    stream, err := session.OpenStreamSync()
    if err != nil {
        fmt.Println(err)
        return
    }
    for{
        fmt.Printf("Client: Sending '%s'\n", message)
        _, err = stream.Write([]byte(message))
        if err != nil {
            fmt.Println(err)
            return
        }
        buf := make([]byte, len(message))
        _, err = io.ReadFull(stream, buf)
        if err != nil {
            fmt.Println(err)
            return
        }
        fmt.Printf("Client: Got '%s'\n", buf)

        time.Sleep(2*time.Second)
    }
}