QUIC协议的基本原理等

wangffan12 2022-01-18 15:39:49

目录

1. QUIC简介

2. QUIC原理分析

    2.1. 连接迁移

    2.2. 低延时连接

    2.3. 自定义拥塞控制

 3. QUIC配置(以Nginx为例)

     3.1. 环境准备

     3.2. 依赖安装

     3.3. 编译nginx-quic源码

     3.4. Nginx QUIC配置

 4. quic-go源码编译及测试Demo运行

     4.1. 准备

     4.2. 启动demo程序

     4.3. 测试(以服务器程序为例)

5. QUIC源码分析(基于quic-go)

     5.1. 服务端

            5.1.1. 服务器启动核心逻辑

            5.1.2. 0RTT核心逻辑

     5.2. 客户端

            5.2.1. 客户端基本数据结构定义

            5.2.2. 客户端发送请求


 

1. QUIC简介

       QUIC(Quick UDP Internet Connection)是谷歌推出的一套基于 UDP 的传输协议,它实现了 TCP + HTTPS + HTTP/2 的功能,目的是保证可靠性的同时降低网络延迟。因为 UDP 是一个简单传输协议,基于 UDP 可以摆脱 TCP 传输确认、重传慢启动等因素,建立安全连接只需要一的个往返时间,它还实现了 HTTP/2 多路复用、头部压缩等功能。

       网络传输层的TCP协议虽然保证了安全的数据传输,但是需要TCP连接的双方进行多次通信,这些通信带来的传输损耗在有些场景下并不小。其次TCP为了保证安全完整的传输,使用了很多操作系统内核的特性,如果将用户使用的TCP协议升级,那么就要对操作系统进行升级,这显然对大部分的客户来说是不能接受的,因为你永远不知道升级了操作系统之后会给其他业务带来什么影响。而QUIC协议基于UDP改造,不仅不和内核深度绑定,而且能独立发挥,只要有通信的双方都可以做到。

 

2. QUIC原理分析

   2.1.连接迁移

       在传统的TCP协议中,如果连接的某一条件发生了变动,那么就会引起TCP的重新连接。例如手机连接的WIFI发生了切换而导致IP地址发生了切换,那么基于TCP的HTTP协议就无法保持原有的状态而必须重新连接;如果使用公共 NAT 出口时,有些连接竞争时需要重新绑定端口,导致客户端的端口发生变化,同样需要重新建立 TCP 连接。

       在QUIC协议中,连接将不再由TCP四元组和IP地址进行标识,这种标识方式是造成TCP连接必须重新连接的“罪魁祸首”,四元组和IP任何一个发生了变化,就是一条新的TCP连接了。QUIC使用一个随机的64位标识符表示一个连接,就算IP地址和端口发生了变化,只要ID不变,这条连接依然存在和有效,因此可以继续使用该连接。

       随机ID的生成方式很多,UUID算法或者雪花算法都能生成全局唯一ID,而QUIC有自己的生成模式,确保了任意连接不会发生冲突。

  2.2.低延时连接

       目前任何一个连接,只要使用了TCP连接,就必有一个RTT的TCP握手延迟,这是TCP协议的“通病”,而且在TCP握手阶段无法传输任何数据,传输数据最快也得等到TCP握手结束。

       而QUIC基于UDP协议,无须进行TCP的握手阶段,在最好情况下,短连接下 QUIC 可以做到 0RTT 开启数据传输。而基于 TCP 的 HTTPS,即使在最好的 TLS1.3 的 early data 下仍然需要 1RTT 开启数据传输。而对于目前线上常见的 TLS1.2 完全握手的情况,则需要 3RTT 开启数据传输。对于 RTT 敏感的业务,QUIC 可以有效的降低连接建立延迟。

