QUIC协议和HTTP3.0技术研究

1. QUIC协议

1. 什么是QUIC协议

QUIC是快速UDP网络连接(Quick UDP Internet Connections)的缩写,这是一种实验性的传输层网络传输协议,由Google公司开发,在2013年实现。QUIC使用UDP协议,它在两个端点间创建连接,且支持多路复用连接。在设计之初,QUIC希望能够提供等同于SSL/TLS层级的网络安全保护,减少数据传输及创建连接时的延迟时间,双向控制带宽,以避免网络拥塞。Google希望使用这个协议来取代TCP协议,使网页传输速度加快,计划将QUIC提交至互联网工程任务小组(IETF),让它成为下一代的正式网络规范。可以用一个公式大致概括:
TCP + TLS + HTTP2 = UDP + QUIC + HTTP2’s API
可以看出,QUIC协议因为是基于UDP,所以它不但具有TCP的可靠性、拥塞控制、流量控制等,但它在TCP协议的基础上做了一些改进,比如避免了队首阻塞;另外,QUIC协议具有TLS的安全传输特性,实现了TLS的保密功能,同时又使用更少的RTT建立安全的会话。

2. 使用QUIC协议的目的

为了整合TCP协议的可靠性和UDP协议的速度和效率

3. QUIC的特性

  • 低延迟连接的建立:建立一个TCP连接需要进行三次握手,这意味着每次连接都会产生额外的RTT,从而给每个连接增加了显著的延迟。另外,如果还需要TLS协商来创建一个安全的、加密的https连接,那么就需要更多的RTT,无疑会产生更大的延迟。而QUIC协议可以在1个RTT中启动一个连接并且获取完成握手所需的必要信息:
    QUIC协议和HTTP3.0技术研究

QUIC 1 RTT

如果连接的是一个新的服务器,这时候client是没有server的任何信息的,当然也不知道用那种密钥交换算法,因此,对于新的QUIC连接至少需要1 RTT才能完成握手

QUIC 0 RTT

客户端在缓存了ServerConfig的情况下,客户端根据缓存的ServerConifg获取到密钥交换算法及公钥,同时生成一个全新的密钥,直接向服务器发送full Client hello消息,开始正式握手,消息中包括客户端选择的公开数。服务器收到full Client hello,不同意回复REJ;同意连接,则根据客户端的公开数计算出初始密钥,回复SHLO消息。
具体的握手过程如图所示:
QUIC协议和HTTP3.0技术研究

  • 改进的拥塞控制:QUIC协议在TCP拥塞算法基础上做了些改进
    1. 可插拔:单个应用程序的不同连接也能支持配置不同的拥塞控制,不需要停机和升级就能实现拥塞控制的变更。
    2. 单调递增的Packet Number:QUIC并没有使用TCP的基于字节序号及ACK来确认消息的有序到达,QUIC使用的是Packet Number,每个Packet Number严格递增,所以如果Packet N丢失了,重传Packet N的Packet Number已不是N,而是一个大于N的值。 这样就很容易解决TCP的重传歧义问题。
    3. 更多的ACK块:QUIC ACK帧支持256个ACK块,相比TCP的SACK在TCP选项中实现,有长度限制,最多只支持3个ACK块
    4. 精确计算RTT时间:QUIC ACK包同时携带了从收到包到回复ACK的延时,这样结合递增的包序号,能够精确的计算RTT
  • 无队头阻塞的多路复用:QUIC的丢包、流控都是基于stream的,所有stream是相互独立的,一条stream上的丢包,不会影响其他stream的数据传输
  • 前向纠错:QUIC使用了FEC(前向纠错码)来恢复数据,FEC采用简单异或的方式,每发送一组数据,包括若干个数据包后,并对这些数据包依次做异或运算,最后的结果作为一个FEC包再发送出去。接收方收到一组数据后,根据数据包和FEC包即可以进行校验和纠错。
  • 连接迁移:TCP的连接是基于4元组的,而QUIC使用64为的Connection ID进行唯一识别客户端和服务器的逻辑连接,这就意味着如果一个客户端改变IP地址或端口号,TCP连接不再有效,而QUIC层的逻辑连接维持不变,仍然采用老的Connection ID。

