quic协议及核心源码分析

发布时间:2023年12月23日

quic协议

1、网络通信时,为了确保数据不丢包,早在几十年前就发明了tcp协议!然而此一时非彼一时,随着技术进步和业务需求增多,tcp也暴露了部分比较明显的缺陷,比如:

  •  建立连接的3次握手延迟大; TLS需要至少需要2个RTT,延迟也大

  • 协议缺陷可能导致syn反射类的DDOS攻击

  • tcp协议紧耦合到了操作系统,升级需要操作系统层面改动,无法快速、大面积推广升级补丁包

  • 对头阻塞:数据被分成sequence,一旦中间的sequence丢包,后面的sequence也不会处理

  • 中转设备僵化:路由器、交换机等设备“认死理”,比如只认80、443等端口,其他端口一律丢弃

为了解决这些问题,牛逼plus的google早在10年前,也就是2012年发布了基于UDP的quic协议!为啥不基于tcp了,因为tcp有上述5条缺陷的嘛,所以干脆“另起炉灶”重新开搞!

2、正式介绍前,先看一张图:quci在右边,底层用了udp的协议;自生实现了Multistreaming、tls、拥塞控制,然后支撑了上层的http/2,所以我个人理解quic是一个夹在应用层和传输层之间的协议!

上面“数落”了tcp协议的5点不是,quic又是怎么基于udp解决这些问题的了?quic 是基于 UDP 实现的协议,而 UDP 是不可靠的面向报文的协议,这和 TCP 基于 IP 层的实现并没有什么本质上的不同,都是:

  • 底层只负责尽力而为的,以 packet 为单位的传输;

  • 上层协议实现更关键的特性,如可靠,有序,安全等。

(1)由于quic并未改造udp,而是直接使用udp,所以不需要改动现有的操作系统,也兼容了现有的网络中转设备,这些都不需要做任何改动,所以quic部署的改造成本相对较低!但是quic毕竟是新的协议,在哪部署和使用了?只有应用层了!这个和操作系统是解耦的,全靠3环的app自己想办法实现(和之前介绍的协程是不是类似了?)!google已经开源了算法,下载连接见文章末尾的参考5;PS:微软也实现了QUIC协议,名称叫MsQuic,源码在这:https://github.com/microsoft/msquic;

这里多说几句:应用层app能操作的最底层协议就是传输层了。大家在用libc库编写通信代码时可以对指定的ip地址和端口收发数据,没法改自己的mac地址吧?也没法改自己的ip地址吧?这些都是操作系统内核封装的,app的开发人员是不需要、也是没法改变的,所以站在安全防护的角度,部分大厂基于传输层自研了类似quic的通信协议,逆向时需要人工挨个分析协议字段的含义了,现成的fiddler/charles/burpsuit等https/http的抓包工具是无效的,用wireshark这类工具抓包也无法自动解析这些厂家自研的协议!

(2)TCP连接需要3次握手,tls最少需要2次RTT,两个加起来一共要耗费5个RTT,究其原因一方面是 TCP 和 TLS 分层设计导致的:分层的设计需要每个逻辑层次分别建立自己的连接状态。另一方面是 TLS 的握手阶段复杂的密钥协商机制导致的,quic又是怎么改进的了?quic建立握手的步骤如下:

  • 客户端判断本地是否已有服务器的全部配置参数(证书配置信息),如果有则直接跳转到(5),否则继续 。

  • 客户端向服务器发送 inchoate client hello(CHLO) 消息,请求服务器传输配置参数。

  • 服务器收到 CHLO,回复 rejection(REJ) 消息,其中包含服务器的部分配置参数

  • 客户端收到 REJ,提取并存储服务器配置参数,跳回到 (1) 。

  • 客户端向服务器发送 full client hello 消息,开始正式握手,消息中包括客户端选择的公开数。此时客户端根据获取的服务器配置参数和自己选择的公开数,可以计算出初始密钥 K1。

  • 服务器收到 full client hello,如果不同意连接就回复 REJ,同(3);如果同意连接,根据客户端的公开数计算出初始密钥 K1,回复 server hello(SHLO) 消息, SHLO 用初始密钥 K1 加密,并且其中包含服务器选择的一个临时公开数。

  • 客户端收到服务器的回复,如果是 REJ 则情况同(4);如果是 SHLO,则尝试用初始密钥 K1 解密,提取出临时公开数。

  • 客户端和服务器根据临时公开数和初始密钥 K1,各自基于 SHA-256 算法推导出会话密钥 K2。

  • 双方更换为使用会话密钥 K2 通信,初始密钥 K1 此时已无用,QUIC 握手过程完毕。之后会话密钥 K2 更新的流程与以上过程类似,只是数据包中的某些字段略有不同。这里为啥不继续使用key1,而是要重新生成key2来加密了?核心是为了前向安全!万一key1泄漏,之前用key1加密的数据全都被解密。所以为了前向安全,每次通信时会重新生成key2加密!

总的来说:

  • udp本身就不是面向连接的协议,所以省略了tcp 3次握手连接的耗时;直接通过事先内置的服务器参数发起通信请求;

  • 既然不是面向连接的,怎么确保所有的数据都能到达了?通过stream id和stream offset确保数据包不会丢失,接收方能收到完整的全量数据

  • 第一次用DH算法计算对称加密的密钥需要1个RTT;后续每次都用这个缓存的密钥加密,又省了一个RTT;本质上是把tcp的打招呼、握手,还有tls交换密钥的工作在1个RTT中全做了,这就是相比于tcp实现的tls效率高的根本原因!

注意:通信双方用于密钥交换的DH算法无法防止中间人攻击,所以仅通过密钥交换是无法防止被抓包的,所以还要通过证书等其他方式验证身份!x音就是通过libboringssl.so(google开源的一个openssl分支)SSL_CTX_set_custom_verify函数验证客户端是否是原来的client,而不是抓包软件!

相关视频推荐

Linux C/C++开发(后端/音视频/游戏/嵌入式/高性能网络/存储/基础架构/安全)icon-default.png?t=N7T8https://link.zhihu.com/?target=https%3A//ke.qq.com/course/417774%3FflowToken%3D1013300

需要C/C++ Linux服务器架构师学习资料加qun812855908获取(资料包括C/C++,Linux,golang技术,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK,ffmpeg等),免费分享