QUIC具体握手过程如下:

       1. 客户端判断本地是否已有服务器的全部配置参数(证书配置信息),如果有则直接跳转到(5),否则继续 。
       2. 客户端向服务器发送 `inchoate client hello(CHLO)` 消息,请求服务器传输配置参数。
       3. 服务器收到 `CHLO`,回复 `rejection(REJ)` 消息,其中包含服务器的部分配置参数
       4. 客户端收到 `REJ`,提取并存储服务器配置参数,跳回到 (1) 。
       5. 客户端向服务器发送 `full client hello` 消息,开始正式握手,消息中包括客户端选择的公开数。此时客户端根据获取的服务器配置参数和自己选择的公开数,可以计算出初始密钥 `K1`。
       6. 服务器收到 `full client hello`,如果不同意连接就回复 `REJ`,同(3);如果同意连接,根据客户端的公开数计算出初始密钥 `K1`,回复 `server hello(SHLO)` 消息, `SHLO` 用初始密钥 `K1` 加密,并且其中包含服务器选择的一个临时公开数。
       7. 客户端收到服务器的回复,如果是 `REJ` 则情况同(4);如果是 `SHLO`,则尝试用初始密钥 `K1` 解密,提取出临时公开数。
       8. 客户端和服务器根据临时公开数和初始密钥 `K1`,各自基于 `SHA-256` 算法推导出会话密钥 `K2`。
       9. 双方更换为使用会话密钥 `K2` 通信,初始密钥 `K1` 此时已无用,QUIC 握手过程完毕。之后会话密钥 `K2` 更新的流程与以上过程类似,只是数据包中的某些字段略有不同。

 

 2.3. 自定义拥塞控制

        拥塞控制技术是网络在出现阻塞,超时等因流量过多情况而“自我调节”的能力。TCP提供了拥塞控制的功能,但是对于用户来说是“透明的”,及拥塞控制的流程和用户不相关,是自动进行的,而QUIC提供了一种方式让用户能够自定义拥塞控制信息。比如对于每一个包,不管是原始包还是重传包,都带有一个新的序列号(seq),这使得 QUIC 能够区分 ACK 是重传包还是原始包,从而避免了 TCP 重传模糊的问题。QUIC 同时还带有收到数据包与发出 ACK 之间的时延信息。这些信息能够帮助更精确的计算 RTT。

       这种拥塞控制方法不再依赖内核的算法,而且能够在业务层根据业务场景实现自定义拥塞控制,配置和使用不同的拥塞控制算法和参数。

 3. QUIC配置(以Nginx为例)

  3.1. 环境准备

        实验环境为CentOS7操作系统,内存2GB,带宽2MB,硬盘100GB。

  3.2. 依赖安装

* OS相关依赖安装
* CentOS 安装libunwind扩展库

yum install libunwind-devel -y

* 进入 Nginx 安装目录

cd /www/server/nginx

* 下载编译需要用到的相关
 * 下载nginx

wget http://nginx.org/download/nginx-1.19.4.tar.gz

 * 下载quiche

git clone --recursive https://github.com/cloudflare/quiche

 * go的下载(注意,若系统之前已经安装过,需要先卸载旧版)

yum remove go

 * 下载 go

wget https://dl.google.com/go/go1.15.2.linux-amd64.tar.gz

 * cmake 的下载(注意,若系统之前已经安装过,请确认所安装版本大于 3.0,负责需要先卸载旧版)
 * 查看当前 cmake 版本

cmake --version

 * 卸载旧版

yum remove cmake

 * 下载cmake-3.11.4(高于该版本的后边可能会编译出错。)

wget https://github.com/Kitware/CMake/releases/download/v3.11.4/cmake-3.11.4.tar.gz

 * 编译安装

tar zxf cmake-3.11.4.tar.gz && cd cmake-3.11.4 && ./bootstrap && make && make install

 * 设置软连接

ln -s /usr/local/bin/cmake /usr/bin/cmake

 * 解压go

tar -C /usr/local -xzf go1.15.2.linux-amd64.tar.gz

 * 安装rust

curl https://sh.rustup.rs -sSf | sh

 * 设置系统环境变量,也可以写入到profile中

