SA20225528 许天逸 - QUIC和HTTP3.0技术调研

QUIC协议和HTTP3.0技术研究

1. QUIC协议概述

1.1 HTTP2 vs HTTP3

 

 

HTTP3 本质不是对 HTTP 协议本身的改进,它主要是集中在如何提高传输效率。上图是相比 HTTP2 而言 HTTP3 提升的点:

HTTP3 使用 stream 进一步扩展了 HTTP2 的多路复用。在 HTTP3 模式下,一般传输多少个文件就会产生对应数量的 stream。当这些文件中的其中一个发生丢包时,你只需要重传丢包文件的对应 stream 即可。

HTTP3 不再是基于 TCP 建立的,而是通过 UDP 建立,在用户空间保证传输的可靠性,相比 TCPUDP 之上的 QUIC 协议提高了连接建立的速度,降低了延迟。

通过引入 Connection ID,使得 HTTP3 支持连接迁移以及 NAT 的重绑定。

HTTP3 含有一个包括验证、加密、数据及负载的 built-in TLS安全机制。

拥塞控制。TCP 是在内核区实现的,而 HTTP3 将拥塞控制移出了内核,通过用户空间来实现。这样做的好处就是不再需要等待内核更新可以实现很方便的进行快速迭代。

头部压缩。HTTP2 使用的 HPACKHTTP3 更换成了兼容 HPACK QPACK 压缩方案。QPACK 优化了对乱序发送的支持,也优化了压缩率。

1.2 QUIC介绍

Quic 全称 quick udp internet connection 快速 UDP 互联网连接,(和英文 quick 谐音,简称)是由 Google 提出的使用 udp 进行多路并发传输的协议。

Quic 相比现在广泛应用的 http2+tcp+tls 协议有如下优势 [2]

减少了 TCP 三次握手及 TLS 握手时间;

改进的拥塞控制;

避免队头阻塞的多路复用;

连接迁移;

前向冗余纠错。

下面是协议层视图:

 

 

下面是通信延迟对比图:

2. 为什么需要QUIC?

2.1概述

从上个世纪 90 年代互联网开始兴起一直到现在,大部分的互联网流量传输只使用了几个网络协议。使用 IPv4 进行路由,使用 TCP 进行连接层面的流量控制,使用 SSL/TLS 协议实现传输安全,使用 DNS 进行域名解析,使用 HTTP 进行应用数据的传输。

而且近三十年来,这几个协议的发展都非常缓慢。TCP 主要是拥塞控制算法的改进,SSL/TLS 基本上停留在原地,几个小版本的改动主要是密码套件的升级,TLS1.3[3] 是一个飞跃式的变化,但截止到今天,还没有正式发布。IPv4 虽然有一个大的进步,实现了 IPv6DNS 也增加了一个安全的 DNSSEC,但和 IPv6 一样,部署进度较慢。

随着移动互联网快速发展以及物联网的逐步兴起,网络交互的场景越来越丰富,网络传输的内容也越来越庞大,用户对网络传输效率和 WEB 响应速度的要求也越来越高。

一方面是历史悠久使用广泛的古老协议,另外一方面用户的使用场景对传输性能的要求又越来越高。

 

如下几个由来已久的问题和矛盾就变得越来越突出:

协议历史悠久导致中间设备僵化;

依赖于操作系统的实现导致协议本身僵化;

建立连接的握手延迟大;

队头阻塞。

 

2.2中间设备的僵化

可能是 TCP 协议使用得太久,也非常可靠。所以我们很多中间设备,包括防火墙、NAT 网关,整流器等出现了一些约定俗成的动作。

比如有些防火墙只允许通过 80 443,不放通其他端口。NAT 网关在转换网络地址时重写传输层的头部,有可能导致双方无法使用新的传输格式。整流器和中间代理有时候出于安全的需要,会删除一些它们不认识的选项字段。

TCP 协议本来是支持端口、选项及特性的增加和修改。但是由于 TCP 协议和知名端口及选项使用的历史太悠久,中间设备已经依赖于这些潜规则,所以对这些内容的修改很容易遭到中间环节的干扰而失败。