(3)拥塞控制:QUIC 使用可插拔的拥塞控制,相较于 TCP,它能提供更丰富的拥塞控制信息。比如对于每一个包,不管是原始包还是重传包,都带有一个新的序列号(seq),这使得 QUIC 能够区分 ACK 是重传包还是原始包,从而避免了 TCP 重传模糊的问题。QUIC 同时还带有收到数据包与发出 ACK 之间的时延信息。这些信息能够帮助更精确的计算 RTT!同时,因为quic不依赖操作系统,而是在应用层实现,所以开发人员对于quic有非常强的操控能力:完全可以根据不同的业务场景,实现和配置不同的拥塞控制算法以及参数;比如Google 提出的 BBR 拥塞控制算法与 CUBIC 是思路完全不一样的算法,在弱网和一定丢包场景,BBR 比 CUBIC 更不敏感,性能也更好;

(4)队头阻塞:TCP 为了保证可靠性,使用了基于字节序号的 Sequence Number 及 Ack 来确认消息的有序到达;一旦中间某个sequence的包丢失,哪怕是这个sequence后面的数据已经到达接收端,操作系统也不会立即把数据发给上层的应用来接受处理,而是一直等待发送端重新发送丢失的sequence包,举例如下:

应用层可以顺利读取 stream1 中的内容,但由于 stream2 中的第三个 segment 发生了丢包,TCP 为了保证数据的可靠性,需要发送端重传第 3 个 segment 才能通知应用层读取接下去的数据。所以即使 stream3、stream4 的内容已顺利抵达,应用层仍然无法读取,只能等待 stream2 中丢失的包进行重传。在弱网环境下,HTTP2 的队头阻塞问题在用户体验上极为糟糕!quic是怎么既确保数据传输可靠不丢失,又解决队头阻塞的这个问题的了?

对于数据包的传输,肯定是要编号的,否则接受方在拼接这些数据包的时候怎么知道顺序了?quic协议用Packet Number 代替了 TCP 的 Sequence Number,不同之处在于:

  • 每个 Packet Number 都严格递增,也就是说就算 Packet N 丢失了,重传的 Packet N 的 Packet Number 已经不是 N,而是一个比 N 大的值,比如Packet N+M;

  • 数据包支持乱序确认,不再要求 TCP 那样必须有序确认

当数据包 Packet N 丢失后,只要有新的已接收数据包确认,当前窗口就会继续向右滑动。待发送端获知数据包 Packet N 丢失后,会将需要重传的数据包放到待发送队列,重新编号比如数据包 Packet N+M 后重新发送给接收端,对重传数据包的处理跟发送新的数据包类似,这样就不会因为丢包重传将当前窗口阻塞在原地,从而解决了队头阻塞问题;但是问题又来了:怎么确认Package N+M就是重传PackageN的数据包了?这就涉及到quic另一个重要的特性了:多路复用!比如用户访问某个网页,这个页面有两个文件,分别是index.htm和index.js,可以同时、分别传输这两个文件!每个传输的stream都有各自的id,所以可以通过id确认是哪个stream超时丢包了!但包的Packet 编号是N+M,怎么进一步确认就是重传的Packet N包了?这就需要另一个重要的变量了:offset!怎么样,单从英语是不是就能猜到这个变量的作用了?每个数据包都有个offset字段,用于标识在stream id中的偏移!接收方完全可以根据offset来拼接收到的数据包!

总结:quic协议可以在乱序发送的情况下任然可靠不丢失,靠的就是每个数据包的offset字段;再搭配上stream id字段,接收方完全可以在乱序的情况下无误拼接收到的数据包了!

(4)除了以上通过stream id和stream offset确保数据不丢失外,quic还采用了另一个叫向前纠错 (Forward Error Correction,FEC)的校验方式:即每个数据包除了它本身的内容之外,还包括了部分其他数据包的数据,因此少量的丢包可以通过其他包的冗余数据直接组装而无需重传。向前纠错牺牲了每个数据包可以发送数据的上限,但是减少了因为丢包导致的数据重传,因为数据重传将会消耗更多的时间(包括确认数据包丢失、请求重传、等待新数据包等步骤的时间消耗);这个原理和纠删码没有本质区别!

(5)通信双方不论使用何种协议,发送的数据必须事前约定好格式,否则接受方怎么从数据包(本质就是一段字符串)中解析和提取关键的信息了?quic协议的格式如下:

数据包中除了个别报文比如 PUBLIC_RESET 和 CHLO,所有报文头部(上图红色部分)都是经过认证的(哈希散列值),报文 Body (上图绿色部分)都是经过加密的,这样只要对 QUIC 报文任何修改,接收端都能够及时发现;每个字段的含义如下:

  • Flags:用于表示 Connection ID 长度、Packet Number 长度等信息;

  • Connection ID:客户端随机选择的最大长度为64位的无符号整数,用于标识连接;如果app更换了ip地址(比如wifi和4G之间切换了),仍然可以通过这个id和服务端在0 RTT下通信!

  • QUIC Version:QUIC 协议的版本号,32 位的可选字段。如果 Public Flag & FLAG_VERSION != 0,这个字段必填。客户端设置 Public Flag 中的 Bit0 为1,并且填写期望的版本号。如果客户端期望的版本号服务端不支持,服务端设置 Public Flag 中的 Bit0 为1,并且在该字段中列出服务端支持的协议版本(0或者多个),并且该字段后不能有任何报文;

  • Packet Number:长度取决于 Public Flag 中 Bit4 及 Bit5 两位的值,最大长度 6 字节。发送端在每个普通报文中设置 Packet Number。发送端发送的第一个包的序列号是 1,随后的数据包中的序列号的都大于前一个包中的序列号;

  • Stream ID:用于标识当前数据流属于哪个资源请求,用于消除队头阻塞;

  • Offset:标识当前数据包在当前 Stream ID 中的字节偏移量,用于消除队头阻塞。

(6)为了便于理解和记忆,这里把quic的要点做了总结,如下:

3、正式因为quic有这么多优点,国内很多互联网一、二线厂商都开始采用,其中比较著名的app就是x音了!lib库中有个libsscronet.so就支持quic协议!

quic协议核心源码

quic协议最早是google提出来的,所以狗家的源码肯定是最“正宗”的!

1、quic相比tcp实现的tls,前面省略了3~4个RTT,根因就是发起连接请求时就发送自己的公钥给对方,让对方利用自己的公钥计算后续对称加密的key,这就是所谓的handshake;在libquic-master\src\net\quic\core\quic_crypto_client_stream.cc中有具体实现握手的代码,先看DoHandshakeLoop函数:

