一、UDP协议
UDP用户数据报协议,非连接的协议,传输数据之前源端和终端不建立连接,当它想传送时直接去抓取来自应用程序的数据,并尽可能快地把它扔到网络上。
UDP传输协议的特点
- UDP无连接,时间上不存在建立连接需要的时延。
- UDP没有拥塞控制,应用层能够更好的控制要发送的数据和发送时间,网络中的拥塞控制也不会影响主机的发送速率。
- UDP提供尽最大努力的交付,不保证可靠交付。所有维护传输可靠性的工作需要用户在应用层来完成。
- UDP是面向报文的,对应用层交下来的报文,添加首部后直接向下交付给IP层,既不合并,也不拆分,保留这些报文的边界。
- UDP常用一次性传输比较少量数据的网络应用,如DNS,SNMP等;
UDP的首部格式
![](https://i-blog.csdnimg.cn/blog_migrate/c748c520ed4aa234e488290458912872.png)
UDP校验
在计算校验和的时候,需要在UDP数据报之前增加12字节的伪首部,伪首部并不是UDP真正的首部。 仅仅是为了计算校验和。这样的校验和,既检查了UDP数据报,又对IP数据报的源IP地址和目的IP地址进行了检验。
UDP客户端和服务器端编程示例
客户端:
#include<sys/socket.h>
#include<stdio.h>
#include<assert.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<string.h>
#include<unistd.h>
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<sys/socket.h>
#include<stdio.h>
#include<assert.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<unistd.h>
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协议
1.QUIC 是如何实现可靠传输的?
一个 Packet 报文中可以存放多个 QUIC Frame。每一个 Frame 都有明确的类型,针对类型的不同,功能也不同,自然格式也不同。
举例 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 按照顺序组织起来,然后交给应用程序处理。
总的来说,QUIC 通过单向递增的 Packet Number,配合 Stream ID 与 Offset 字段信息,可以支持乱序确认而不影响数据包的正确组装,摆脱了TCP 必须按顺序确认应答 ACK 的限制,解决了 TCP 因某个数据包重传而阻塞后续所有待发送数据包的问题。
2.QUIC 是如何解决 TCP 队头阻塞问题的?
HTTP/2 的队头阻塞
HTTP/2 多个 Stream 请求都是在一条 TCP 连接上传输,这意味着多个 Stream 共用同一个 TCP 滑动窗口,那么当发生数据丢失,滑动窗口是无法往前移动的,此时就会阻塞住所有的 HTTP 请求,这属于 TCP 层队头阻塞。
没有队头阻塞的 QUIC
QUIC 给每一个 Stream 都分配了一个独立的滑动窗口,这样使得一个连接上的多个 Stream 之间没有依赖关系,都是相互独立的,各自控制的滑动窗口。
假如 Stream2 丢了一个 UDP 包,也只会影响 Stream2 的处理,不会影响其他 Stream,与 HTTP/2 不同,HTTP/2 只要某个流中的数据包丢失了,同一个HTTP连接的其他流也会因此受影响。
3.QUIC 是如何做流量控制的?
同一个 Stream 的数据也是要保证顺序的,不然无法实现可靠传输,因此同一个 Stream 的数据包丢失了,也会造成窗口无法滑动。
QUIC 的 每个 Stream 都有各自的滑动窗口,不同 Stream 互相独立,队头的 Stream A 被阻塞后,不妨碍 StreamB、C的读取。而对于 HTTP/2 而言,所有的 Stream 都跑在一条 TCP 连接上,而这些 Stream 共享一个滑动窗口,因此同一个连接内,Stream A 被阻塞后, StreamB、StreamC 必须等待。
QUIC 实现了两种级别的流量控制,分别为 Stream 和 Connection 两种级别:
- Stream 级别的流量控制:Stream 可以认为就是一条 HTTP 请求,每个 Stream 都有独立的滑动窗口,所以每个 Stream 都可以做流量控制,防止单个 Stream 消耗连接的全部接收缓冲。
- Connection 级别的流量控制:限制连接中所有 Stream 相加起来的总字节数,防止发送方超过连接的缓冲容量。
Stream 级别的流量控制:
Connection 级别的流量控制:
4.QUIC 对拥塞控制改进
因为 QUIC 处于应用层,所以就可以针对不同的应用设置不同的拥塞控制算法。
5.QUIC 更快的连接建立
QUIC 内部包含了 TLS,它在自己的帧会携带 TLS 里的“记录”,再加上 QUIC 使用的是 TLS1.3,因此仅需 1 个 RTT 就可以「同时」完成建立连接与密钥协商,甚至在第二次连接的时候,应用数据包可以和 QUIC 握手信息一起发送,达到 0-RTT 的效果。
QUIC 具体握手过程如下:
- 客户端判断本地是否已有服务器的全部配置参数(证书配置信息),如果有则直接跳转到(5),否则继续;
- 客户端向服务器发送CHLO消息,请求服务器传输配置参数;
- 服务器收到 CHLO,回复REJ消息,其中包含服务器的部分配置参数;
- 客户端收到REJ,提取并存储服务器配置参数,跳回到(1) ;
- 客户端向服务器发送 full client hello 消息,开始正式握手,消息中包括客户端选择的一个公开数。此时客户端根据获取的服务器配置参数和客户端选择的公开数,可以计算出初始密钥 K1;
- 服务器收到 full client hello,如果同意连接,根据客户端的公开数计算出初始密钥 K1,回复 SHLO消息,SHLO 用初始密钥 K1 加密,并且其中包含服务器选择的一个临时公开数;
- 客户端收到服务器的回复,如果是 SHLO,则尝试用初始密钥 K1 解密,提取出临时公开数;
- 客户端和服务器根据临时公开数和初始密钥 K1,各自基于 SHA-256 算法推导出会话密钥 K2;
- 双方更换为使用会话密钥 K2 通信,初始密钥 K1 此时已无用,QUIC 握手过程完毕。
上述QUIC握手步骤,其中步骤1-4均为客户端获取服务器配置信息,步骤5-8为真正的握手阶段,耗时1RTT。
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 协议 - 掘金