流量控制的目的是保护接收端。确保发送方的发送速度不会超过接收方的处理速度,从而防止接收方的内存缓冲区被占满溢出。

QUIC 的流量控制机制是基于信用而非滑动窗口,并且QUIC拥有两个层次的控制:连接层和流层

TCP的的队头阻塞

对于TCP来说,假设发送了5,6,7,8序号的包,如果5号包的ACK丢了,那么即使6、7、8都已经收到ACK了,那也没办法往右滑动,必须得等5的ACK回来了,才能往右滑动,这就是经典的队头阻塞问题。

QUIC杜绝了这个问题。

QUIC的流量控制

QUIC采用了一种更直观的“信用”模型:

  1. 授予信用: 接收方通过发送控制帧,明确告知发送方“你最多可以向我发送多少字节的数据”。这个字节数就是“信用额度”或“流量控制窗口”。
  2. 消耗信用: 发送方每发送一个字节的数据,就消耗一点信用额度。
  3. 等待信用: 当发送方耗尽了接收方授予的信用额度后,它必须停止发送该范围内的数据,直到接收方处理完已收到的数据,并发送新的控制帧来授予更多信用(即更新窗口)。

双层控制

QUIC不仅在整个连接上进行流量控制,还在每个独立的流上进行控制。

连接层流量控制

  • 目的: 保护接收方为整个 QUIC 连接分配的总内存缓冲区。这是对所有流的一个全局上限。
  • 工作方式:
    • 接收方设定一个连接级别的最大可接收字节数。
    • 发送方必须确保,在所有流上发送的数据总字节数,不能超过这个上限。
    • 当接收方处理并向上层应用交付数据后,会释放这部分缓冲区空间,并通过发送 MAX_DATA 帧来提高这个上限,从而授予发送方更多“信用”。

流层流量控制

  • 目的:
    1. 保护接收方为单个流分配的内存缓冲区。
    2. 防止队头阻塞:避免一个大流量、高延迟或者被应用层暂停读取的流,耗尽整个连接的流量控制窗口,从而阻塞其他正常流的数据传输。
  • 工作方式:
    • 接收方为每一个独立的流都设定一个最大可接收字节数。
    • 发送方在某个特定的流上发送数据时,消耗的是该流的信用额度
    • 当接收方处理完某个流的数据后,会通过发送 MAX_STREAM_DATA 帧,只为那个特定的流增加信用额度。

控制帧

QUIC 使用四种特定的控制帧来管理流量控制:

接收方 -> 发送方 (授予信用)

  • **MAX_DATA 帧 **
    • 作用: 增大连接层的流量控制窗口。
    • 内容: 包含一个字段 Maximum Data,表示在该连接上允许发送的总字节数的绝对值上限。
  • MAX_STREAM_DATA
    • 作用:增大指定流的流量控制窗口。
    • 内容: 包含两个字段:Stream ID(指明要为哪个流更新窗口)和 Maximum Stream Data(表示该流上允许发送的总字节数的绝对值上限)。

发送方 -> 接收方 (通知阻塞)

  • DATA_BLOCKED
    • 作用: 当发送方想发送数据,但因为连接层的流量控制窗口耗尽而被阻塞时,它会发送此帧。
    • 目的: 告知接收方:“我已经因为总信用额度不足而被阻塞了。” 这可以提示接收方尽快发送 MAX_DATA 帧(如果它有能力处理更多数据),同时也有助于网络问题的诊断。
  • STREAM_DATA_BLOCKED
    • 作用: 当发送方在某个特定流上想发送数据,但因为该流的流量控制窗口耗尽而被阻塞时,发送此帧。
    • 目的: 告知接收方:“我在 Stream X 上因为该流的信用额度不足而被阻塞了。”

更新策略

  1. 握手时传输参数设置

    QUIC在建联握手的过程中,双方会传递传输参数,传输参数中的以下几个值与流控有关:

    connection级别:

    • initial_max_data:这个connection的最大可接收字节数

    流级别:

    id name 含义
    0x05 initia_max_stream_data_bidi_local 本端建立双向流时,本端的最大接收窗口
    0x06 initial_max_stream_data_bidi_remote 对端建立双向流时,本端的最大接收窗口
    0x07 initial_max_stream_data_uni 对端建立单向流时,本端的最大接收窗口
  2. 当接收方处理的数据量,使得剩余的窗口大小低于某个阈值时(小于总窗口大小的一半),它就会主动发送一个 MAX_DATAMAX_STREAM_DATA 帧。

  3. 新的窗口上限通常会被设置为当前已接收数据量的两倍。例如,如果初始窗口是 64 KB,当接收方处理了 32 KB 数据后,它会发送一个 MAX_DATA 帧,将窗口上限更新到 32KB + 64KB = 96KB 或者直接是 2 * 64KB = 128KB(但会受到其物理缓冲区大小的严格约束)。