void QuicCryptoClientStream::DoHandshakeLoop(const CryptoHandshakeMessage* in) {
  QuicCryptoClientConfig::CachedState* cached =
      crypto_config_->LookupOrCreate(server_id_);

  QuicAsyncStatus rv = QUIC_SUCCESS;
  do {
    CHECK_NE(STATE_NONE, next_state_);
    const State state = next_state_;
    next_state_ = STATE_IDLE;
    rv = QUIC_SUCCESS;
    switch (state) {
      case STATE_INITIALIZE:
        DoInitialize(cached);
        break;
      case STATE_SEND_CHLO:
        DoSendCHLO(cached);
        return;  // return waiting to hear from server.
      case STATE_RECV_REJ:
        DoReceiveREJ(in, cached);
        break;
      case STATE_VERIFY_PROOF:
        rv = DoVerifyProof(cached);
        break;
      case STATE_VERIFY_PROOF_COMPLETE:
        DoVerifyProofComplete(cached);
        break;
      case STATE_GET_CHANNEL_ID:
        rv = DoGetChannelID(cached);
        break;
      case STATE_GET_CHANNEL_ID_COMPLETE:
        DoGetChannelIDComplete();
        break;
      case STATE_RECV_SHLO:
        DoReceiveSHLO(in, cached);
        break;
      case STATE_IDLE:
        // This means that the peer sent us a message that we weren't expecting.
        CloseConnectionWithDetails(QUIC_INVALID_CRYPTO_MESSAGE_TYPE,
                                   "Handshake in idle state");
        return;
      case STATE_INITIALIZE_SCUP:
        DoInitializeServerConfigUpdate(cached);
        break;
      case STATE_NONE:
        NOTREACHED();
        return;  // We are done.
    }
  } while (rv != QUIC_PENDING && next_state_ != STATE_NONE);
}

只要quic的状态不是pending,并且下一个状态不是NONE,就根据不同的状态调用不同的处理函数!具体发送handshake小的函数是DoSendCHLO,代码如下:

/*发送client hello消息*/
void QuicCryptoClientStream::DoSendCHLO(
    QuicCryptoClientConfig::CachedState* cached) {
  if (stateless_reject_received_) {//如果收到了server拒绝的消息
    // If we've gotten to this point, we've sent at least one hello
    // and received a stateless reject in response.  We cannot
    // continue to send hellos because the server has abandoned state
    // for this connection.  Abandon further handshakes.
    next_state_ = STATE_NONE;
    if (session()->connection()->connected()) {
      session()->connection()->CloseConnection(//关闭连接
          QUIC_CRYPTO_HANDSHAKE_STATELESS_REJECT, "stateless reject received",
          ConnectionCloseBehavior::SILENT_CLOSE);
    }
    return;
  }

  // Send the client hello in plaintext.
  //注意:这是client hello消息,没必要加密
  session()->connection()->SetDefaultEncryptionLevel(ENCRYPTION_NONE);
  encryption_established_ = false;
  if (num_client_hellos_ > kMaxClientHellos) {//握手消息已经发送了很多,不能再发了
    CloseConnectionWithDetails(
        QUIC_CRYPTO_TOO_MANY_REJECTS,
        base::StringPrintf("More than %u rejects", kMaxClientHellos).c_str());
    return;
  }
  num_client_hellos_++;
  //开始构造握手消息了
  CryptoHandshakeMessage out;
  DCHECK(session() != nullptr);
  DCHECK(session()->config() != nullptr);
  // Send all the options, regardless of whether we're sending an
  // inchoate or subsequent hello.
  /*填充握手消息的各个字段*/
  session()->config()->ToHandshakeMessage(&out);

  // Send a local timestamp to the server.
  out.SetValue(kCTIM,
               session()->connection()->clock()->WallNow().ToUNIXSeconds());

  if (!cached->IsComplete(session()->connection()->clock()->WallNow())) {
    crypto_config_->FillInchoateClientHello(
        server_id_, session()->connection()->supported_versions().front(),
        cached, session()->connection()->random_generator(),
        /* demand_x509_proof= */ true, &crypto_negotiated_params_, &out);
    // Pad the inchoate client hello to fill up a packet.
    const QuicByteCount kFramingOverhead = 50;  // A rough estimate.
    const QuicByteCount max_packet_size =
        session()->connection()->max_packet_length();
    if (max_packet_size <= kFramingOverhead) {
      DLOG(DFATAL) << "max_packet_length (" << max_packet_size
                   << ") has no room for framing overhead.";
      CloseConnectionWithDetails(QUIC_INTERNAL_ERROR,
                                 "max_packet_size too smalll");
      return;
    }
    if (kClientHelloMinimumSize > max_packet_size - kFramingOverhead) {
      DLOG(DFATAL) << "Client hello won't fit in a single packet.";
      CloseConnectionWithDetails(QUIC_INTERNAL_ERROR, "CHLO too large");
      return;
    }
    // TODO(rch): Remove this when we remove:
    // FLAGS_quic_use_chlo_packet_size
    out.set_minimum_size(
        static_cast<size_t>(max_packet_size - kFramingOverhead));
    next_state_ = STATE_RECV_REJ;
    /*做hash签名,接收方会根据hash验证消息是否完整*/
    CryptoUtils::HashHandshakeMessage(out, &chlo_hash_);
    //发送消息
    SendHandshakeMessage(out);
    return;
  }

  // If the server nonce is empty, copy over the server nonce from a previous
  // SREJ, if there is one. 
  if (FLAGS_enable_quic_stateless_reject_support &&
      crypto_negotiated_params_.server_nonce.empty() &&
      cached->has_server_nonce()) {
    crypto_negotiated_params_.server_nonce = cached->GetNextServerNonce();
    DCHECK(!crypto_negotiated_params_.server_nonce.empty());
  }

  string error_details;
  /*继续填充client hello消息*/
  QuicErrorCode error = crypto_config_->FillClientHello(
      server_id_, session()->connection()->connection_id(),
      session()->connection()->version(),
      session()->connection()->supported_versions().front(), cached,
      session()->connection()->clock()->WallNow(),
      //这个随机数会被server用来计算生成对称加密的key
      session()->connection()->random_generator(), 
      channel_id_key_.get(),
      //保存了nonce、key、token相关信息;后续对称加密的方法是CTR,需要NONCE值
      &crypto_negotiated_params_,
      &out, &error_details);
  if (error != QUIC_NO_ERROR) {
    // Flush the cached config so that, if it's bad, the server has a
    // chance to send us another in the future.
    cached->InvalidateServerConfig();
    CloseConnectionWithDetails(error, error_details);
    return;
  }
  /*继续对消息做hash,便于server验证收到的消息是否完整*/
  CryptoUtils::HashHandshakeMessage(out, &chlo_hash_);
  channel_id_sent_ = (channel_id_key_.get() != nullptr);
  if (cached->proof_verify_details()) {
    proof_handler_->OnProofVerifyDetailsAvailable(
        *cached->proof_verify_details());
  }
  next_state_ = STATE_RECV_SHLO;
  SendHandshakeMessage(out);
  // Be prepared to decrypt with the new server write key.
  session()->connection()->SetAlternativeDecrypter(
      ENCRYPTION_INITIAL,
      crypto_negotiated_params_.initial_crypters.decrypter.release(),
      true /* latch once used */);
  // Send subsequent packets under encryption on the assumption that the
  // server will accept the handshake.
  session()->connection()->SetEncrypter(
      ENCRYPTION_INITIAL,
      crypto_negotiated_params_.initial_crypters.encrypter.release());
  session()->connection()->SetDefaultEncryptionLevel(ENCRYPTION_INITIAL);

  // TODO(ianswett): Merge ENCRYPTION_REESTABLISHED and
  // ENCRYPTION_FIRST_ESTABLSIHED
  encryption_established_ = true;
  session()->OnCryptoHandshakeEvent(QuicSession::ENCRYPTION_REESTABLISHED);
}

