Bootstrap

udp可靠传输中ACK与NACK的选择

介绍

在 UDP 可靠传输中,ACK(Acknowledgment)和 NACK(Negative
Acknowledgment)是两种常用的反馈机制,用于确保数据包的成功传输。这两种机制主要用于在不可靠的 UDP
协议之上建立可靠的传输层。

如果网络环境相对稳定,丢包率较低,则使用 ACK 可以更好地保证数据的可靠性。
如果网络环境不稳定,丢包率较高,则使用 NACK 可以减少不必要的网络流量,但可能需要更复杂的重传机制来处理可能的 NACK 丢失问题。

ACK

ACK 是一种确认机制,表示接收方成功收到了数据包。 当接收方收到一个期望的数据包时,会发送一个 ACK 给发送方,确认该数据包已成功接收。通常不是收到一包就发 而是是使用分组ACK ,收到一定数量的包后发送ACK+包号

用途
ACK 用于确认数据包已被正确接收。
发送方可以根据 ACK 来确定哪些数据包需要重传。
优点
可以减少不必要的重传,提高传输效率。
提供了明确的确认信号,减少了不确定性。
缺点
在高丢包率的情况下,ACK 可能也会丢失,导致发送方无法判断数据包是否成功接收。如果使用 ACK,那么每个数据包都需要发送 ACK,这可能会增加网络流量。

NACK

NACK 是一种否定确认机制,表示接收方没有收到某个数据包。 当接收方检测到数据包丢失时,会发送一个 NACK
给发送方,指示发送方重新发送丢失的数据包

用途
NACK 用于通知发送方哪些数据包丢失了,需要重传。
只有当接收方检测到数据包丢失时才会发送 NACK。
优点
只在数据包丢失时发送 NACK,可以减少网络流量。
更加节省带宽,特别是在数据包丢失率较高的情况下。
缺点
如果数据包丢失率很高,NACK 也可能丢失,导致发送方无法得到反馈。在某些情况下,NACK 可能会导致过多的重传,影响传输效率。

丢包触发

在 UDP 可靠传输中,丢包判断逻辑是确保数据包成功传输的关键组成部分。丢包判断逻辑通常基于以下几个方面来实现:

  • 序列号:
    每个数据包都分配一个唯一的序列号,这样接收方就可以跟踪接收到的数据包顺序,并判断是否有数据包丢失。

  • 乱序包
    UDP包乱序需要使用队列重新排序,同时可以统计出某包(pack_a)后续N个包已经到达而pack_a 还未收到,则判断丢包

  • 超时重传:
    发送方为每个发送的数据包设置一个超时时间(Timeout)。
    如果在超时时间内没有收到接收方的 ACK 或 NACK,发送方就会认为该数据包丢失,并重新发送。
    两次重传的间隔设定,应超过一个 RTT

  • 滑动窗口:
    滑动窗口是一种高效的流量控制机制,可以控制发送方连续发送多个数据包而不等待每个数据包的 ACK。
    当窗口内的数据包没有收到 ACK 时,发送方会等待超时重传。

  • 确认反馈:
    接收方发送 ACK 或 NACK 作为反馈。
    ACK 表示数据包成功接收,NACK 表示数据包丢失。
    丢包判断逻辑的具体实现:

  • 重传限制
    当网络质量非常差时,一直重传可能也不成功,一个包最多重传 N次,不重传时间过久的包,

  • 优先级
    当大量重传无法成功时,判断网络是否正常,如果正常可以放弃重传,直接请求 优先级高的数据 或者关键数据

常见问题

包序号比较

在比较包序列号的先后时,不能简单的比较数值的大小,因为大部分协议设计的时候为了精简字段, 包序列号的类型通常是2字节,16bit(uint16_t),最大只能表示 65535,如果传输数据持续时间比较长,包序列号就很容易超过这个限制而从 0 开始循环,此时序列号为 0 的包就比序列号为 65535 的包要新了。
解决方法
可以通过设置一个安全距离来判断 比如最大值的一半,这样可以判断是否发生了包序列号的循环

RTT&RTO估算

在基于 UDP 的可靠传输中,估算 Round-Trip Time (RTT) 是一项重要的任务,它可以帮助优化重传策略、调整超时时间等参数。RTT 是指从发送方发送数据包到接收方接收到数据包并返回确认(ACK)所花费的时间。