而这些干扰,也导致很多在 TCP 协议上的优化变得小心谨慎,步履维艰。

 

2.3依赖操作系统的实现导致僵化

TCP 是由操作系统在内核西方栈层面实现的,应用程序只能使用,不能直接修改。虽然应用程序的更新迭代非常快速和简单。但是 TCP 的迭代却非常缓慢,原因就是操作系统升级很麻烦。

现在移动终端更加流行,但是移动端部分用户的操作系统升级依然可能滞后数年时间。PC 端的系统升级滞后得更加严重,windows xp 现在还有大量用户在使用,尽管它已经存在快 20 年。

服务端系统不依赖用户升级,但是由于操作系统升级涉及到底层软件和运行库的更新,所以也比较保守和缓慢。

这也就意味着即使 TCP 有比较好的特性更新,也很难快速推广。比如 TCP Fast Open。它虽然 2013 年就被提出了,但是 Windows 很多系统版本依然不支持它。

 

2.4建立连接的握手延迟大

不管是 HTTP1.0/1.1 还是 HTTPSHTTP2,都使用了 TCP 进行传输。HTTPS HTTP2 还需要使用 TLS 协议来进行安全传输。

这就出现了两个握手延迟:

1TCP 三次握手导致的 TCP 连接建立的延迟;

2TLS 完全握手需要至少 2 RTT 才能建立,简化握手需要 1 RTT 的握手延迟。

对于很多短连接场景,这样的握手延迟影响很大,且无法消除。

 

2.5队头阻塞

队头阻塞主要是 TCP 协议的可靠性机制引入的。TCP 使用序列号来标识数据的顺序,数据必须按照顺序处理,如果前面的数据丢失,后面的数据就算到达了也不会通知应用层来处理。

另外 TLS 协议层面也有一个队头阻塞,因为 TLS 协议都是按照 record 来处理数据的,如果一个 record 中丢失了数据,也会导致整个 record 无法正确处理。

概括来讲,TCP TLS1.2 之前的协议存在着结构性的问题,如果继续在现有的 TCPTLS 协议之上实现一个全新的应用层协议,依赖于操作系统、中间设备还有用户的支持。部署成本非常高,阻力非常大。

所以 QUIC 协议选择了 UDP,因为 UDP 本身没有连接的概念,不需要三次握手,优化了连接建立的握手延迟,同时在应用程序层面实现了 TCP 的可靠性,TLS 的安全性和 HTTP2 的并发性,只需要用户端和服务端的应用程序支持 QUIC 协议,完全避开了操作系统和中间设备的限制。

 

3. QUIC核心特性

3.1 建立连接延时低

 

0RTT 建连可以说是 QUIC 相比 HTTP2 最大的性能优势。那什么是 0RTT 建连呢?

这里面有两层含义:

传输层 0RTT 就能建立连接;

加密层 0RTT 就能建立加密连接。

比如上图左边是 HTTPS 的一次完全握手的建连过程,需要 3 RTT。就算是 Session Resumption,也需要至少 2 RTT

QUIC 呢?由于建立在 UDP 的基础上,同时又实现了 0RTT 的安全握手,所以在大部分情况下,只需要 0 RTT 就能实现数据发送,在实现前向加密的基础上,并且 0RTT 的成功率相比 TLS Sesison Ticket 要高很多。

 

3.2改进的拥塞控制

TCP 的拥塞控制实际上包含了四个算法:慢启动,拥塞避免,快速重传,快速恢复 [22]

QUIC 协议当前默认使用了 TCP 协议的 Cubic 拥塞控制算法,同时也支持 CubicBytes, Reno, RenoBytes, BBR, PCC 等拥塞控制算法。

 从拥塞算法本身来看,QUIC 只是按照 TCP 协议重新实现了一遍,那么 QUIC 协议到底改进在哪些方面呢?主要有如下几点。

【可插拔】:

什么叫可插拔呢?就是能够非常灵活地生效,变更和停止。体现在如下方面:

