QUIC网络协议分析

MurraywV 2022-01-17 01:12:32

一、如何使用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

...全文
729 回复 打赏 收藏 转发到动态 举报
写回复
用AI写文章
回复
切换为时间正序
请发表友善的回复…
发表回复

571

社区成员

发帖
与我相关
我的任务
社区描述
软件工程教学新范式,强化专项技能训练+基于项目的学习PBL。Git仓库:https://gitee.com/mengning997/se
软件工程 高校
社区管理员
  • 码农孟宁
加入社区
  • 近7日
  • 近30日
  • 至今

试试用AI创作助手写篇文章吧