个人觉得最核心的代码就是FillClientHello函数了,这里会生成随机数,后续server会利用这个随机数生成对称加密的key!部分通信的参数也会通过这个函数的执行保存在crypto_negotiated_params_对象中!client发送了hello包,接下来该server处理这个包了,代码在libquic-master\src\net\quic\core\quic_crypto_server_stream.cc和quic_crypto_server_config.cc中,代码如下:核心功能是生成自己的公钥,还有后续对称加密的key!

QuicErrorCode QuicCryptoServerConfig::ProcessClientHello(
    const ValidateClientHelloResultCallback::Result& validate_chlo_result,
    bool reject_only,
    QuicConnectionId connection_id,
    const IPAddress& server_ip,
    const IPEndPoint& client_address,
    QuicVersion version,
    const QuicVersionVector& supported_versions,
    bool use_stateless_rejects,
    QuicConnectionId server_designated_connection_id,
    const QuicClock* clock,
    QuicRandom* rand,//发送给client用于计算对称key
    QuicCompressedCertsCache* compressed_certs_cache,
    QuicCryptoNegotiatedParameters* params,
    QuicCryptoProof* crypto_proof,
    QuicByteCount total_framing_overhead,
    QuicByteCount chlo_packet_size,
    CryptoHandshakeMessage* out,
    DiversificationNonce* out_diversification_nonce,
    string* error_details) const {
  DCHECK(error_details);

  const CryptoHandshakeMessage& client_hello =
      validate_chlo_result.client_hello;
  const ClientHelloInfo& info = validate_chlo_result.info;

  QuicErrorCode valid = CryptoUtils::ValidateClientHello(
      client_hello, version, supported_versions, error_details);
  if (valid != QUIC_NO_ERROR)
    return valid;

  StringPiece requested_scid;
  client_hello.GetStringPiece(kSCID, &requested_scid);
  const QuicWallTime now(clock->WallNow());

  scoped_refptr<Config> requested_config;
  scoped_refptr<Config> primary_config;
  {
    base::AutoLock locked(configs_lock_);

    if (!primary_config_.get()) {
      *error_details = "No configurations loaded";
      return QUIC_CRYPTO_INTERNAL_ERROR;
    }

    if (!next_config_promotion_time_.IsZero() &&
        next_config_promotion_time_.IsAfter(now)) {
      SelectNewPrimaryConfig(now);
      DCHECK(primary_config_.get());
      DCHECK_EQ(configs_.find(primary_config_->id)->second, primary_config_);
    }

    // Use the config that the client requested in order to do key-agreement.
    // Otherwise give it a copy of |primary_config_| to use.
    primary_config = crypto_proof->config;
    requested_config = GetConfigWithScid(requested_scid);
  }

  if (validate_chlo_result.error_code != QUIC_NO_ERROR) {
    *error_details = validate_chlo_result.error_details;
    return validate_chlo_result.error_code;
  }

  out->Clear();

  if (!ClientDemandsX509Proof(client_hello)) {
    *error_details = "Missing or invalid PDMD";
    return QUIC_UNSUPPORTED_PROOF_DEMAND;
  }
  DCHECK(proof_source_.get());
  string chlo_hash;
  CryptoUtils::HashHandshakeMessage(client_hello, &chlo_hash);
  // No need to get a new proof if one was already generated.
  if (!crypto_proof->chain &&
      !proof_source_->GetProof(server_ip, info.sni.as_string(),
                               primary_config->serialized, version, chlo_hash,
                               &crypto_proof->chain, &crypto_proof->signature,
                               &crypto_proof->cert_sct)) {
    return QUIC_HANDSHAKE_FAILED;
  }

  StringPiece cert_sct;
  if (client_hello.GetStringPiece(kCertificateSCTTag, &cert_sct) &&
      cert_sct.empty()) {
    params->sct_supported_by_client = true;
  }

  if (!info.reject_reasons.empty() || !requested_config.get()) {
    BuildRejection(version, clock->WallNow(), *primary_config, client_hello,
                   info, validate_chlo_result.cached_network_params,
                   use_stateless_rejects, server_designated_connection_id, rand,
                   compressed_certs_cache, params, *crypto_proof,
                   total_framing_overhead, chlo_packet_size, out);
    return QUIC_NO_ERROR;
  }

  if (reject_only) {
    return QUIC_NO_ERROR;
  }

  const QuicTag* their_aeads;
  const QuicTag* their_key_exchanges;
  size_t num_their_aeads, num_their_key_exchanges;
  if (client_hello.GetTaglist(kAEAD, &their_aeads, &num_their_aeads) !=
          QUIC_NO_ERROR ||
      client_hello.GetTaglist(kKEXS, &their_key_exchanges,
                              &num_their_key_exchanges) != QUIC_NO_ERROR ||
      num_their_aeads != 1 || num_their_key_exchanges != 1) {
    *error_details = "Missing or invalid AEAD or KEXS";
    return QUIC_INVALID_CRYPTO_MESSAGE_PARAMETER;
  }

  size_t key_exchange_index;
  if (!QuicUtils::FindMutualTag(requested_config->aead, their_aeads,
                                num_their_aeads, QuicUtils::LOCAL_PRIORITY,
                                &params->aead, nullptr) ||
      !QuicUtils::FindMutualTag(requested_config->kexs, their_key_exchanges,
                                num_their_key_exchanges,
                                QuicUtils::LOCAL_PRIORITY,
                                &params->key_exchange, &key_exchange_index)) {
    *error_details = "Unsupported AEAD or KEXS";
    return QUIC_CRYPTO_NO_SUPPORT;
  }

  if (!requested_config->tb_key_params.empty()) {
    const QuicTag* their_tbkps;
    size_t num_their_tbkps;
    switch (client_hello.GetTaglist(kTBKP, &their_tbkps, &num_their_tbkps)) {
      case QUIC_CRYPTO_MESSAGE_PARAMETER_NOT_FOUND:
        break;
      case QUIC_NO_ERROR:
        if (QuicUtils::FindMutualTag(
                requested_config->tb_key_params, their_tbkps, num_their_tbkps,
                QuicUtils::LOCAL_PRIORITY, &params->token_binding_key_param,
                nullptr)) {
          break;
        }
      default:
        *error_details = "Invalid Token Binding key parameter";
        return QUIC_INVALID_CRYPTO_MESSAGE_PARAMETER;
    }
  }

  StringPiece public_value;
  /*提取client hello数据包发送的公钥,server要用来生成对称加密的key*/
  if (!client_hello.GetStringPiece(kPUBS, &public_value)) {
    *error_details = "Missing public value";
    return QUIC_INVALID_CRYPTO_MESSAGE_PARAMETER;
  }

  const KeyExchange* key_exchange =
      requested_config->key_exchanges[key_exchange_index];
  if (!key_exchange->CalculateSharedKey(public_value,
                                        &params->initial_premaster_secret)) {
    *error_details = "Invalid public value";
    return QUIC_INVALID_CRYPTO_MESSAGE_PARAMETER;
  }

  if (!info.sni.empty()) {
    std::unique_ptr<char[]> sni_tmp(new char[info.sni.length() + 1]);
    memcpy(sni_tmp.get(), info.sni.data(), info.sni.length());
    sni_tmp[info.sni.length()] = 0;
    params->sni = CryptoUtils::NormalizeHostname(sni_tmp.get());
  }

  string hkdf_suffix;
  //client hello消息序列化,便于提取?
  const QuicData& client_hello_serialized = client_hello.GetSerialized();
  /*根据一个原始密钥材料,用hkdf算法推导出指定长度的密钥;
    这里明显是要根据client hello的数据生成对称加密的密钥了
  */
  hkdf_suffix.reserve(sizeof(connection_id) + client_hello_serialized.length() +
                      requested_config->serialized.size());
  hkdf_suffix.append(reinterpret_cast<char*>(&connection_id),
                     sizeof(connection_id));
  hkdf_suffix.append(client_hello_serialized.data(),
                     client_hello_serialized.length());
  hkdf_suffix.append(requested_config->serialized);
  DCHECK(proof_source_.get());
  if (crypto_proof->chain->certs.empty()) {
    *error_details = "Failed to get certs";
    return QUIC_CRYPTO_INTERNAL_ERROR;
  }
  hkdf_suffix.append(crypto_proof->chain->certs.at(0));

  StringPiece cetv_ciphertext;
  if (requested_config->channel_id_enabled &&
      client_hello.GetStringPiece(kCETV, &cetv_ciphertext)) {
    CryptoHandshakeMessage client_hello_copy(client_hello);
    client_hello_copy.Erase(kCETV);
    client_hello_copy.Erase(kPAD);

    const QuicData& client_hello_copy_serialized =
        client_hello_copy.GetSerialized();
    string hkdf_input;
    hkdf_input.append(QuicCryptoConfig::kCETVLabel,
                      strlen(QuicCryptoConfig::kCETVLabel) + 1);
    hkdf_input.append(reinterpret_cast<char*>(&connection_id),
                      sizeof(connection_id));
    hkdf_input.append(client_hello_copy_serialized.data(),
                      client_hello_copy_serialized.length());
    hkdf_input.append(requested_config->serialized);

    CrypterPair crypters;
    if (!CryptoUtils::DeriveKeys(params->initial_premaster_secret, params->aead,
                                 info.client_nonce, info.server_nonce,
                                 hkdf_input, Perspective::IS_SERVER,
                                 CryptoUtils::Diversification::Never(),
                                 &crypters, nullptr /* subkey secret */)) {
      *error_details = "Symmetric key setup failed";
      return QUIC_CRYPTO_SYMMETRIC_KEY_SETUP_FAILED;
    }

    char plaintext[kMaxPacketSize];
    size_t plaintext_length = 0;
    const bool success = crypters.decrypter->DecryptPacket(
        kDefaultPathId, 0 /* packet number */,
        StringPiece() /* associated data */, cetv_ciphertext, plaintext,
        &plaintext_length, kMaxPacketSize);
    if (!success) {
      *error_details = "CETV decryption failure";
      return QUIC_INVALID_CRYPTO_MESSAGE_PARAMETER;
    }
    std::unique_ptr<CryptoHandshakeMessage> cetv(
        CryptoFramer::ParseMessage(StringPiece(plaintext, plaintext_length)));
    if (!cetv.get()) {
      *error_details = "CETV parse error";
      return QUIC_INVALID_CRYPTO_MESSAGE_PARAMETER;
    }

    StringPiece key, signature;
    if (cetv->GetStringPiece(kCIDK, &key) &&
        cetv->GetStringPiece(kCIDS, &signature)) {
      if (!ChannelIDVerifier::Verify(key, hkdf_input, signature)) {
        *error_details = "ChannelID signature failure";
        return QUIC_INVALID_CRYPTO_MESSAGE_PARAMETER;
      }

      params->channel_id = key.as_string();
    }
  }

  string hkdf_input;
  size_t label_len = strlen(QuicCryptoConfig::kInitialLabel) + 1;
  hkdf_input.reserve(label_len + hkdf_suffix.size());
  hkdf_input.append(QuicCryptoConfig::kInitialLabel, label_len);
  hkdf_input.append(hkdf_suffix);

  string* subkey_secret = &params->initial_subkey_secret;
  CryptoUtils::Diversification diversification =
      CryptoUtils::Diversification::Never();
  if (version > QUIC_VERSION_32) {
    rand->RandBytes(out_diversification_nonce->data(),
                    out_diversification_nonce->size());
    diversification =
        CryptoUtils::Diversification::Now(out_diversification_nonce);
  }

  if (!CryptoUtils::DeriveKeys(params->initial_premaster_secret, params->aead,
                               info.client_nonce, info.server_nonce, hkdf_input,
                               Perspective::IS_SERVER, diversification,
                               &params->initial_crypters, subkey_secret)) {
    *error_details = "Symmetric key setup failed";
    return QUIC_CRYPTO_SYMMETRIC_KEY_SETUP_FAILED;
  }

  string forward_secure_public_value;
  if (ephemeral_key_source_.get()) {
    params->forward_secure_premaster_secret =
        ephemeral_key_source_->CalculateForwardSecureKey(
            key_exchange, rand, clock->ApproximateNow(), public_value,
            &forward_secure_public_value);
  } else {
    std::unique_ptr<KeyExchange> forward_secure_key_exchange(
        key_exchange->NewKeyPair(rand));
    forward_secure_public_value =
        forward_secure_key_exchange->public_value().as_string();
    /*生成共享密钥*/
    if (!forward_secure_key_exchange->CalculateSharedKey(
            public_value, &params->forward_secure_premaster_secret)) {
      *error_details = "Invalid public value";
      return QUIC_INVALID_CRYPTO_MESSAGE_PARAMETER;
    }
  }

  string forward_secure_hkdf_input;
  label_len = strlen(QuicCryptoConfig::kForwardSecureLabel) + 1;
  forward_secure_hkdf_input.reserve(label_len + hkdf_suffix.size());
  forward_secure_hkdf_input.append(QuicCryptoConfig::kForwardSecureLabel,
                                   label_len);
  forward_secure_hkdf_input.append(hkdf_suffix);

  string shlo_nonce;
  shlo_nonce = NewServerNonce(rand, info.now);
  out->SetStringPiece(kServerNonceTag, shlo_nonce);
  /*生成密钥*/
  if (!CryptoUtils::DeriveKeys(
          params->forward_secure_premaster_secret, params->aead,
          info.client_nonce,
          shlo_nonce.empty() ? info.server_nonce : shlo_nonce,
          forward_secure_hkdf_input, Perspective::IS_SERVER,
          CryptoUtils::Diversification::Never(),
          &params->forward_secure_crypters, &params->subkey_secret)) {
    *error_details = "Symmetric key setup failed";
    return QUIC_CRYPTO_SYMMETRIC_KEY_SETUP_FAILED;
  }

  out->set_tag(kSHLO);
  QuicTagVector supported_version_tags;
  for (size_t i = 0; i < supported_versions.size(); ++i) {
    supported_version_tags.push_back(
        QuicVersionToQuicTag(supported_versions[i]));
  }
  out->SetVector(kVER, supported_version_tags);
  out->SetStringPiece(
      kSourceAddressTokenTag,
      NewSourceAddressToken(*requested_config.get(), info.source_address_tokens,
                            client_address.address(), rand, info.now, nullptr));
  QuicSocketAddressCoder address_coder(client_address);
  out->SetStringPiece(kCADR, address_coder.Encode());
  /*server hello包中设置server的公钥,后续client会利用这个生成对称加密的key*/
  out->SetStringPiece(kPUBS, forward_secure_public_value);

  return QUIC_NO_ERROR;
}

