UDP用户数据报协议,非连接的协议,传输数据之前源端和终端不建立连接,当它想传送时直接去抓取来自应用程序的数据,并尽可能快地把它扔到网络上。
在计算校验和的时候,需要在UDP数据报之前增加12字节的伪首部,伪首部并不是UDP真正的首部。 仅仅是为了计算校验和。这样的校验和,既检查了UDP数据报,又对IP数据报的源IP地址和目的IP地址进行了检验。
客户端:
#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);
}
一个 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 因某个数据包重传而阻塞后续所有待发送数据包的问题。
HTTP/2 的队头阻塞
HTTP/2 多个 Stream 请求都是在一条 TCP 连接上传输,这意味着多个 Stream 共用同一个 TCP 滑动窗口,那么当发生数据丢失,滑动窗口是无法往前移动的,此时就会阻塞住所有的 HTTP 请求,这属于 TCP 层队头阻塞。
没有队头阻塞的 QUIC
QUIC 给每一个 Stream 都分配了一个独立的滑动窗口,这样使得一个连接上的多个 Stream 之间没有依赖关系,都是相互独立的,各自控制的滑动窗口。
假如 Stream2 丢了一个 UDP 包,也只会影响 Stream2 的处理,不会影响其他 Stream,与 HTTP/2 不同,HTTP/2 只要某个流中的数据包丢失了,同一个HTTP连接的其他流也会因此受影响。
同一个 Stream 的数据也是要保证顺序的,不然无法实现可靠传输,因此同一个 Stream 的数据包丢失了,也会造成窗口无法滑动。
QUIC 的 每个 Stream 都有各自的滑动窗口,不同 Stream 互相独立,队头的 Stream A 被阻塞后,不妨碍 StreamB、C的读取。而对于 HTTP/2 而言,所有的 Stream 都跑在一条 TCP 连接上,而这些 Stream 共享一个滑动窗口,因此同一个连接内,Stream A 被阻塞后, StreamB、StreamC 必须等待。
QUIC 实现了两种级别的流量控制,分别为 Stream 和 Connection 两种级别:
Stream 级别的流量控制:
因为 QUIC 处于应用层,所以就可以针对不同的应用设置不同的拥塞控制算法。
QUIC 内部包含了 TLS,它在自己的帧会携带 TLS 里的“记录”,再加上 QUIC 使用的是 TLS1.3,因此仅需 1 个 RTT 就可以「同时」完成建立连接与密钥协商,甚至在第二次连接的时候,应用数据包可以和 QUIC 握手信息一起发送,达到 0-RTT 的效果。
QUIC 具体握手过程如下:
上述QUIC握手步骤,其中步骤1-4均为客户端获取服务器配置信息,步骤5-8为真正的握手阶段,耗时1RTT。
那么当移动设备的网络从 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 协议 - 掘金