•1)应用程序层面就能实现不同的拥塞控制算法,不需要操作系统,不需要内核支持。这是一个飞跃,因为传统的 TCP 拥塞控制,必须要端到端的网络协议栈支持,才能实现控制效果。而内核和操作系统的部署成本非常高,升级周期很长,这在产品快速迭代,网络爆炸式增长的今天,显然有点满足不了需求;

•2)即使是单个应用程序的不同连接也能支持配置不同的拥塞控制。就算是一台服务器,接入的用户网络环境也千差万别,结合大数据及人工智能处理,我们能为各个用户提供不同的但又更加精准更加有效的拥塞控制。比如 BBR 适合,Cubic 适合;

•3)应用程序不需要停机和升级就能实现拥塞控制的变更,我们在服务端只需要修改一下配置,reload 一下,完全不需要停止服务就能实现拥塞控制的切换。

STGW 在配置层面进行了优化,我们可以针对不同业务,不同网络制式,甚至不同的 RTT,使用不同的拥塞控制算法。

 

【单调递增的 Packet Number】:

TCP 为了保证可靠性,使用了基于字节序号的 Sequence Number Ack 来确认消息的有序到达。

QUIC 同样是一个可靠的协议,它使用 Packet Number 代替了 TCP sequence number,并且每个 Packet Number 都严格递增,也就是说就算 Packet N 丢失了,重传的 Packet N Packet Number 已经不是 N,而是一个比 N 大的值。

但是单纯依靠严格递增的 Packet Number 肯定是无法保证数据的顺序性和可靠性。QUIC 又引入了一个 Stream Offset 的概念。

即一个 Stream 可以经过多个 Packet 传输,Packet Number 严格递增,没有依赖。但是 Packet 里的 Payload 如果是 Stream 的话,就需要依靠 Stream Offset 来保证应用数据的顺序。如错误! 未找到引用源。所示,发送端先后发送了 Pakcet N Pakcet N+1Stream Offset 分别是 x x+y

假设 Packet N 丢失了,发起重传,重传的 Packet Number N+2,但是它的 Stream Offset 依然是 x,这样就算 Packet N + 2 是后到的,依然可以将 Stream x Stream x+y 按照顺序组织起来,交给应用程序处理。

 

 

针对stream

 

针对connection

 

 

3.3没有队头阻塞的多路复用

QUIC 的多路复用和 HTTP2 类似。在一条 QUIC 连接上可以并发发送多个 HTTP 请求 (stream)。但是 QUIC 的多路复用相比 HTTP2 有一个很大的优势。

QUIC 一个连接上的多个 stream 之间没有依赖。这样假如 stream2 丢了一个 udp packet,也只会影响 stream2 的处理。不会影响 stream2 之前及之后的 stream 的处理。

这也就在很大程度上缓解甚至消除了队头阻塞的影响。

多路复用是 HTTP2 最强大的特性,能够将多条请求在一条 TCP 连接上同时发出去。但也恶化了 TCP 的一个问题,队头阻塞,如下图示:

 

 

HTTP2 在一个 TCP 连接上同时发送 4 Stream。其中 Stream1 已经正确到达,并被应用层读取。但是 Stream2 的第三个 tcp  segment 丢失了,TCP 为了保证数据的可靠性,需要发送端重传第 3 segment 才能通知应用层读取接下去的数据,虽然这个时候 Stream3 Stream4 的全部数据已经到达了接收端,但都被阻塞住了。

 

不仅如此,由于 HTTP2 强制使用 TLS,还存在一个 TLS 协议层面的队头阻塞

Record TLS 协议处理的最小单位,最大不能超过 16K,一些服务器比如 Nginx 默认的大小就是 16K。由于一个 record 必须经过数据一致性校验才能进行加解密,所以一个 16K record,就算丢了一个字节,也会导致已经接收到的 15.99K 数据无法处理,因为它不完整。

QUIC 多路复用为什么能避免上述问题呢?

•1QUIC 最基本的传输单元是 Packet,不会超过 MTU 的大小,整个加密和认证过程都是基于 Packet 的,不会跨越多个 Packet。这样就能避免 TLS 协议存在的队头阻塞;

•2Stream 之间相互独立,比如 Stream2 丢了一个 Pakcet,不会影响 Stream3 Stream4。不存在 TCP 队头阻塞。

 