这里用了不同的方法来生成对称加密的key。这里以椭圆曲线为例,计算对称加密key的代码如下:这是直接调用了openssl/curve25519.h的接口计算出来的。一旦双方都生成了对称密钥,后续就可以通过对称加密通信了!

bool Curve25519KeyExchange::CalculateSharedKey(StringPiece peer_public_value,
                                               string* out_result) const {
  if (peer_public_value.size() != crypto::curve25519::kBytes) {
    return false;
  }

  uint8_t result[crypto::curve25519::kBytes];
  if (!crypto::curve25519::ScalarMult(
          private_key_,
          reinterpret_cast<const uint8_t*>(peer_public_value.data()), result)) {
    return false;
  }
  out_result->assign(reinterpret_cast<char*>(result), sizeof(result));

  return true;
}
bool ScalarMult(const uint8_t* private_key,
                const uint8_t* peer_public_key,
                uint8_t* shared_key) {
  return !!X25519(shared_key, private_key, peer_public_key);
}

通信时给packet加密的方法:

bool AeadBaseEncrypter::EncryptPacket(QuicPathId path_id,
                                      QuicPacketNumber packet_number,
                                      StringPiece associated_data,
                                      StringPiece plaintext,
                                      char* output,
                                      size_t* output_length,
                                      size_t max_output_length) {
  size_t ciphertext_size = GetCiphertextSize(plaintext.length());
  if (max_output_length < ciphertext_size) {
    return false;
  }
  // TODO(ianswett): Introduce a check to ensure that we don't encrypt with the
  // same packet number twice.
  const size_t nonce_size = nonce_prefix_size_ + sizeof(packet_number);
  ALIGNAS(4) char nonce_buffer[kMaxNonceSize];
  memcpy(nonce_buffer, nonce_prefix_, nonce_prefix_size_);
  uint64_t path_id_packet_number =
      QuicUtils::PackPathIdAndPacketNumber(path_id, packet_number);
  memcpy(nonce_buffer + nonce_prefix_size_, &path_id_packet_number,
         sizeof(path_id_packet_number));
  /*这里用nonce给明文加密*/
  if (!Encrypt(StringPiece(nonce_buffer, nonce_size), associated_data,
               plaintext, reinterpret_cast<unsigned char*>(output))) {
    return false;
  }
  *output_length = ciphertext_size;
  return true;
}