export PATH=$PATH:/usr/local/go/bin
export PATH=$PATH:$HOME/.cargo/bin
export GOROOT=/usr/local/go
export GOBIN=$GOROOT/bin
export PATH=$PATH:$GOBIN

 * 使其生效

source ~/.profile
```

 * 安装`nginx-quic`源码

```bash
git clone https://github.com/evansun922/nginx-quic.git
```

 * 安装`chromium`源码
  * 拉取源码

git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git

 * 设置环境变量

export PATH=/path/to/depot_tools:$PATH

 * 下载chromium

fetch chromium
```

 * 在工作目录下执行命令来完成依赖准备

  ```bash
  .gclient   # A configuration file for you source checkout
  src/       # Top-level Chromium source checkout.
  cd src && ./build/install-build-deps.sh
  gclient sync
  ```

 * 依赖安装完成之后可以检查安装情况(每条命令都有相应的输出)

```bash
git checkout 86.0.4224.0
gclient sync -D
```

 3.3.编译nginx-quic源码

* 安装nginx,直接从官网下载解压即可
* 在`nginx-quic`源码目录下执行

```bash
python3 mk2gn.py "/home/zxilly/Downloads/nginx-1.19.2" \
"/home/zxilly/quic/quic/src" "--user=www --group=www \
--prefix=/www/server/nginx \
--add-module=/www/server/nginx/src/ngx_devel_kit \
--add-module=/www/server/nginx/src/lua_nginx_module \
--add-module=/www/server/nginx/src/ngx_cache_purge \
--add-module=/www/server/nginx/src/nginx-sticky-module \
--with-pcre=pcre-8.43 \
--with-http_v2_module \
--with-stream --with-stream_ssl_module --with-stream_ssl_preread_module \
--with-http_stub_status_module \
--with-http_ssl_module --with-http_image_filter_module \
--with-http_gzip_static_module \
--with-http_gunzip_module \
--with-http_sub_module \
--with-http_flv_module \
--with-http_addition_module \
--with-http_realip_module \
--with-http_mp4_module \
--with-ld-opt=-Wl,-E \
--with-cc-opt=-Wno-error \
--with-ld-opt=-ljemalloc \
--with-http_dav_module \
--add-module=/www/server/nginx/src/nginx-dav-ext-module \
--add-module=/root/ngx_brotli"
```

* 在`chromium`目录下进行编译

```bash
gn gen out/Release --args="is_component_build=false is_debug=false"
```

**如果此项编译失败,可以转为执行下面的步骤** :

1. 把需要的头文件单独软链接到其他目录

```bash
executable("nginx") {
sources = [
# 所用到的.c或者.cc源码,一般不用修改
]
include_dirs = [
#头文件dir,类似与 "-I"
]
lib_dirs = [
#  库的搜索目录,类似与-L your libdir
]
libs = [
# dynamic library: pthread or static library: /path/xxx.a
]
cflags_c = [
# 编译选项
"-D_FORTIFY_SOURCE=2",
"-DTCP_FASTOPEN=23",
"-DNDK_SET_VAR",
]
}
```

2. 执行 `ninja -C out/Release nginx`,编译好的 nginx-quic 就在 `out/Release` 目录中。
3. 执行`./nginx -V` 来检查编译是否成功

3.4. Nginx QUIC配置

* 命令行配置Nginx的QUIC支持
 * 测试一下

sudo objs/nginx -t

* 复制新的编译好的执行文件 nginx,覆盖替换旧版本执行文件 nginx

mv /www/server/nginx/sbin/nginx /www/server/nginx/sbin/nginx.bak
cp /www/server/nginx/nginx-1.19.4/objs/nginx /www/server/nginx/sbin/nginx
 

* 升级

make upgrade

* 查看版本,Nginx 名称和版本号被我修改了。