3.4连接迁移

一条 TCP 连接 [17] 是由四元组标识的(源 IP,源端口,目的 IP,目的端口)。什么叫连接迁移呢?就是当其中任何一个元素发生变化时,这条连接依然维持着,能够保持业务逻辑不中断。当然这里面主要关注的是客户端的变化,因为客户端不可控并且网络环境经常发生变化,而服务端的 IP 和端口一般都是固定的。

比如大家使用手机在 WIFI 4G 移动网络切换时,客户端的 IP 肯定会发生变化,需要重新建立和服务端的 TCP 连接。

又比如大家使用公共 NAT 出口时,有些连接竞争时需要重新绑定端口,导致客户端的端口发生变化,同样需要重新建立 TCP 连接。

针对 TCP 的连接变化,MPTCP[5] 其实已经有了解决方案,但是由于 MPTCP 需要操作系统及网络协议栈支持,部署阻力非常大,目前并不适用。

所以从 TCP 连接的角度来讲,这个问题是无解的。

QUIC 是如何做到连接迁移呢?很简单,任何一条 QUIC 连接不再以 IP 及端口四元组标识,而是以一个 64 位的随机数作为 ID 来标识,这样就算 IP 或者端口发生变化时,只要 ID 不变,这条连接依然维持着,上层业务逻辑感知不到变化,不会中断,也就不需要重连。

由于这个 ID 是客户端随机产生的,并且长度有 64 位,所以冲突概率非常低。

 

 

3.5其他亮点

此外,QUIC 还能实现前向冗余纠错,在重要的包比如握手消息发生丢失时,能够根据冗余信息还原出握手消息。

QUIC 还能实现证书压缩,减少证书传输量,针对包头进行验证等。

 

4. QUIC部署,编译和运行

4.1准备配置文件

  • mkdir -p /containers/caddy
  • cd /containers/caddy
  • vi docker-compose.yml
version: '2'

services:

caddy-server:

  image: abiosoft/caddy:latest

  container_name: caddy-server

  ports:

    - "443:443/udp"

    - "443:443"

    - "80:80"

  volumes:

    - "/containers/caddy/Caddyfile:/etc/Caddyfile"

    - "/containers/caddy/.caddy:/root/.caddy"

    - "/containers/caddy/log:/root/log"

    - "/containers/caddy/www:/www"

  restart: always

  entrypoint :  "tail -f /dev/null"
  • 保存退出
  • vi ./www/index.html

<h1>Hello World</h1>

  • 保存退出
  • vi Caddyfile
https://www.1996.live {

gzip

tls imqksl@gmail.com

root /www

log /root/log/log.log

errors /root/log/error.log

}
  • 保存退出
  • docker-compose up -d
  • docker ps

如果没有意外,你这个时候应该是跟我的截图类似,注意 STATUS Up * Seconds应该就是正常启动了。

 

docker exec -i -t caddy-server sh

执行这条命令后,我们会进入该容器内容,可以通过命令行左侧路径分辨出来。

 

caddy -quic -conf /etc/Caddyfile

执行这条命令后,Caddy会询问是否同意相关协议。

 

 

然后Caddy会自动去申请HTTPS证书,免费的,一般两分钟以内完成。 这里可能会让你再输入一次邮箱地址。完成后,大概就是这个样子。

 

通过域名访问即可,注意使用https://

确保服务器防火墙允许80,443端口流量通过

 

4.2善后处理

  • ctrl+c
  • exit
  • docker-compose down
  • vi docker-compose.yml
version: '2'

services:

caddy-server:

  image: abiosoft/caddy:latest

  container_name: caddy-server

  ports:

    - "443:443/udp"

    - "443:443"

    - "80:80"

  volumes:

    - "/containers/caddy/Caddyfile:/etc/Caddyfile"

    - "/containers/caddy/.caddy:/root/.caddy"

    - "/containers/caddy/log:/root/log"

    - "/containers/caddy/www:/www"

  restart: always

  entrypoint :  "caddy -quic -conf /etc/Caddyfile"  # 整个文件只修改这里

 

这个文件只修改了最后一行的内容

