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 1 RTT
如果连接的是一个新的服务器,这时候client是没有server的任何信息的,当然也不知道用那种密钥交换算法,因此,对于新的QUIC连接至少需要1 RTT才能完成握手
QUIC 0 RTT
客户端在缓存了ServerConfig的情况下,客户端根据缓存的ServerConifg获取到密钥交换算法及公钥,同时生成一个全新的密钥,直接向服务器发送full Client hello消息,开始正式握手,消息中包括客户端选择的公开数。服务器收到full Client hello,不同意回复REJ;同意连接,则根据客户端的公开数计算出初始密钥,回复SHLO消息。
具体的握手过程如图所示:
- 改进的拥塞控制:QUIC协议在TCP拥塞算法基础上做了些改进
- 可插拔:单个应用程序的不同连接也能支持配置不同的拥塞控制,不需要停机和升级就能实现拥塞控制的变更。
- 单调递增的Packet Number:QUIC并没有使用TCP的基于字节序号及ACK来确认消息的有序到达,QUIC使用的是Packet Number,每个Packet Number严格递增,所以如果Packet N丢失了,重传Packet N的Packet Number已不是N,而是一个大于N的值。 这样就很容易解决TCP的重传歧义问题。
- 更多的ACK块:QUIC ACK帧支持256个ACK块,相比TCP的SACK在TCP选项中实现,有长度限制,最多只支持3个ACK块
- 精确计算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
依赖安装
- argparse安装
apt-get install python-argparse
- 安装binutils
cd proto-quic
export PROTO_QUIC_ROOT=`pwd`/src
export PATH=$PATH:`pwd`/depot_tools
./proto_quic_tools/sync.sh
- 安装其它依赖库
./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加密握手提供以下属性:
- 认证密钥交换,其中
- 服务端总是经过身份验证
- 客户端可以选择性进行身份验证
- 每个连接都会产生不同并且不相关的密钥
- 密钥材料(keying material)可用于 0-RTT 和 1-RTT 数据包的保护
- 两个端点(both endpoints)传输参数的认证值,以及服务端传输参数的保密保护
- 应用协议的认证协商(TLS 使用 ALPN)
1-rtt的握手流程如下所示:
0-rtt的握手流程如下所示:
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()将数据进行处理