Nginx -V
nginx version: QUIC/1.1.1 (--build=quiche-fd5e028)
built by gcc 4.8.5 20150623 (Red Hat 4.8.5-39) (GCC) 
built with OpenSSL 1.1.0 (compatible; BoringSSL) (running with BoringSSL)
TLS SNI support enabled
configure arguments: --user=www --group=www --prefix=/www/server/nginx --with-openssl=../quiche/deps/boringssl --with-quiche=../quiche --add-module=/www/server/nginx/src/lua_nginx_module --add-module=/www/server/nginx/src/redis2-nginx-module --add-module=/www/server/nginx/src/ngx_devel_kit --add-module=/www/server/nginx/src/ngx-pagespeed --add-module=/www/server/nginx/src/ngx_cache_purge --add-module=/www/server/nginx/src/nginx-sticky-module --add-module=/www/server/nginx/src/ngx_brotli --add-module=/www/server/nginx/src/nginx-http-concat --add-module=/www/server/nginx/src/nginx-sorted-querystring-module --add-module=/www/server/nginx/src/ngx_http_substitutions_filter_module --add-module=/www/server/nginx/src/headers-more-nginx-module --add-module=/www/server/nginx/src/nginx_cookie_flag_module --add-module=/www/server/nginx/src/ModSecurity-nginx --add-module=/www/server/nginx/src/ngx_webp --with-http_stub_status_module --with-pcre=/www/server/nginx/src/pcre-8.44 --with-zlib=/www/server/nginx/src/zlib --with-http_ssl_module --with-http_v2_module --with-http_image_filter_module --with-http_gzip_static_module --with-http_gunzip_module --with-stream --with-stream_ssl_module --with-ipv6 --with-http_sub_module --with-http_flv_module --with-http_addition_module --with-http_realip_module --with-http_mp4_module --with-http_v2_hpack_enc --with-google_perftools_module --with-http_geoip_module --with-http_v3_module --build=--build=quiche-fd5e028

 * 开启QUIC特性

# Enable QUIC and HTTP/3.
listen 443 quic reuseport;
# Enable HTTP/2 (optional).
listen 443 ssl http2;
 
# 只支持tls1.3
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers [TLS13+AESGCM+AES128|TLS13+CHACHA20]:TLS13+AESGCM+AES256:[EECDH+ECDSA+AESGCM+AES128|EECDH+ECDSA+CHACHA20]:EECDH+ECDSA+AESGCM+AES256:EECDH+ECDSA+AES128+SHA:EECDH+ECDSA+AES256+SHA:[EECDH+aRSA+AESGCM+AES128|EECDH+aRSA+CHACHA20]:EECDH+aRSA+AESGCM+AES256:EECDH+aRSA+AES128+SHA:EECDH+aRSA+AES256+SHA;
ssl_prefer_server_ciphers on;
ssl_early_data on;
ssl_ecdh_curve X25519:P-256:P-384;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
error_page 497  https://$host$request_uri;
 
# OCSP Stapling 启用
ssl_stapling on;
ssl_stapling_verify on;
ssl_stapling_file /home/others/ocsp/nange.cn.ocsp.resp;
 
