571
社区成员
发帖
与我相关
我的任务
分享一、如何使用QUIC协议
以火狐浏览器为例,快速体验QUIC协议。
使用火狐浏览器访问:about:config;
在选项卡中搜索network.http.http3.enabled,并置为true(默认为true);

进行一点简单的测试,测试地址:https://quic.nginx.org/quic.html;
运行结果如下。我们可以感受到QUIC是一种对标HTTP协议的传输协议。开启使用QUIC协议后,打开支持HTTP3的网站的速度会更快。

二、什么是QUIC协议
2.1 QUIC协议的定义
QUIC,全称quick UDP Internet connection,是谷歌开发的基于UDP协议的低延迟互联网传输协议。
众所周知,TCP/IP协议族是网络的基础。 传输层协议只有两种:TCP 和 UDP。 与 TCP 协议相比,UDP 更轻量级,错误校验很少。 因为UDP是不可靠的协议,不保证按顺序传递,所以它的可靠性不如TCP协议可靠。
QUIC 传输层基于 UDP 协议但却是一种可靠的传输协议,因为它将很多可靠性的验证策略从系统层转移到应用层来做,这样可以使用更适合现代流媒体传输的拥塞控制策略。
用一张图来说明QUIC协议在网络结构中的作用:

2.2 为什么需要QUIC协议
首先了解TCP协议的两个重要缺陷:
(1)建立连接时握手时延大。TCP是传输可靠的协议,在传输数据之前需要建立连接,完成“三次握手”。并且在采用TLS协议时,还需要交换密钥,又增加了往返时延,因此TCP在创建连接的时候握手时延较大。
(2)队头阻塞问题。指的是下一个请求必须在上一个请求完成并返回后才能发出,导致后续请求被阻塞,无法充分利用带宽。因为TCP协议是有序的,必须保证数据传递的顺序不变,当前面有任务被阻塞的时候,后面的任务都不能发出请求。
了解了TCP协议的问题后,再看QUIC协议在这些问题上做出的改进。
(1)0RTT连接。在最佳情况下,QUIC可以在短连接下以0RTT开始数据传输。而基于TCP的HTTPS,即使在最好的TLS1.3下,仍然需要1RTT启动数据传输。现在常见的TLS1.2,在完全握手的情况下,需要3RTT来启动数据传输。对于RTT敏感业务,QUIC可以有效减少连接建立延迟。
通过下图对比传统TCP协议握手过程和QUIC协议握手过程的差异。

TCP协议的三次握手

QUIC协议握手
(2)多路复用,避免队头阻塞。
QUIC也是一个可靠的协议。它使用包编号而不是TCP序列号,并且每个包编号严格递增。即,即使分组n丢失,重发分组n的分组号也不再是n,而是大于n的值,例如分组n+M。Quic采用了一种单调递增的包数设计,这可以使数据包不再像TCP那样需要按顺序确认,支持无序确认。当数据包n丢失时,只要有新接收的数据包确认,当前窗口将继续向右滑动。发送端在得知分组n丢失后,将要重发的分组放入要发送的队列中,重新编号,如分组n+m,然后重新发送给接收端。重发分组的处理类似于发送新分组,使得当前窗口不会因分组丢失重发而被阻塞到位,从而解决了队列头阻塞问题。
Quic使用流ID来识别当前数据流所属的资源请求,这也是在多路复用和发送到接收端后数据包正常组装的基础。仅通过流ID的比较,重发分组n+m和丢失分组n是一致的,仍然无法判断两个分组的内容是否一致。需要添加新的字段流偏移量,以标识当前流ID中当前数据包的字节偏移量。
用一张图来说明QUIC协议的无头阻塞实现方法。