最后,server hello消息是从这里发出去的,并且在某些情况下server hello已经用server新生成的key加密了,如下:

void QuicCryptoServerStream::FinishProcessingHandshakeMessage(
    const ValidateClientHelloResultCallback::Result& result,
    std::unique_ptr<ProofSource::Details> details) {
  const CryptoHandshakeMessage& message = result.client_hello;

  // Clear the callback that got us here.
  DCHECK(validate_client_hello_cb_ != nullptr);
  validate_client_hello_cb_ = nullptr;

  if (use_stateless_rejects_if_peer_supported_) {
    peer_supports_stateless_rejects_ = DoesPeerSupportStatelessRejects(message);
  }

  CryptoHandshakeMessage reply;
  DiversificationNonce diversification_nonce;
  string error_details;
  QuicErrorCode error = 
  /*server处理client的hello消息:重点是生成对称加密key、自己的公钥和nonce
   同时生成给client回复的消息*/
      ProcessClientHello(result, std::move(details), &reply,
                         &diversification_nonce, &error_details);

  if (error != QUIC_NO_ERROR) {
    CloseConnectionWithDetails(error, error_details);
    return;
  }

  if (reply.tag() != kSHLO) {
    if (reply.tag() == kSREJ) {
      DCHECK(use_stateless_rejects_if_peer_supported_);
      DCHECK(peer_supports_stateless_rejects_);
      // Before sending the SREJ, cause the connection to save crypto packets
      // so that they can be added to the time wait list manager and
      // retransmitted.
      session()->connection()->EnableSavingCryptoPackets();
    }
    SendHandshakeMessage(reply);//给client发server hello

    if (reply.tag() == kSREJ) {
      DCHECK(use_stateless_rejects_if_peer_supported_);
      DCHECK(peer_supports_stateless_rejects_);
      DCHECK(!handshake_confirmed());
      DVLOG(1) << "Closing connection "
               << session()->connection()->connection_id()
               << " because of a stateless reject.";
      session()->connection()->CloseConnection(
          QUIC_CRYPTO_HANDSHAKE_STATELESS_REJECT, "stateless reject",
          ConnectionCloseBehavior::SILENT_CLOSE);
    }
    return;
  }

  // If we are returning a SHLO then we accepted the handshake.  Now
  // process the negotiated configuration options as part of the
  // session config.
  //代码到这里已经给client发送了client hello,表示server已经准备好接受数据了
  //这里保存一些双方协商好的通信配置
  QuicConfig* config = session()->config();
  OverrideQuicConfigDefaults(config);
  error = config->ProcessPeerHello(message, CLIENT, &error_details);
  if (error != QUIC_NO_ERROR) {
    CloseConnectionWithDetails(error, error_details);
    return;
  }

  session()->OnConfigNegotiated();

  config->ToHandshakeMessage(&reply);

  // Receiving a full CHLO implies the client is prepared to decrypt with
  // the new server write key.  We can start to encrypt with the new server
  // write key. 可以开始用服务端新生成的key解密数据了
  //
  // NOTE: the SHLO will be encrypted with the new server write key.
  /*既然在server已经生成了对称加密的key,这里可以用这个key加密server hello消息*/
  session()->connection()->SetEncrypter(
      ENCRYPTION_INITIAL,
      crypto_negotiated_params_.initial_crypters.encrypter.release());
  session()->connection()->SetDefaultEncryptionLevel(ENCRYPTION_INITIAL);
  // Set the decrypter immediately so that we no longer accept unencrypted
  // packets.
  session()->connection()->SetDecrypter(
      ENCRYPTION_INITIAL,
      crypto_negotiated_params_.initial_crypters.decrypter.release());
  if (version() > QUIC_VERSION_32) {
    session()->connection()->SetDiversificationNonce(diversification_nonce);
  }

  SendHandshakeMessage(reply);//发送server hello

  session()->connection()->SetEncrypter(
      ENCRYPTION_FORWARD_SECURE,
      crypto_negotiated_params_.forward_secure_crypters.encrypter.release());
  session()->connection()->SetDefaultEncryptionLevel(ENCRYPTION_FORWARD_SECURE);

  session()->connection()->SetAlternativeDecrypter(
      ENCRYPTION_FORWARD_SECURE,
      crypto_negotiated_params_.forward_secure_crypters.decrypter.release(),
      false /* don't latch */);

  encryption_established_ = true;
  handshake_confirmed_ = true;
  session()->OnCryptoHandshakeEvent(QuicSession::HANDSHAKE_CONFIRMED);
}