# Add Alt-Svc header to negotiate HTTP/3.
add_header alt-svc 'quic=":443"; ma=2592000; ';
```

* nginx config文件的通用配置(在nginx的配置文件中配置)

```bash
      http {

          ...

          server {
               listen              443 quic reuseport  sndbuf=1048576 rcvbuf=1048576;
              
              ssl_certificate           ssl/tv.test.com.crt;
              ssl_certificate_key       ssl/tv.test.com.pkcs8;

              ...
          }
```

* nginx-rtmp配置(在nginx的配置文件中配置)

```bash
      rtmp {

          ...

          server {

                     listen         1935 so_keepalive=on;
                     listen         1935 quic reuseport;

                     ssl_certificate                ssl/tv.test.com.crt;
                     ssl_certificate_key       ssl/tv.test.com.key;

                   application live {
                             live on;
                             idle_streams        off;
                             drop_idle_publisher 1800s;
                             sync                1s;
                             wait_key            on;
                             wait_video          on;
                    }
          }
  }
```

4. quic-go源码编译及测试Demo运行

       本测试基于QUIC内置的demo程序进行。

   4.1. 准备

* 下载源码

```bash
git clone https://github.com/lucas-clemente/quic-go.git
```

* 编译服务端源码

```bash
cd example
go build main.go
```

* 编译客户端源码

  修改`example/client/main.go`,在60行之后加上`qconf.Versions = []protocol.VersionNumber{protocol.VersionDraft29}`,选择quic版本为draft-29。

```bash
cd example/client
go build main.go
```

   4.2.启动demo程序

* 启动服务端,首次启动必须加上`-v tcp`参数,否则无法访问

```bash
./main -qlog -v -tcp
```

* 启动客户端

```bash
./main -v -insecure -keylog ssl.log https://quic.rocks:4433
```

   4.3. 测试(以服务器程序为例)

在firefox中打开`about:config`,搜索HTTP3,将值设为True以打开HTTP3的实验特性。打开`https://localhost:6121/demo/tile`网页,通过调试工具查看请求,当第一次请求该网页时,会通过TCP协议进行,并且得到的响应如下:

```bash
200  GET  localhost:6121  tile  HTTP/1.1
```

   并且在响应的头部会带上`Alt-Svc`字段,告诉浏览器对方网站支持HTTP3协议:

```markdown
Response Header:
* Alt-Svc: h3-29=".6121"; ma=34102120; h3-29=":6121"; ma=24316500
* Content-Length: 83
```

当第一次成功访问之后,再次访问网页,有了之前的沟通,网页就会通过QUIC协议访问:

```bash
200  GET  localhost:6121  title  HTTP/3 # 这里变成了HTTP/3
```

5. QUIC源码分析(基于quic-go)

  5.1. 服务端

    5.1.1.服务器启动核心逻辑

  服务器的启动调用的是`ListenAndServe()`函数:

```go
func (s *Server) ListenAndServe() error {
    if s.Server == nil {
        return errors.New("use of http3.Server without http.Server")
    }
    return s.serveImpl(s.TLSConfig, nil)
}
```

具体调用了`#serveImpl`来实现HTTP3服务器的启动:

```go
func (s *Server) serveImpl(tlsConf *tls.Config, conn net.PacketConn) error {
    if s.closed.Get() {
        return http.ErrServerClosed
    }
    if s.Server == nil {
        return errors.New("use of http3.Server without http.Server")
    }
    s.loggerOnce.Do(func() {
        s.logger = utils.DefaultLogger.WithPrefix("server")
    })

    // 解析tls配置项 
    baseConf := &tls.Config{
        GetConfigForClient: func(ch *tls.ClientHelloInfo) (*tls.Config, error) {
            // determine the ALPN from the QUIC version used
            proto := nextProtoH3Draft29
            if qconn, ok := ch.Conn.(handshake.ConnWithVersion); ok {
                // 检查quic版本
                if qconn.GetQUICVersion() == protocol.Version1 {
                    proto = nextProtoH3
                }
            }
            config := tlsConf
            if tlsConf.GetConfigForClient != nil {
                getConfigForClient := tlsConf.GetConfigForClient
                var err error
                // 根据配置创建一个新的client端
                conf, err := getConfigForClient(ch)
                if err != nil {
                    return nil, err
                }
                if conf != nil {
                    config = conf
                }
            }
            if config == nil {
                return nil, nil
            }
           // 为了节省资源,采用clone的方式复制配置项
            config = config.Clone()
            config.NextProtos = []string{proto}
            return config, nil
        },
    }

    var ln quic.EarlyListener
    var err error
    quicConf := s.QuicConfig
    if quicConf == nil {
        quicConf = &quic.Config{}
    } else {
        quicConf = s.QuicConfig.Clone()
    }
    if s.EnableDatagrams {
        quicConf.EnableDatagrams = true
    }
    if conn == nil {
        ln, err = quicListenAddr(s.Addr, baseConf, quicConf)
    } else {
        ln, err = quicListen(conn, baseConf, quicConf)
    }
    if err != nil {
        return err
    }
    s.addListener(&ln)
    defer s.removeListener(&ln)

    for {
        // 监听器监听数据
        sess, err := ln.Accept(context.Background())
        if err != nil {
            return err
        }
        // 启动一个新的协程处理请求
        go s.handleConn(sess)
    }
}
```

而核心逻辑是处理新的连接请求,即`#handleConn`函数

```go
func (s *Server) handleConn(sess quic.EarlySession) {
    decoder := qpack.NewDecoder(nil)

    // send a SETTINGS frame
    // 开启框架流
    str, err := sess.OpenUniStream()
    if err != nil {
        s.logger.Debugf("Opening the control stream failed.")
        return
    }
    buf := &bytes.Buffer{}
    quicvarint.Write(buf, streamTypeControlStream) // stream type
    (&settingsFrame{Datagram: s.EnableDatagrams}).Write(buf)
    str.Write(buf.Bytes())

    go s.handleUnidirectionalStreams(sess)

    // Process all requests immediately.
    // It's the client's responsibility to decide which requests are eligible for 0-RTT.
    // 采用0RTT的方式立即处理所有请求
    for {
        // 获取请求上下文信息
        str, err := sess.AcceptStream(context.Background())
        if err != nil {
            s.logger.Debugf("Accepting stream failed: %s", err)
            return
        }
        go func() {
            // 采用goroutine并行处理req
            // ######################### 这里是0RTT的核心逻辑 ##################################3
            rerr := s.handleRequest(sess, str, decoder, func() {
                sess.CloseWithError(quic.ApplicationErrorCode(errorFrameUnexpected), "")
            })
            if rerr.err != nil || rerr.streamErr != 0 || rerr.connErr != 0 {
                s.logger.Debugf("Handling request failed: %s", err)
                if rerr.streamErr != 0 {
                    str.CancelWrite(quic.StreamErrorCode(rerr.streamErr))
                }
                if rerr.connErr != 0 {
                    var reason string
                    if rerr.err != nil {
                        reason = rerr.err.Error()
                    }
                    sess.CloseWithError(quic.ApplicationErrorCode(rerr.connErr), reason)
                }
                return
            }
            str.Close()
        }()
    }
}
```

  5.1.2. 0RTT核心逻辑

```go
func (s *Server) handleRequest(sess quic.Session, str quic.Stream, decoder *qpack.Decoder, onFrameError func()) requestError {
    frame, err := parseNextFrame(str)
    if err != nil {
        return newStreamError(errorRequestIncomplete, err)
    }
    hf, ok := frame.(*headersFrame)
    if !ok {
        return newConnError(errorFrameUnexpected, errors.New("expected first frame to be a HEADERS frame"))
    }
    if hf.Length > s.maxHeaderBytes() {
        return newStreamError(errorFrameError, fmt.Errorf("HEADERS frame too large: %d bytes (max: %d)", hf.Length, s.maxHeaderBytes()))
    }
    headerBlock := make([]byte, hf.Length)
    if _, err := io.ReadFull(str, headerBlock); err != nil {
        return newStreamError(errorRequestIncomplete, err)
    }
    hfs, err := decoder.DecodeFull(headerBlock)
    if err != nil {
        // TODO: use the right error code
        return newConnError(errorGeneralProtocolError, err)
    }
    req, err := requestFromHeaders(hfs)
    if err != nil {
        // TODO: use the right error code
        return newStreamError(errorGeneralProtocolError, err)
    }

    req.RemoteAddr = sess.RemoteAddr().String()
    req.Body = newRequestBody(str, onFrameError)

    if s.logger.Debug() {
        s.logger.Infof("%s %s%s, on stream %d", req.Method, req.Host, req.RequestURI, str.StreamID())
    } else {
        s.logger.Infof("%s %s%s", req.Method, req.Host, req.RequestURI)
    }

    ctx := str.Context()
    ctx = context.WithValue(ctx, ServerContextKey, s)
    ctx = context.WithValue(ctx, http.LocalAddrContextKey, sess.LocalAddr())
    req = req.WithContext(ctx)
    r := newResponseWriter(str, s.logger)
    defer func() {
        if !r.usedDataStream() {
            r.Flush()
        }
    }()
    handler := s.Handler
    if handler == nil {
        handler = http.DefaultServeMux
    }

    var panicked bool
    func() {
        defer func() {
            if p := recover(); p != nil {
                // Copied from net/http/server.go
                const size = 64 << 10
                buf := make([]byte, size)
                buf = buf[:runtime.Stack(buf, false)]
                s.logger.Errorf("http: panic serving: %v\n%s", p, buf)
                panicked = true
            }
        }()
        handler.ServeHTTP(r, req)
    }()

    if !r.usedDataStream() {
        if panicked {
            r.WriteHeader(500)
        } else {
            r.WriteHeader(200)
        }
        // If the EOF was read by the handler, CancelRead() is a no-op.
        str.CancelRead(quic.StreamErrorCode(errorNoError))
    }
    return requestError{}
}
```

至此就完成了处理一次HTTP请求的核心流程,当然上述代码只显示了最核心的处理逻辑,实际上需要处理的内容远远不止这么多。

5.2. 客户端

    5.2.1. 客户端基本数据结构定义

```go
const MethodGet0RTT = "GET_0RTT"    // 请求方式统一为0RTT

const (
    defaultUserAgent              = "quic-go HTTP/3"
    defaultMaxResponseHeaderBytes = 10 * 1 << 20 // 10 MB
)

var defaultQuicConfig = &quic.Config{
    MaxIncomingStreams: -1, // don't allow the server to create bidirectional streams
    KeepAlive:          true,
    Versions:           []protocol.VersionNumber{protocol.VersionTLS},
}

// 拨号地址,默认为本地地址
var dialAddr = quic.DialAddrEarly

// HTTP RoundTriper改写,不同于原本的TCP RoundTriper
type roundTripperOpts struct {
    DisableCompression bool
    EnableDatagram     bool
    MaxHeaderBytes     int64
}

// client is a HTTP3 client doing requests
type client struct {
    // tls连接配置
    tlsConf *tls.Config
    config  *quic.Config
    // roundtripper相关参数
    opts    *roundTripperOpts
    // 是否只调用一次
    dialOnce     sync.Once
    dialer       func(network, addr string, tlsCfg *tls.Config, cfg *quic.Config) (quic.EarlySession, error)
    handshakeErr error

    // 请求写容器
    requestWriter *requestWriter

    decoder *qpack.Decoder
    
    // 主机地址
    hostname string
    // quic session,用唯一uuid来标识一个连接
    session  quic.EarlySession

    logger utils.Logger
}
```

 5.2.2. 客户端发送请求

```go
func (c *client) dial() error {
    var err error
    if c.dialer != nil {
        c.session, err = c.dialer("udp", c.hostname, c.tlsConf, c.config)
    } else {
        c.session, err = dialAddr(c.hostname, c.tlsConf, c.config)
    }
    if err != nil {
        return err
    }

    // send the SETTINGs frame, using 0-RTT data, if possible
    go func() {
        if err := c.setupSession(); err != nil {
            c.logger.Debugf("Setting up session failed: %s", err)
            c.session.CloseWithError(quic.ApplicationErrorCode(errorInternalError), "")
        }
    }()

    go c.handleUnidirectionalStreams()
    return nil
}
```

客户端发送请求实际上调用的是`dial`函数,里面的逻辑相较于`Server`端很简单,主要就是客户端发送一次0RTT请求,之后再异步监听来自服务器的响应,这个和TCP原本的连接没有太大区别。
 

作者:NP437+王魏

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

571

社区成员

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

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