docker-compose up -d

  • systemctl enable docker

自此,如果没有意外的话,以后该服务器都会自动运行。

静态文件根据自己的需求替换即可

Caddy也有反向代理等功能,可以轻松实现整站QUIC。

如果已经完全按照本文操作,但是依然不能建立QUIC连接,一个可能的原因是ISP阻断了UDP数据,可以让其它地区的朋友帮忙测试一下。

 

 

4.3强制HTTPS

完成前面的操作后,虽然我们可以通过输入https://yourdomain实现HTTPS连接,但是如果输入http://yourdomain浏览器与服务器的连接还是HTTP连接。这里我们通过配置Caddyfile的内容就可以实现强制HTTPS,原理很简单,将HTTP重定向至HTTPS即可。

  • vi Caddyfile
http://www.1996.live  http://1996.live  https://1996.live{

    redir https://www.1996.live{url}

}

https://www.1996.live {

gzip

tls imqksl@gmail.com

root /www

log /root/log/log.log

errors /root/log/error.log

}
  • docker-compose down
  • docker-compose up -d

4.4整站HTTPS

我们的网站可能不只一个Web服务,比如我除了www.1996.live的主页以外,在另一台服务器上还跑了一个harbor镜像仓库,域名已经指向当前服务器的IP地址,镜像仓库只能通过IP+端口访问。

 

 

  • 通过反向代理功能,可以轻松的将域名指向服务器上的另一个或者另一个服务器上的应用,Apache,Caddy,Nginx,同时因为是通过Caddy访问的,连接依然是HTTPS和QUIC的,(Caddy与应用之间应该是普通的HTTP)。
  • 比如这里我打算给我的镜像仓库分配域名hub.1996.live,修改Caddyfile即可。
  • vi Caddyfile
http://www.1996.live  http://1996.live  https://1996.live{

    redir https://www.1996.live{url}

}

https://www.1996.live {

gzip

tls imqksl@gmail.com

root /www

log /root/log/log.log

errors /root/log/error.log

}

 

# 添加下面的这部分内容即可

http://hub.1996.live{

    redir https://hub.1996.live{url}

}

https://hub.1996.live {

gzip

tls imqksl@gmail.com

# 填写另一个应用所在服务器的IP地址和端口即可

# 一般填局域网IP地址,速度更快,更安全。

proxy / http://157.230.159.156 {

  header_upstream Host {host}

  header_upstream X-Real-IP {remote}

  header_upstream X-Forwarded-For {remote}

  header_upstream X-Forwarded-Proto {scheme}

}

log /root/log/log.log

errors /root/log/error.log

}
  • docker-compose down
  • docker-compose up -d

 

 

 

5. QUIC源代码分析-帧重传

最新的quic代码里中的重传逻辑,实现了两种处理模式,一个是在connection层实现的重传,另一个是在session层实现的重传。在应用中只能启用一个,要有由connection负责重传,要么由session负责重传。session层的重传的启动开关是session_decides_what_to_write_

早点的代码,其实也就是几个月前的代码,QuicFrame的定义是:

struct QUIC_EXPORT_PRIVATE QuicFrame {

explicit QuicFrame(QuicStreamFrame* stream_frame);

 QuicFrameType type;

  union {

    // Frames smaller than a pointer are inline.

    QuicPaddingFrame padding_frame;

    QuicMtuDiscoveryFrame mtu_discovery_frame;

    QuicPingFrame ping_frame;

 

    // Frames larger than a pointer.

    QuicStreamFrame* stream_frame;

    QuicAckFrame* ack_frame;

    QuicStopWaitingFrame* stop_waiting_frame;

    QuicRstStreamFrame* rst_stream_frame;

    QuicConnectionCloseFrame* connection_close_frame;

    QuicGoAwayFrame* goaway_frame;

    QuicWindowUpdateFrame* window_update_frame;

    QuicBlockedFrame* blocked_frame;

  };

}

stream_frame是一个指针形式,但是在新的代码里,就是个结构体。这样的改变,就是为了

实现这个session层面的重传逻辑。

struct QUIC_EXPORT_PRIVATE QuicFrame {