2. 编译QUIC服务器和客户端

下载

git clone https://github.com/google/proto-quic.git

依赖安装

  1. argparse安装
apt-get install python-argparse
  1. 安装binutils
cd proto-quic  
export PROTO_QUIC_ROOT=`pwd`/src  
export PATH=$PATH:`pwd`/depot_tools  
./proto_quic_tools/sync.sh
  1. 安装其它依赖库
./src/build/install-build-deps.sh --no-syms --no-arm --no-chromeos-fonts

编译生成服务器和客户端程序

cd src
gn gen out/Default && ninja -C out/Default quic_client quic_server net_unittests

编译完成后输出的文件在proto-quic/src/out/Default下

3. 运行

运行服务器

准备测试数据

cd /tmp
wget -p --save-headers https://www.example.org

生成证书

cd proto-quic/src/net/tools/quic/certs/
./generate-certs.sh

运行服务器

./out/Default/quic_server   --quic_response_cache_dir=/tmp/www.example.org   --certificate_file=net/tools/quic/certs/out/leaf_cert.pem   --key_file=net/tools/quic/certs/out/leaf_cert.pkcs8   --v=1

服务器启动的成功后会查看6121端口

运行客户端

使用chromium导入CA证书。这里需要注意,Ubuntu只允许普通用户登陆,所以chromium导入证书时是以普通用户导入的。在运行客户端时也需要以普通用户来运行,否则会报证书无效的错误

sudo ./out/Default/quic_client --host=127.0.0.1 --port=6121 https://www.example.org/

客户端运行成功后将会看到前面准备的数据内容

4. QUIC原理

quic的数据包是通过UDP数据报进行传输的,一个数据报中可以包含一个或多个quic数据包。quic数据包编号被分为三个空间:

  • Initial:所有初始包
  • Handshake:所有握手包
  • Application data:所有 0-RTT 和 1-RTT 加密的数据包

首部
quic首部分为两种:Long header 和 Short Header,通过第一个有效字节的最高位来区分。
Long header的定义如下:

Long Header Packet {
     Header Form (1) = 1,
     Fixed Bit (1) = 1,
     Long Packet Type (2),
     Type-Specific Bits (4),
     Version (32),
     Destination Connection ID Length (8),
     Destination Connection ID (0..160),
     Source Connection ID Length (8),
     Source Connection ID (0..160),
}

Long Header Packets的类型包括四种:Initial,0-RTT,Handshake,Retry。
Short Header的定义如下:

Short Header Packet {
     Header Form (1) = 0,
     Fixed Bit (1) = 1,
     Spin Bit (1),
     Reserved Bits (2),
     Key Phase (1),
     Packet Number Length (2),
     Destination Connection ID (0..160),
     Packet Number (8..32),
     Packet Payload (..),
}

在版本协商以及1-RTT密钥传输完成后,quic就会使用Short Header Packet来传输数据。

握手
quic加密握手提供以下属性:

  • 认证密钥交换,其中
    1. 服务端总是经过身份验证
    2. 客户端可以选择性进行身份验证
    3. 每个连接都会产生不同并且不相关的密钥
    4. 密钥材料(keying material)可用于 0-RTT 和 1-RTT 数据包的保护
  • 两个端点(both endpoints)传输参数的认证值,以及服务端传输参数的保密保护
  • 应用协议的认证协商(TLS 使用 ALPN)

1-rtt的握手流程如下所示:
QUIC协议和HTTP3.0技术研究
0-rtt的握手流程如下所示:
QUIC协议和HTTP3.0技术研究

5. 源代码分析

Quic服务端客户端发送数据流程

