基于UDP的可靠传输——QUIC 协议

一、UDP协议

UDP用户数据报协议,非连接的协议,传输数据之前源端和终端不建立连接,当它想传送时直接去抓取来自应用程序的数据,并尽可能快地把它扔到网络上。

UDP传输协议的特点

  • UDP无连接,时间上不存在建立连接需要的时延。
  • UDP没有拥塞控制,应用层能够更好的控制要发送的数据和发送时间,网络中的拥塞控制也不会影响主机的发送速率。
  • UDP提供尽最大努力的交付,不保证可靠交付。所有维护传输可靠性的工作需要用户在应用层来完成。
  • UDP是面向报文的,对应用层交下来的报文,添加首部后直接向下交付给IP层,既不合并,也不拆分,保留这些报文的边界。
  • UDP常用一次性传输比较少量数据的网络应用,如DNS,SNMP等;

UDP的首部格式
基于UDP的可靠传输——QUIC 协议_第1张图片

UDP校验

计算校验和的时候,需要在UDP数据报之前增加12字节的伪首部,伪首部并不是UDP真正的首部。 仅仅是为了计算校验和。这样的校验和,既检查了UDP数据报,又对IP数据报的源IP地址和目的IP地址进行了检验

UDP客户端和服务器端编程示例

客户端:

#include
#include
#include
#include
#include
#include
#include
int main()
{
    //创建套接字,SOCK_DGRAM为UDP数据报套接字
    int fd=socket(AF_INET,SOCK_DGRAM,0);
    assert(fd!=-1);

    struct sockaddr_in saddr;//服务器套接字结构
    struct sockaddr_in caddr;//客户端套接字结构
    //saddr其实有四项成员,最后一项用来占位的,必须搞为0,索性我们开始直接给全部置为0,后面再来绑定ip和端口
    saddr.sin_family=AF_INET;//地址族,TCP/ipv4协议族
    saddr.sin_port=htons(6000);//将端口值从小端序列转换为大端序列
    saddr.sin_addr.s_addr=inet_addr("172.27.209.173");//将一个点分十进制的ip地址转换为一个长整型数

    while(1)
    {
        //创建发送缓冲区
        char buff[128]={0};
        //从标准输入中输入数据
        fgets(buff,128,stdin);
        //设置退出暗号
        if(strncmp(buff,"break",5)==0)
        break;
        //向服务器发送数据
        sendto(fd,buff,sizeof(buff)-1,0,(struct sockaddr*)&saddr,sizeof(saddr));
        //将发送缓冲区转换成接收缓冲区
        memset(buff,0,128);
        int len = sizeof(saddr);
        //接收服务器的数据回复
        recvfrom(fd,buff,127,0,(struct sockaddr*)&saddr,(socklen_t*)&len);
        printf("buff: %s \n",buff);
    }
    close(fd);
}

服务器端:

#include
#include
#include
#include
#include
#include
int main()
{
    //创建套接字,SOCK_DGRAM为UDP数据报套接字
    int fd=socket(AF_INET,SOCK_DGRAM,0);
    assert(fd!=-1);

    struct sockaddr_in saddr;//服务器套接字结构
    struct sockaddr_in caddr;//客户端套接字结构
    //saddr其实有四项成员,最后一项用来占位的,必须搞为0,索性我们开始直接给全部置为0,后面再来绑定ip和端口
    saddr.sin_family=AF_INET;//地址族,TCP/ipv4协议族
    saddr.sin_port=htons(6000);//将端口值从小端序列转换为大端序列
    saddr.sin_addr.s_addr=inet_addr("172.27.209.173");//将一个点分十进制的ip地址转换为一个长整型数

    //将套接字fd与本地ip地址绑定【此处注意强转】
    int res=bind(fd,(struct sockaddr*)&saddr,sizeof(saddr));
    assert(res!=-1);

    /*UDP协议传输中没有监听队列这个东西*/

    while(1)
    {
        //接收缓冲区
        char buff[128]={0};
        int len=sizeof(caddr);
        //接收来自客户端发送的数据
        recvfrom(fd,buff,127,0,(struct sockaddr*)&caddr,(socklen_t*)&len);
        printf("buff: %s\n",buff);
        //将OK发送回客户端
        sendto(fd,"ok\n",3,0,(struct sockaddr*)&caddr,(socklen_t)len);
    }
    close(fd);
}