  explicit QuicFrame(QuicStreamFrame stream_frame);

      struct {

      QuicFrameType type;

 

      // TODO(wub): These frames can also be inlined without increasing the size

      // of QuicFrame: QuicStopWaitingFrame, QuicRstStreamFrame,

      // QuicWindowUpdateFrame, QuicBlockedFrame, QuicPathResponseFrame,

      // QuicPathChallengeFrame and QuicStopSendingFrame.

      union {

        QuicAckFrame* ack_frame;

        QuicStopWaitingFrame* stop_waiting_frame;

        QuicRstStreamFrame* rst_stream_frame;

        QuicConnectionCloseFrame* connection_close_frame;

        QuicGoAwayFrame* goaway_frame;

        QuicWindowUpdateFrame* window_update_frame;

        QuicBlockedFrame* blocked_frame;

        QuicApplicationCloseFrame* application_close_frame;

        QuicNewConnectionIdFrame* new_connection_id_frame;

        QuicRetireConnectionIdFrame* retire_connection_id_frame;

        QuicPathResponseFrame* path_response_frame;

        QuicPathChallengeFrame* path_challenge_frame;

        QuicStopSendingFrame* stop_sending_frame;

        QuicMessageFrame* message_frame;

        QuicCryptoFrame* crypto_frame;

        QuicNewTokenFrame* new_token_frame;

      };

    };

} 

数据的重传,首先要判断数据的丢包。使用来阅读最新的quic代码,还是很方便的,比许多ide好用多了,需要梯子。

bool QuicSentPacketManager::OnAckFrameEnd(QuicTime ack_receive_time){

  PostProcessAfterMarkingPacketHandled(last_ack_frame_, ack_receive_time,

                                       rtt_updated_, prior_bytes_in_flight);

}

void QuicSentPacketManager::PostProcessAfterMarkingPacketHandled(

    const QuicAckFrame& ack_frame,

    QuicTime ack_receive_time,

    bool rtt_updated,

    QuicByteCount prior_bytes_in_flight){

     InvokeLossDetection(ack_receive_time);

    }

void QuicSentPacketManager::InvokeLossDetection(QuicTime time) {

MarkForRetransmission(packet.packet_number, LOSS_RETRANSMISSION);    

 }

 //在这里就进入分叉处理,

void QuicSentPacketManager::MarkForRetransmission(

    QuicPacketNumber packet_number,

    TransmissionType transmission_type) {

  //  记录要重传的数据包序号,后续connection层的重传会用到

  if (!session_decides_what_to_write()) {

    if (!unacked_packets_.HasRetransmittableFrames(*transmission_info)) {

      return;

    }

    if (!QuicContainsKey(pending_retransmissions_, packet_number)) {

      pending_retransmissions_[packet_number] = transmission_type;

    }

    return;

  }

//如果session_decides_what_to_write_开启,则由session负责重传。

  HandleRetransmission(transmission_type, transmission_info);    

}

 

 

5.1 Connection负责的重传

void QuicConnection::WritePendingRetransmissions()

{

  DCHECK(!session_decides_what_to_write());

  // Keep writing as long as there's a pending retransmission which can be

  // written.

  while (sent_packet_manager_.HasPendingRetransmissions() &&

         CanWrite(HAS_RETRANSMITTABLE_DATA)) {

    const QuicPendingRetransmission pending =

        sent_packet_manager_.NextPendingRetransmission();

 

    // Re-packetize the frames with a new packet number for retransmission.

    // Retransmitted packets use the same packet number length as the

    // original.

    // Flush the packet generator before making a new packet.

    // TODO(ianswett): Implement ReserializeAllFrames as a separate path that

    // does not require the creator to be flushed.

    // TODO(fayang): FlushAllQueuedFrames should only be called once, and should

    // be moved outside of the loop. Also, CanWrite is not checked after the

    // generator is flushed.

    {

      ScopedPacketFlusher flusher(this, NO_ACK);

      packet_generator_.FlushAllQueuedFrames();

    }

    DCHECK(!packet_generator_.HasQueuedFrames());

    char buffer[kMaxPacketSize];

    packet_generator_.ReserializeAllFrames(pending, buffer, kMaxPacketSize);

  }

}