(2)为了防止tcp的队头阻塞,quic在前面丢包的情况下任然继续发包,丢的包用新的packet number重新发,怎么区别这个新包是以往丢包的重发了?核心是每个包都有stream id和stream offset字段,根据这两个字段定位包的位置,而不是packet number。整个包结构定义的类在这里:

struct NET_EXPORT_PRIVATE QuicStreamFrame {
  QuicStreamFrame();
  QuicStreamFrame(QuicStreamId stream_id,
                  bool fin,
                  QuicStreamOffset offset,
                  base::StringPiece data);
  QuicStreamFrame(QuicStreamId stream_id,
                  bool fin,
                  QuicStreamOffset offset,
                  QuicPacketLength data_length,
                  UniqueStreamBuffer buffer);
  ~QuicStreamFrame();

  NET_EXPORT_PRIVATE friend std::ostream& operator<<(std::ostream& os,
                                                     const QuicStreamFrame& s);

  QuicStreamId stream_id;
  bool fin;
  QuicPacketLength data_length;
  const char* data_buffer;
  QuicStreamOffset offset;  // Location of this data in the stream.
  // nullptr when the QuicStreamFrame is received, and non-null when sent.
  UniqueStreamBuffer buffer;

 private:
  QuicStreamFrame(QuicStreamId stream_id,
                  bool fin,
                  QuicStreamOffset offset,
                  const char* data_buffer,
                  QuicPacketLength data_length,
                  UniqueStreamBuffer buffer);

  DISALLOW_COPY_AND_ASSIGN(QuicStreamFrame);
};

收到后自然要把payload取出来拼接成完整的数据,stream id和stream offset必不可少,拼接和处理的逻辑在这里:里面涉及到很多duplicate冗余去重的动作,都是依据offset来判断的!