二、QUIC协议

基于UDP的可靠传输——QUIC 协议_第2张图片

1.QUIC 是如何实现可靠传输的?

一个 Packet 报文中可以存放多个 QUIC Frame。每一个 Frame 都有明确的类型,针对类型的不同,功能也不同,自然格式也不同。

 基于UDP的可靠传输——QUIC 协议_第3张图片基于UDP的可靠传输——QUIC 协议_第4张图片基于UDP的可靠传输——QUIC 协议_第5张图片

 举例 Stream 类型的 Frame 格式,Stream 可以认为就是一条 HTTP 请求:

  • Stream ID 作用:多个并发传输的 HTTP 消息,通过不同的 Stream ID 加以区别,类似于 HTTP2 的 Stream ID;
  • Offset 作用:类似于 TCP 协议中的 Seq 序号,保证数据的顺序性和可靠性
  • Length 作用:指明了 Frame 数据的长度。

数据包 Packet N 丢失了,后面重传该数据包的编号为 Packet N+2丢失的数据包和重传的数据包 Stream ID 与 Offset 都一致,说明这两个数据包的内容一致。这些数据包传输到接收端后,接收端能根据 Stream ID Offset 字段信息将 Stream x Stream x+y 按照顺序组织起来,然后交给应用程序处理。
基于UDP的可靠传输——QUIC 协议_第6张图片

总的来说,QUIC 通过单向递增的 Packet Number,配合 Stream ID 与 Offset 字段信息,可以支持乱序确认而不影响数据包的正确组装,摆脱了TCP 必须按顺序确认应答 ACK 的限制,解决了 TCP 因某个数据包重传而阻塞后续所有待发送数据包的问题。

2.QUIC 是如何解决 TCP 队头阻塞问题的?

HTTP/2 的队头阻塞

HTTP/2 多个 Stream 请求都是在一条 TCP 连接上传输,这意味着多个 Stream 共用同一个 TCP 滑动窗口,那么当发生数据丢失,滑动窗口是无法往前移动的,此时就会阻塞住所有的 HTTP 请求,这属于 TCP 层队头阻塞。

基于UDP的可靠传输——QUIC 协议_第7张图片

没有队头阻塞的 QUIC

QUIC 给每一个 Stream 都分配了一个独立的滑动窗口,这样使得一个连接上的多个 Stream 之间没有依赖关系,都是相互独立的各自控制的滑动窗口

假如 Stream2 丢了一个 UDP 包,也只会影响 Stream2 的处理,不会影响其他 Stream,与 HTTP/2 不同,HTTP/2 只要某个流中的数据包丢失了,同一个HTTP连接的其他流也会因此受影响。

基于UDP的可靠传输——QUIC 协议_第8张图片

3.QUIC 是如何做流量控制的?

同一个 Stream 的数据也是要保证顺序的,不然无法实现可靠传输,因此同一个 Stream 的数据包丢失了,也会造成窗口无法滑动

QUIC 的 每个 Stream 都有各自的滑动窗口,不同 Stream 互相独立,队头的 Stream A 被阻塞后,不妨碍 StreamB、C的读取。而对于 HTTP/2 而言,所有的 Stream 都跑在一条 TCP 连接上,而这些 Stream 共享一个滑动窗口,因此同一个连接内,Stream A 被阻塞后, StreamB、StreamC 必须等待。