void QuicPacketCreator::ReserializeAllFrames(

    const QuicPendingRetransmission& retransmission,

    char* buffer,

    size_t buffer_len) {

   SerializePacket(buffer, buffer_len);

  packet_.original_packet_number = retransmission.packet_number;   

   }

 

 

这里最终还是去stream中的send_buffer_获取数据。packet_.original_packet_number记录数据包上次发送使用的序列号,下次发送的时候回调用QuicSentPacketManager::OnPacketSent将原来记录的丢失帧信息更新。

bool QuicSentPacketManager::OnPacketSent(

    SerializedPacket* serialized_packet,

    QuicPacketNumber original_packet_number,

    QuicTime sent_time,

    TransmissionType transmission_type,

    HasRetransmittableData has_retransmittable_data) {

    unacked_packets_.AddSentPacket(serialized_packet, original_packet_number,

                                 transmission_type, sent_time, in_flight);  

    }

    void QuicUnackedPacketMap::AddSentPacket(SerializedPacket* packet,

                                         QuicPacketNumber old_packet_number,

                                         TransmissionType transmission_type,

                                         QuicTime sent_time,

                                         bool set_in_flight){

  if (old_packet_number.IsInitialized()) {

    TransferRetransmissionInfo(old_packet_number, packet_number,

                               transmission_type, &info);

  }  

}

 

5.2 session负责的重传

session层实现的重传,就不需要sent_packet_manager_.NextPendingRetransmission()获取pending中含有的可重传帧的原理的传输序号了(retransmission.packet_number)。也可以说QuicUnackedPacketMap中的记录的unacked_packets_信息就不太重要了。

void QuicSentPacketManager::HandleRetransmission(

    TransmissionType transmission_type,

    QuicTransmissionInfo* transmission_info) {

   unacked_packets_.NotifyFramesLost(*transmission_info, transmission_type);

    }

    

void QuicUnackedPacketMap::NotifyFramesLost(const QuicTransmissionInfo& info,

                                            TransmissionType type) {

  DCHECK(session_decides_what_to_write_);

  for (const QuicFrame& frame : info.retransmittable_frames) {

    session_notifier_->OnFrameLost(frame);

  }

}

 

void QuicSession::OnFrameLost(const QuicFrame& frame){

  QuicStream* stream = GetStream(frame.stream_frame.stream_id);

  if (stream == nullptr) {

    return;

  }

  stream->OnStreamFrameLost(frame.stream_frame.offset,

                            frame.stream_frame.data_length,

                            frame.stream_frame.fin);

}

void QuicStream::OnStreamFrameLost(QuicStreamOffset offset,

                                   QuicByteCount data_length,

                                   bool fin_lost) {

  if (data_length > 0) {

    send_buffer_.OnStreamDataLost(offset, data_length);

  }

}

void QuicStreamSendBuffer::OnStreamDataLost(QuicStreamOffset offset,

                                            QuicByteCount data_length) {

  for (const auto& lost : bytes_lost) {

    pending_retransmissions_.Add(lost.min(), lost.max());

  }

}

再次将数据发送出去是什么时候呢?

void QuicStream::OnCanWrite() {

  if (HasPendingRetransmission()) {

    WritePendingRetransmission();

    // Exit early to allow other streams to write pending retransmissions if

    // any.

    return;

  }

}

void QuicStream::WritePendingRetransmission() {

      consumed = session()->WritevData(this, id_, pending.length, pending.offset,

                                can_bundle_fin ? FIN : NO_FIN);

}

但是感觉session层实现的重传有个问题,要是多个ack均表明,某个序号的数据包丢失,这样好像会多次重传这个数据。因为未被ack的包在unacked_packets_.GetLeastUnacked()中记录,直到收到更大的ack,将unacked_packets_中的信息删除。而connection层的数据重发的时候,会调用TransferRetransmissionInfo()函数删除已经重传的数据包信息,使得least_unacked_增加。

 

posted @ 2021-01-26 20:36  阿飞uuu  阅读(443)  评论(0)    收藏  举报