三、QUIC协议的源码分析
3.1 在linux下编译QUIC
这里以msquic为例,msquic是Microsoft开发的quic
下载源码:git clone --recursive https://github.com/microsoft/msquic.git
安装powershell,过程如下(可参考https://docs.microsoft.com/en-us/powershell/scripting/install/installing-powershell-core-on-linux?view=powershell-7.1 ):
sudo apt-get update
sudo apt-get install -y wget apt-transport-https software-properties-common
wget -q https://packages.microsoft.com/config/ubuntu/18.04/packages-microsoft-prod.deb
sudo dpkg -i packages-microsoft-prod.deb
sudo apt-get update
sudo apt-get install -y powershell
安装.NET Core,.NET Core支持的版本有限,但可用版本包括常用的Ubuntu20.04,Ubuntu18.04,Ubuntu16.04,已经足够了。我这里使用ubuntu18.04,其他版本将18.04改为对应版本即可。
wget https://packages.microsoft.com/config/ubuntu/18.04/packages-microsoft-prod.deb -O packages-microsoft-prod.deb
sudo dpkg -i packages-microsoft-prod.deb
rm packages-microsoft-prod.deb
安装SDK
sudo apt-get update; \
sudo apt-get install -y apt-transport-https && \
sudo apt-get update && \
sudo apt-get install -y dotnet-sdk-6.0
安装runtime
sudo apt-get update; \
sudo apt-get install -y apt-transport-https && \
sudo apt-get update && \
sudo apt-get install -y aspnetcore-runtime-6.0
进入到msquic目录,首先检查依赖
./scripts/prepare-machine.ps1 -Configuration Dev
然后构建项目。构建过程中遇到了cmake版本过低的错误,需要手动下载新版的cmake
./scripts/build.ps1
下载cmake
sudo apt remove --purge cmake
hash -r
sudo snap install cmake –classic
cmake --version
接下来运行msquic自带的一个简单样例,通过这个样例来对msquic进行分析。
首先生成证书密钥。
openssl req -nodes -new -x509 -keyout server.key -out server.cert
然后输入指令pwsh,进入powershell。运行服务端,server.cert和server.key的位置根据实际位置修改。
./quicsample -server -cert_file:/root/msquic/server.cert -key_file:/root/msquic/server.key
服务端运行结果:

运行客户端,端口号4567是代码中写好的,-unsecur表示不检查证书。
./quicsample -client -target:127.0.0.1 4567 -unsecur
客户端运行结果:

再看一下服务端的反馈信息:

3.2 源码分析
以上面运行的sample为例,简单地分析msquic源码。
首先是服务器运行的部分。
void
RunServer(
_In_ int argc,
_In_reads_(argc) _Null_terminated_ char* argv[]
)
{
QUIC_STATUS Status;
HQUIC Listener = NULL;
//配置侦听器用于在所有IP上侦听的地址
//地址和给定的UDP端口。
QUIC_ADDR Address = {0};
QuicAddrSetFamily(&Address, QUIC_ADDRESS_FAMILY_UNSPEC);
QuicAddrSetPort(&Address, UdpPort);
// 根据命令行加载服务器配置。
if (!ServerLoadConfiguration(argc, argv)) {
return;
}
// 创建/分配新的侦听器对象。
if (QUIC_FAILED(Status = MsQuic->ListenerOpen(Registration, ServerListenerCallback, NULL, &Listener))) {
printf("ListenerOpen failed, 0x%x!\n", Status);
goto Error;
}
// 开始侦听传入的连接。
if (QUIC_FAILED(Status = MsQuic->ListenerStart(Listener, &Alpn, 1, &Address))) {
printf("ListenerStart failed, 0x%x!\n", Status);
goto Error;
}
// 继续侦听连接,直到按下Enter键。
printf("Press Enter to exit.\n\n");
getchar();
Error:
if (Listener != NULL) {
MsQuic->ListenerClose(Listener);
}
}
然后是客户端运行的部分。
void
RunClient(
_In_ int argc,
_In_reads_(argc) _Null_terminated_ char* argv[]
)
{
// 根据“unsecure”命令行选项加载客户端配置。
if (!ClientLoadConfiguration(GetFlag(argc, argv, "unsecure"))) {
return;
}
QUIC_STATUS Status;
const char* ResumptionTicketString = NULL;
HQUIC Connection = NULL;
// 分配一个新的连接对象。
if (QUIC_FAILED(Status = MsQuic->ConnectionOpen(Registration, ClientConnectionCallback, NULL, &Connection))) {
printf("ConnectionOpen failed, 0x%x!\n", Status);
goto Error;
}
if ((ResumptionTicketString = GetValue(argc, argv, "ticket")) != NULL) {
//如果在命令行中提供,请设置可用于恢复上一个会话的恢复凭证。
uint8_t ResumptionTicket[1024];
uint16_t TicketLength = (uint16_t)DecodeHexBuffer(ResumptionTicketString, sizeof(ResumptionTicket), ResumptionTicket);
if (QUIC_FAILED(Status = MsQuic->SetParam(Connection, QUIC_PARAM_LEVEL_CONNECTION, QUIC_PARAM_CONN_RESUMPTION_TICKET, TicketLength, ResumptionTicket))) {
printf("SetParam(QUIC_PARAM_CONN_RESUMPTION_TICKET) failed, 0x%x!\n", Status);
goto Error;
}
}
// 从命令行获取目标/服务器名称或IP。
const char* Target;
if ((Target = GetValue(argc, argv, "target")) == NULL) {
printf("Must specify '-target' argument!\n");
Status = QUIC_STATUS_INVALID_PARAMETER;
goto Error;
}
printf("[conn][%p] Connecting...\n", Connection);
// 启动与服务器的连接。
if (QUIC_FAILED(Status = MsQuic->ConnectionStart(Connection, Configuration, QUIC_ADDRESS_FAMILY_UNSPEC, Target, UdpPort))) {
printf("ConnectionStart failed, 0x%x!\n", Status);
goto Error;
}
Error:
if (QUIC_FAILED(Status) && Connection != NULL) {
MsQuic->ConnectionClose(Connection);
}
}
四、小结
Quic有很多优点。它将UDP协议的速度和性能与TCP的安全性和可靠性结合起来。同时也解决了http1和http1.1、http2中的一些缺点,极大地优化了互联网传输体验。
作者:NP114