RTT 估算方法:

  • 基本 RTT 估算:
    对于每个数据包,记录发送时间。
    当接收到 ACK 时,计算发送时间与接收 ACK 时间之间的差值。
    这个差值就是 RTT 的估计值。
  • 指数平滑法:
    使用指数平滑法来逐步更新 RTT 的估计值。
    新的 RTT 估计值是旧的 RTT 估计值和当前测量值的加权平均。
  • 动态调整:
    根据网络状况动态调整 RTT 估计值。
    如果网络状况变好,RTT 估计值可以逐渐减小;反之亦然。
  • 超时重传时间 (RTO):
    根据 RTT 估计值来确定超时重传时间。
    RTO 通常是 RTT 估计值加上一定的安全系数(例如 3 到 5 倍的标准偏差)。

乱序包

UDP包乱序的原因

  • 无连接协议:UDP是一种无连接的协议,它不像TCP那样在通信双方之间建立和维护一个连接。因此,UDP在发送数据包时,不会对数据包进行排序或管理,也不会在接收端进行数据包的重新排序。
  • 不同的路由和延迟:在Internet上,数据包可能需要经过多个路由器才能到达目的地。不同的数据包可能会选择不同的路由,导致它们在网络中的传输路径和延迟时间不同。因此,即使数据包在发送端是按顺序发送的,在接收端也可能以不同的顺序到达。
  • 路由器处理:路由器在转发数据包时,会对数据包进行存储、处理、合法性判定等操作。这些操作可能导致数据包在路由器中的等待时间不同,进一步加剧了数据包到达接收端的乱序现象。
  • 无流量控制和拥塞控制:UDP协议不会进行流量控制或拥塞控制,这意味着UDP可能会以任何速率向网络发送数据包。在网络拥塞的情况下,数据包可能会在网络中堆积,导致后续数据包延迟到达或丢失,从而影响数据包的顺序。

乱序现象所说明的问题

  • 网络状况复杂:UDP包乱序往往反映了网络环境的复杂性和不确定性。在网络传输过程中,数据包可能会遇到各种延迟、丢包和路由变更等问题,这些问题都可能导致数据包乱序。
  • 协议特性限制:UDP协议的快速和简单特性也限制了其在保证数据包顺序方面的能力。由于UDP没有内置的排序和重传机制,因此它无法像TCP那样确保数据包按顺序到达接收端。

乱序处理

假设

发送方发包 序列  1 2 3 4 5  6 
接收方收到  序列 1 2 6 4 5  3

当接收方收到包解析出包号是 6包 时,按照当前的丢包检测策略,会认为3 4 5 丢包了,此时不会立即触发重传。因为实际上可能只是一个乱序,3 4 5 紧接着很快就来了,这种情况需要进行判断。
通过数据统计,估计一个合理的需要等待的包个数,再触发重传
比如使用 直方图统计,统计一个样本(比如128或者256)乱序个数(0-10)出现的次数以及对应的概率 ,此时给定一个概率,比如 50%,可以获得这个概率下包的乱序个数,
当检测丢包发生时,不是立即触发一次重传,而是等待乱序的包个数之后,再进行重传

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <time.h>
#include <math.h>

#define PORT 8080
#define BUFFER_SIZE 4096
#define TIMEOUT 5000 // 超时时间,单位:毫秒
#define MAX_SEQ_NUM 100 // 最大序列号

struct Packet {
   int seq_num; // 序列号
   char data[BUFFER_SIZE]; // 数据
};

void error(const char *msg) {
   perror(msg);
   exit(1);
}

// 计算直方图
void calculate_histogram(int histogram[], int data[], int size) {
   for (int i = 0; i < size; ++i) {
       histogram[data[i]]++;
   }
}

// 计算 CDF
void calculate_cdf(int histogram[], double cdf[], int size) {
   double sum = 0.0;
   for (int i = 0; i < size; ++i) {
       sum += histogram[i];
       cdf[i] = sum / size;
   }
}

// 计算 ICDF
int calculate_icdf(double cdf[], int histogram[], double probability, int size) {
   int index = 0;
   while (cdf[index] < probability && index < size) {
       index++;
   }
   return index;
}