QUIC 实现了两种级别的流量控制,分别为 StreamConnection 两种级别:

  • Stream 级别的流量控制:Stream 可以认为就是一条 HTTP 请求,每个 Stream 都有独立的滑动窗口,所以每个 Stream 都可以做流量控制,防止单个 Stream 消耗连接的全部接收缓冲。
  • Connection 级别的流量控制:限制连接中所有 Stream 相加起来的总字节数,防止发送方超过连接的缓冲容量。

Stream 级别的流量控制:

基于UDP的可靠传输——QUIC 协议_第9张图片

Connection 级别的流量控制基于UDP的可靠传输——QUIC 协议_第10张图片

4.QUIC 对拥塞控制改进

因为 QUIC 处于应用层,所以就可以针对不同的应用设置不同的拥塞控制算法

5.QUIC 更快的连接建立

QUIC 内部包含了 TLS,它在自己的帧会携带 TLS 里的“记录”,再加上 QUIC 使用的是 TLS1.3,因此仅需 1 个 RTT 就可以「同时」完成建立连接与密钥协商,甚至在第二次连接的时候,应用数据包可以和 QUIC 握手信息一起发送,达到 0-RTT 的效果。

QUIC 具体握手过程如下:

  1. 客户端判断本地是否已有服务器的全部配置参数(证书配置信息),如果有则直接跳转到(5),否则继续;
  2. 客户端向服务器发送CHLO消息请求服务器传输配置参数
  3. 服务器收到 CHLO回复REJ消息其中包含服务器的部分配置参数
  4. 客户端收到REJ提取并存储服务器配置参数,跳回到(1) ;
  5. 客户端向服务器发送 full client hello 消息,开始正式握手,消息中包括客户端选择的一个公开数。此时客户端根据获取的服务器配置参数客户端选择的公开数,可以计算出初始密钥 K1
  6. 服务器收到 full client hello,如果同意连接,根据客户端的公开数计算出初始密钥 K1,回复 SHLO消息,SHLO 用初始密钥 K1 加密,并且其中包含服务器选择的一个临时公开数
  7. 客户端收到服务器的回复,如果是 SHLO,则尝试用初始密钥 K1 解密,提取出临时公开数
  8. 客户端和服务器根据临时公开数和初始密钥 K1,各自基于 SHA-256 算法推导出会话密钥 K2;
  9. 双方更换为使用会话密钥 K2 通信,初始密钥 K1 此时已无用,QUIC 握手过程完毕。

上述QUIC握手步骤,其中步骤1-4均为客户端获取服务器配置信息步骤5-8为真正的握手阶段耗时1RTT

基于UDP的可靠传输——QUIC 协议_第11张图片

6.QUIC 是如何迁移连接的?

那么当移动设备的网络从 4G 切换到 WIFI 时,意味着 IP 地址变化了,那么就必须要断开连接,然后重新建立 TCP 连接

而建立连接的过程包含 TCP 三次握手和 TLS 四次握手的时延,以及 TCP 慢启动的减速过程,给用户的感觉就是网络突然卡顿了一下,因此连接的迁移成本是很高的。

QUIC 协议没有用四元组的方式来“绑定”连接,而是通过64 位的随机数作为连接 ID来标记通信的两个端点,客户端和服务器可以各自选择一组 ID 来标记自己,因此即使移动设备的网络变化后,导致 IP 地址变化了,只要仍保有上下文信息(比如连接 ID、TLS 密钥等),就可以“无缝”地复用原连接,消除重连的成本,没有丝毫卡顿感,达到了连接迁移的功能。

UDP通信协议详解_udp_神厨小福贵!-DevPress官方社区
4.17 如何基于 UDP 协议实现可靠传输? | 小林coding
QUIC 协议原理浅解_腾讯技术工程的博客-CSDN博客
HTTP/3核心概念之QUIC - 掘金
10 分钟讲完 QUIC 协议 - 掘金

你可能感兴趣的:(计算机网络,udp,网络,网络协议)