发送:
最外层的发送数据接口为调用stream流的WriteOrBufferData(body, fin, nullptr)方法,其中body是要发的数据,fin是标识是否是改流的最后一个数据。之后会在流中进行相应的判断和处理,如流上是否有足够的空间来发送这个数据,发送窗口大小是否合适,是否阻塞等。如果判断可以进行发送之后便会调用session类的方法WritevData()。
在session类会调用connection类的SendStreamData方法发送数据,并根据实际发送的数据更新相应stream流的数据消费的数值。
在connection类会调用PacketGenerator类的ConsumeData方法来发送数据。其中会根据包来进行ack的绑定,之后就是在底层进行一系列处理。
之后会返回connection类,根据消息队列情况调用WritePacket()进行socket上包的写入,该方法实现于PacketWriter类。
接收:
当Server端创建好之后循环调用StartReading(),进行接收包,根据synchronous_read_count_ 来判断是否是CHLO包

void QuicSimpleServer::StartReading() {
  if (synchronous_read_count_ == 0) {
    // Only process buffered packets once per message loop.
    dispatcher_->ProcessBufferedChlos(kNumSessionsToCreatePerSocketEvent);
  }
...
  int result = socket_->RecvFrom(
      read_buffer_.get(), read_buffer_->size(), &client_address_,
      base::Bind(&QuicSimpleServer::OnReadComplete, base::Unretained(this)));
...
OnReadComplete(result);
}

OnReadComplete()中会调用dispatcher的处理包方法

void QuicSimpleServer::OnReadComplete(int result) {
...
  dispatcher_->ProcessPacket(
      QuicSocketAddress(QuicSocketAddressImpl(server_address_)),
      QuicSocketAddress(QuicSocketAddressImpl(client_address_)), packet);

  StartReading();
}
void QuicDispatcher::ProcessPacket(const QuicSocketAddress& server_address,
                                   const QuicSocketAddress& client_address,
                                   const QuicReceivedPacket& packet) {
...
  framer_.ProcessPacket(packet);
...
}

跳转到Framer类的处理方法

bool QuicFramer::ProcessPacket(const QuicEncryptedPacket& packet) {
...
  if (!visitor_->OnUnauthenticatedPublicHeader(public_header)) {
    // The visitor suppresses further processing of the packet.
    return true;
  }
...
}

visitor_指向dispatch类,跳转到

QuicDispatcher::OnUnauthenticatedPublicHeader(){
...
  QuicConnectionId connection_id = header.connection_id;
  SessionMap::iterator it = session_map_.find(connection_id);
  if (it != session_map_.end()) {
    DCHECK(!buffered_packets_.HasBufferedPackets(connection_id));
    it->second->ProcessUdpPacket(current_server_address_,
                                 current_client_address_, *current_packet_);
    return false;
  }
...
}

当包头的connection_id 能在session_map里找到时,直接调用connection的ProcessUdpPacket处理,server端的session_map维护在dispatch类里,创建session类都会记录下来。
之后经过处理跳转到Framer类的ProcessFrameData()方法里,其中对stream Framer 和ACK Framer 分别进行了处理
如果是stream包,则对其进行解析后会调用OnStreamFrame()抛到上层

if (!ProcessStreamFrame(reader, frame_type, &frame)) {
          return RaiseError(QUIC_INVALID_STREAM_DATA);
        }
        if (!visitor_->OnStreamFrame(frame)) {
          QUIC_DVLOG(1) << ENDPOINT
                        << "Visitor asked to stop further processing.";
          // Returning true since there was no parsing error.
          return true;
        }
}

visitor_在Framer类里,由创建connection类时初始化,指向connection类,在到connection类里调用visitor_->OnStreamFrame(),visitor_指向session类,在由session类抛到stream类的OnDataAvailable()将数据进行处理

QUIC协议和HTTP3.0技术研究

上一篇:SQL查询当天、本周、本月记录详解


下一篇:图片预览引发的思考 zoomerang.js 解析