QuicErrorCode QuicStreamSequencerBuffer::OnStreamData(
    QuicStreamOffset starting_offset,
    base::StringPiece data,
    QuicTime timestamp,
    size_t* const bytes_buffered,
    std::string* error_details) {
  *bytes_buffered = 0;
  QuicStreamOffset offset = starting_offset;
  size_t size = data.size();
  if (size == 0) {
    *error_details = "Received empty stream frame without FIN.";
    return QUIC_EMPTY_STREAM_FRAME_NO_FIN;
  }

  // Find the first gap not ending before |offset|. This gap maybe the gap to
  // fill if the arriving frame doesn't overlaps with previous ones.
  std::list<Gap>::iterator current_gap = gaps_.begin();
  while (current_gap != gaps_.end() && current_gap->end_offset <= offset) {
    ++current_gap;
  }

  DCHECK(current_gap != gaps_.end());

  // "duplication": might duplicate with data alread filled,but also might
  // overlap across different base::StringPiece objects already written.
  // In both cases, don't write the data,
  // and allow the caller of this method to handle the result.
  if (offset < current_gap->begin_offset &&
      offset + size <= current_gap->begin_offset) {
    DVLOG(1) << "Duplicated data at offset: " << offset << " length: " << size;
    return QUIC_NO_ERROR;
  }
  if (offset < current_gap->begin_offset &&
      offset + size > current_gap->begin_offset) {
    // Beginning of new data overlaps data before current gap.
    *error_details =
        string("Beginning of received data overlaps with buffered data.\n") +
        "New frame range " + RangeDebugString(offset, offset + size) +
        " with first 128 bytes: " +
        string(data.data(), data.length() < 128 ? data.length() : 128) +
        "\nCurrently received frames: " + ReceivedFramesDebugString() +
        "\nCurrent gaps: " + GapsDebugString();
    return QUIC_OVERLAPPING_STREAM_DATA;
  }
  if (offset + size > current_gap->end_offset) {
    // End of new data overlaps with data after current gap.
    *error_details =
        string("End of received data overlaps with buffered data.\n") +
        "New frame range " + RangeDebugString(offset, offset + size) +
        " with first 128 bytes: " +
        string(data.data(), data.length() < 128 ? data.length() : 128) +
        "\nCurrently received frames: " + ReceivedFramesDebugString() +
        "\nCurrent gaps: " + GapsDebugString();
    return QUIC_OVERLAPPING_STREAM_DATA;
  }

  // Write beyond the current range this buffer is covering.
  if (offset + size > total_bytes_read_ + max_buffer_capacity_bytes_) {
    *error_details = "Received data beyond available range.";
    return QUIC_INTERNAL_ERROR;
  }

  if (current_gap->begin_offset != starting_offset &&
      current_gap->end_offset != starting_offset + data.length() &&
      gaps_.size() >= kMaxNumGapsAllowed) {
    // This frame is going to create one more gap which exceeds max number of
    // gaps allowed. Stop processing.
    *error_details = "Too many gaps created for this stream.";
    return QUIC_TOO_MANY_FRAME_GAPS;
  }

  size_t total_written = 0;
  size_t source_remaining = size;
  const char* source = data.data();
  // Write data block by block. If corresponding block has not created yet,
  // create it first.
  // Stop when all data are written or reaches the logical end of the buffer.
  while (source_remaining > 0) {
    const size_t write_block_num = GetBlockIndex(offset);
    const size_t write_block_offset = GetInBlockOffset(offset);
    DCHECK_GT(blocks_count_, write_block_num);

    size_t block_capacity = GetBlockCapacity(write_block_num);
    size_t bytes_avail = block_capacity - write_block_offset;

    // If this write meets the upper boundary of the buffer,
    // reduce the available free bytes.
    if (offset + bytes_avail > total_bytes_read_ + max_buffer_capacity_bytes_) {
      bytes_avail = total_bytes_read_ + max_buffer_capacity_bytes_ - offset;
    }

    if (reduce_sequencer_buffer_memory_life_time_ && blocks_ == nullptr) {
      blocks_.reset(new BufferBlock*[blocks_count_]());
      for (size_t i = 0; i < blocks_count_; ++i) {
        blocks_[i] = nullptr;
      }
    }

    if (blocks_[write_block_num] == nullptr) {
      // TODO(danzh): Investigate if using a freelist would improve performance.
      // Same as RetireBlock().
      blocks_[write_block_num] = new BufferBlock();
    }

    const size_t bytes_to_copy = min<size_t>(bytes_avail, source_remaining);
    char* dest = blocks_[write_block_num]->buffer + write_block_offset;
    DVLOG(1) << "Write at offset: " << offset << " length: " << bytes_to_copy;
    memcpy(dest, source, bytes_to_copy);
    source += bytes_to_copy;
    source_remaining -= bytes_to_copy;
    offset += bytes_to_copy;
    total_written += bytes_to_copy;
  }

  DCHECK_GT(total_written, 0u);
  *bytes_buffered = total_written;
  UpdateGapList(current_gap, starting_offset, total_written);

  frame_arrival_time_map_.insert(
      std::make_pair(starting_offset, FrameInfo(size, timestamp)));
  num_bytes_buffered_ += total_written;
  return QUIC_NO_ERROR;
}

(3)为了精准测量RTT,quic协议的数据包编号都是单调递增的,哪怕是重发的包的编号都是增加的,这部分的控制代码在WritePacket函数里面:函数开头就判断数据包编号。一旦发现编号比最后一次发送包的编号还小,说明出错了,这时就关闭连接退出函数!

bool QuicConnection::WritePacket(SerializedPacket* packet) {
  /*如果数据包号比最后一个发送包的号还小,说明顺序错了,直接关闭连接*/
  if (packet->packet_number <
      sent_packet_manager_->GetLargestSentPacket(packet->path_id)) {
    QUIC_BUG << "Attempt to write packet:" << packet->packet_number << " after:"
             << sent_packet_manager_->GetLargestSentPacket(packet->path_id);
    CloseConnection(QUIC_INTERNAL_ERROR, "Packet written out of order.",
                    ConnectionCloseBehavior::SEND_CONNECTION_CLOSE_PACKET);
    return true;
  }
  /*没有连接、没有加密的包是不能发的*/
  if (ShouldDiscardPacket(*packet)) {
    ++stats_.packets_discarded;
    return true;
  }
  .........................  
}

(4)为啥quic协议要基于udp了?应用层现成的协议很复杂,改造的难度大!传输层只有tcp和udp两种协议;tcp的缺点不再赘述,udp的优点就是简单,只提供最原始的发包功能,完全不管对方有没有收到,quic就是利用了udp这种最基础的send package发包能力,在此之上完成了tls(保证数据安全)、拥塞控制(保证链路被塞满)、多路复用(保证数据不丢失)等应用层的功能!

文章来源:https://blog.csdn.net/qq_40989769/article/details/135173788
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。