int main() {
   int sockfd;
   struct sockaddr_in serv_addr;
   struct Packet packet;
   fd_set readfds;
   struct timeval timeout;
   int histogram[MAX_SEQ_NUM + 1] = {0};
   double cdf[MAX_SEQ_NUM + 1];

   // 创建 UDP 套接字
   sockfd = socket(AF_INET, SOCK_DGRAM, 0);
   if (sockfd < 0)
       error("ERROR opening socket");

   // 设置服务器地址
   bzero((char *) &serv_addr, sizeof(serv_addr));
   serv_addr.sin_family = AF_INET;
   serv_addr.sin_addr.s_addr = INADDR_ANY;
   serv_addr.sin_port = htons(PORT);

   // 绑定端口
   if (bind(sockfd, (struct sockaddr *) &serv_addr, sizeof(serv_addr)) < 0)
       error("ERROR on binding");

   // 发送数据包
   for (int i = 1; i <= MAX_SEQ_NUM; ++i) {
       packet.seq_num = i;
       strcpy(packet.data, "Data packet");
       if (sendto(sockfd, &packet, sizeof(struct Packet), 0, (struct sockaddr *) &serv_addr, sizeof(serv_addr)) < 0)
           error("ERROR on sending");
   }

   // 设置超时
   timeout.tv_sec = TIMEOUT / 1000;
   timeout.tv_usec = (TIMEOUT % 1000) * 1000;

   // 等待 ACK
   FD_ZERO(&readfds);
   FD_SET(sockfd, &readfds);

   // 选择函数
   select(sockfd + 1, &readfds, NULL, NULL, &timeout);

   // 检查是否有数据可读
   if (FD_ISSET(sockfd, &readfds)) {
       struct Packet ack_packet;
       socklen_t len = sizeof(serv_addr);
       int bytes_received = recvfrom(sockfd, &ack_packet, sizeof(struct Packet), 0, (struct sockaddr *) &serv_addr, &len);
       if (bytes_received > 0) {
           histogram[ack_packet.seq_num]++;
       }
   }

   // 构建直方图
   calculate_histogram(histogram, histogram, MAX_SEQ_NUM + 1);

   // 计算 CDF
   calculate_cdf(histogram, cdf, MAX_SEQ_NUM + 1);

   // 计算 ICDF
   double probability = 0.1; // 10% 丢包率
   int icdf_value = calculate_icdf(cdf, histogram, probability, MAX_SEQ_NUM + 1);

   // 根据 ICDF 优化重传策略
   for (int i = 1; i <= icdf_value; ++i) {
       if (histogram[i] == 0) {
           packet.seq_num = i;
           printf("Resending packet with sequence number %d\n", packet.seq_num);
           if (sendto(sockfd, &packet, sizeof(struct Packet), 0, (struct sockaddr *) &serv_addr, sizeof(serv_addr)) < 0)
               error("ERROR on resending");
       }
   }

   close(sockfd);
   return 0;
}

ACK与NACK结合使用

数据包结构

数据包 (Packet):包含序列号和数据。
确认包 (AckPacket):包含序列号,用于确认接收的数据包。
否定确认包 (NackPacket):包含序列号,用于指示丢失的数据包。

发送方策略

  • 发送数据包:
    为每个数据包分配一个唯一的序列号。
    记录发送时间。
  • 超时重传:
    为每个发送的数据包设置一个超时时间。
    如果在超时时间内没有收到 ACK 或 NACK,就认为数据包丢失,并重新发送。
  • RTT 估算:
    计算从发送数据包到接收到 ACK 的时间差,作为 RTT 的估计值。
    使用指数平滑法逐步更新 RTT 估计值。
  • 重传策略:
    根据 RTT 估计值和标准偏差计算超时重传时间 (RTO)。
    如果接收到 NACK,立即重传指定的数据包。

接收方策略

  • 接收数据包:
    按照序列号排序数据包,并记录已接收的数据包。
  • 发送 ACK:
    当接收到一个期望的数据包时,发送一个 ACK 包给发送方。
  • 发送 NACK:
    当检测到序列号不连续时,发送一个 NACK 包给发送方,指示丢失的数据包。
  • 处理乱序包:
    使用滑动窗口机制来跟踪已接收的数据包。
    如果接收到序列号较大的数据包,不立即认为前面的数据包丢失。
    等待一段时间或者N个乱序包,观察是否接收到缺失的数据包。

动态调整

动态调整 RTT 估计值:
根据网络状况动态调整 RTT 估计值。
如果网络状况变好,RTT 估计值可以逐渐减小;反之亦然。
动态调整超时重传时间 (RTO):

悦读

道可道,非常道;名可名,非常名。 无名,天地之始,有名,万物之母。 故常无欲,以观其妙,常有欲,以观其徼。 此两者,同出而异名,同谓之玄,玄之又玄,众妙之门。

;