MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Linux C语言UDP编程的可靠性保障

2024-08-083.1k 阅读

UDP 协议概述

UDP(User Datagram Protocol)即用户数据报协议,是一种无连接的传输层协议。与 TCP(Transmission Control Protocol)不同,UDP 不保证数据的可靠传输、不进行流量控制和拥塞控制。UDP 直接将数据封装成 UDP 数据包发送出去,每个数据包独立处理,不维护连接状态。

UDP 协议具有以下特点:

  1. 无连接:发送方和接收方之间不需要建立连接,直接发送数据。这使得 UDP 协议的开销小,传输速度快,适合一些对实时性要求较高,但对数据准确性要求相对较低的应用场景,如音频、视频流传输等。
  2. 不可靠:UDP 不保证数据一定能到达接收方,也不保证数据到达的顺序与发送顺序一致。数据在传输过程中可能会丢失、重复或乱序。
  3. 面向数据报:UDP 以数据报为单位进行传输,每个数据报包含了完整的源地址和目的地址信息。应用层交给 UDP 多长的报文,UDP 就照样发送,即一次发送一个报文。

UDP 协议的头部结构

UDP 头部长度固定为 8 字节,由以下四个字段组成:

  1. 源端口号(Source Port):16 位,标识发送端应用程序的端口号。
  2. 目的端口号(Destination Port):16 位,标识接收端应用程序的端口号。
  3. 长度(Length):16 位,指示 UDP 头部和数据部分的总长度(以字节为单位)。最小为 8 字节(仅 UDP 头部)。
  4. 校验和(Checksum):16 位,用于检测 UDP 数据包在传输过程中是否发生错误。它是可选的,若不使用校验和,此字段全为 0。

Linux C 语言 UDP 编程基础

在 Linux 环境下,使用 C 语言进行 UDP 编程主要涉及以下几个系统调用:

  1. socket():创建一个套接字(socket),指定协议族(如 AF_INET 表示 IPv4)、套接字类型(如 SOCK_DGRAM 表示 UDP)和协议(通常为 0)。
#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>

int main() {
    int sockfd;
    sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd < 0) {
        perror("socket creation failed");
        return -1;
    }
    // 后续操作
    close(sockfd);
    return 0;
}
  1. bind():将套接字绑定到一个本地地址和端口上。对于 UDP 服务器,通常需要绑定端口,以便接收来自客户端的数据。
struct sockaddr_in servaddr;
memset(&servaddr, 0, sizeof(servaddr));
memset(&cliaddr, 0, sizeof(cliaddr));

servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = INADDR_ANY;
servaddr.sin_port = htons(12345);

if (bind(sockfd, (const struct sockaddr *)&servaddr, sizeof(servaddr)) < 0) {
    perror("bind failed");
    close(sockfd);
    return -1;
}
  1. sendto():用于向指定的目的地址发送数据。
char buffer[1024] = "Hello, UDP!";
struct sockaddr_in cliaddr;
memset(&cliaddr, 0, sizeof(cliaddr));
cliaddr.sin_family = AF_INET;
cliaddr.sin_addr.s_addr = inet_addr("192.168.1.100");
cliaddr.sin_port = htons(54321);

sendto(sockfd, (const char *)buffer, strlen(buffer), MSG_CONFIRM, (const struct sockaddr *) &cliaddr, sizeof(cliaddr));
  1. recvfrom():用于从套接字接收数据,并获取发送方的地址信息。
char buffer[1024];
struct sockaddr_in cliaddr;
socklen_t len = sizeof(cliaddr);
int n = recvfrom(sockfd, (char *)buffer, 1024, MSG_WAITALL, (struct sockaddr *) &cliaddr, &len);
buffer[n] = '\0';
printf("Received message: %s\n", buffer);

UDP 编程可靠性问题分析

数据丢失

UDP 数据丢失可能发生在多个环节:

  1. 网络拥塞:当网络中的数据流量过大,路由器的缓冲区满时,新到达的 UDP 数据包可能会被丢弃。与 TCP 不同,UDP 没有拥塞控制机制,不会主动降低发送速率。
  2. 链路故障:物理链路的故障,如网线断开、无线信号中断等,可能导致 UDP 数据包无法传输。
  3. 接收方缓冲区溢出:如果接收方处理数据的速度较慢,而发送方持续高速发送数据,接收方的缓冲区可能会溢出,从而导致后续到达的数据包丢失。

数据重复

在网络传输过程中,由于某些原因,如路由重传、网络设备故障等,UDP 数据包可能会被重复发送。接收方如果没有相应的机制来处理重复数据,可能会对应用程序产生不良影响,例如导致重复操作。

数据乱序

UDP 是无连接的协议,数据包在网络中独立传输。不同的数据包可能会经过不同的路由路径,导致它们到达接收方的顺序与发送顺序不一致。对于一些对数据顺序敏感的应用,如视频流的帧顺序,乱序的数据可能会影响播放效果。

Linux C 语言 UDP 编程可靠性保障方法

基于超时重传机制

超时重传是一种常用的提高 UDP 可靠性的方法。发送方在发送数据后,启动一个定时器。如果在规定的时间内没有收到接收方的确认(ACK),则重新发送该数据。

  1. 设置定时器:在 Linux 中,可以使用 alarm() 函数或 setitimer() 函数来设置定时器。
#include <sys/time.h>
#include <stdio.h>
#include <unistd.h>

void set_timeout(int seconds) {
    struct itimerval itv;
    itv.it_value.tv_sec = seconds;
    itv.it_value.tv_usec = 0;
    itv.it_interval.tv_sec = 0;
    itv.it_interval.tv_usec = 0;
    setitimer(ITIMER_REAL, &itv, NULL);
}
  1. 处理超时信号:通过注册信号处理函数来处理定时器超时信号,在信号处理函数中重新发送数据。
#include <signal.h>
#include <stdio.h>

void timeout_handler(int signum) {
    printf("Timeout occurred, resending data...\n");
    // 这里编写重新发送数据的代码
}

int main() {
    signal(SIGALRM, timeout_handler);
    set_timeout(5);
    // 发送数据代码
    while (1) {
        // 主循环其他操作
    }
    return 0;
}

序列号机制

为每个发送的 UDP 数据包分配一个唯一的序列号。接收方通过序列号来检测和处理重复数据,并对乱序数据进行重新排序。

  1. 发送方添加序列号:在发送数据前,为每个数据包添加序列号字段。
struct packet {
    int seq_num;
    char data[1024];
};

struct packet pkt;
pkt.seq_num = sequence_number++;
strcpy(pkt.data, "Hello, UDP with sequence");
sendto(sockfd, (const char *)&pkt, sizeof(pkt), MSG_CONFIRM, (const struct sockaddr *) &cliaddr, sizeof(cliaddr));
  1. 接收方处理序列号:接收方维护一个已接收序列号的列表,在接收到新数据包时,检查序列号是否重复。
int received_seq[1000] = {0};
struct packet received_pkt;
recvfrom(sockfd, (char *)&received_pkt, sizeof(received_pkt), MSG_WAITALL, (struct sockaddr *) &cliaddr, &len);
if (!received_seq[received_pkt.seq_num]) {
    received_seq[received_pkt.seq_num] = 1;
    // 处理数据
}

确认机制

发送方发送数据后,等待接收方的确认消息。接收方在正确接收到数据后,向发送方发送确认消息。

  1. 发送方等待确认:发送数据后,使用 select()epoll() 等多路复用函数等待接收方的确认消息。
fd_set read_fds;
FD_ZERO(&read_fds);
FD_SET(sockfd, &read_fds);
struct timeval timeout;
timeout.tv_sec = 5;
timeout.tv_usec = 0;
int activity = select(sockfd + 1, &read_fds, NULL, NULL, &timeout);
if (activity > 0 && FD_ISSET(sockfd, &read_fds)) {
    // 接收确认消息
} else {
    // 超时,重发数据
}
  1. 接收方发送确认:接收方在正确接收到数据后,向发送方发送确认消息。
char ack_message[10] = "ACK";
sendto(sockfd, (const char *)ack_message, strlen(ack_message), MSG_CONFIRM, (const struct sockaddr *) &servaddr, sizeof(servaddr));

流量控制

为了避免接收方缓冲区溢出,可以采用流量控制机制。接收方告知发送方自己当前的接收能力,发送方根据接收方的反馈调整发送速率。

  1. 接收方反馈:接收方定期向发送方发送自己的缓冲区使用情况或剩余空间。
struct buffer_status {
    int free_space;
};

struct buffer_status status;
status.free_space = get_free_buffer_space();
sendto(sockfd, (const char *)&status, sizeof(status), MSG_CONFIRM, (const struct sockaddr *) &servaddr, sizeof(servaddr));
  1. 发送方调整速率:发送方根据接收方反馈的缓冲区状态,调整发送速率。例如,如果接收方缓冲区剩余空间较小,发送方可以降低发送频率。
struct buffer_status received_status;
recvfrom(sockfd, (char *)&received_status, sizeof(received_status), MSG_WAITALL, (struct sockaddr *) &cliaddr, &len);
if (received_status.free_space < 1024) {
    // 降低发送速率
    sleep(1);
}

校验和增强

虽然 UDP 本身已经有校验和机制,但在一些对数据准确性要求极高的场景下,可以进一步增强校验和。例如,除了 UDP 头部和数据部分,还可以对应用层数据进行额外的校验和计算。

  1. 自定义校验和计算:在发送方,对应用层数据进行自定义校验和计算,并将结果添加到数据包中。
unsigned short calculate_custom_checksum(char *data, int length) {
    unsigned long sum = 0;
    while (length > 1) {
        sum += *(unsigned short *)data;
        data += 2;
        length -= 2;
    }
    if (length > 0) {
        sum += *(unsigned char *)data;
    }
    while (sum >> 16) {
        sum = (sum & 0xFFFF) + (sum >> 16);
    }
    return ~sum;
}

struct custom_packet {
    unsigned short custom_checksum;
    char data[1024];
};

struct custom_packet custom_pkt;
custom_pkt.custom_checksum = calculate_custom_checksum(custom_pkt.data, strlen(custom_pkt.data));
sendto(sockfd, (const char *)&custom_pkt, sizeof(custom_pkt), MSG_CONFIRM, (const struct sockaddr *) &cliaddr, sizeof(cliaddr));
  1. 接收方校验:接收方接收到数据包后,重新计算校验和,并与数据包中的校验和进行比较。
struct custom_packet received_custom_pkt;
recvfrom(sockfd, (char *)&received_custom_pkt, sizeof(received_custom_pkt), MSG_WAITALL, (struct sockaddr *) &cliaddr, &len);
unsigned short received_checksum = received_custom_pkt.custom_checksum;
unsigned short calculated_checksum = calculate_custom_checksum(received_custom_pkt.data, strlen(received_custom_pkt.data));
if (received_checksum == calculated_checksum) {
    // 数据校验通过,处理数据
} else {
    // 数据校验失败
}

综合示例代码

以下是一个综合应用上述可靠性保障方法的 UDP 示例代码,包括超时重传、序列号、确认机制和流量控制:

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

#define PORT 12345
#define BUFFER_SIZE 1024
#define SERVER_IP "192.168.1.100"

int sockfd;
struct sockaddr_in servaddr, cliaddr;
int sequence_number = 0;
int received_seq[1000] = {0};

void timeout_handler(int signum) {
    printf("Timeout occurred, resending data...\n");
    // 重新发送数据
}

unsigned short calculate_custom_checksum(char *data, int length) {
    unsigned long sum = 0;
    while (length > 1) {
        sum += *(unsigned short *)data;
        data += 2;
        length -= 2;
    }
    if (length > 0) {
        sum += *(unsigned char *)data;
    }
    while (sum >> 16) {
        sum = (sum & 0xFFFF) + (sum >> 16);
    }
    return ~sum;
}

void send_data_with_reliability(char *data) {
    struct packet {
        int seq_num;
        unsigned short custom_checksum;
        char data[BUFFER_SIZE];
    };

    struct packet pkt;
    pkt.seq_num = sequence_number++;
    strcpy(pkt.data, data);
    pkt.custom_checksum = calculate_custom_checksum(pkt.data, strlen(pkt.data));

    int retry_count = 0;
    while (1) {
        sendto(sockfd, (const char *)&pkt, sizeof(pkt), MSG_CONFIRM, (const struct sockaddr *) &cliaddr, sizeof(cliaddr));
        fd_set read_fds;
        FD_ZERO(&read_fds);
        FD_SET(sockfd, &read_fds);
        struct timeval timeout;
        timeout.tv_sec = 5;
        timeout.tv_usec = 0;
        int activity = select(sockfd + 1, &read_fds, NULL, NULL, &timeout);
        if (activity > 0 && FD_ISSET(sockfd, &read_fds)) {
            char ack_message[10];
            socklen_t len = sizeof(cliaddr);
            recvfrom(sockfd, (char *)ack_message, 10, MSG_WAITALL, (const struct sockaddr *) &cliaddr, &len);
            if (strcmp(ack_message, "ACK") == 0) {
                break;
            }
        } else {
            if (retry_count >= 3) {
                printf("Max retry reached, giving up.\n");
                break;
            }
            retry_count++;
        }
    }
}

void receive_data_with_reliability() {
    struct packet {
        int seq_num;
        unsigned short custom_checksum;
        char data[BUFFER_SIZE];
    };

    struct buffer_status {
        int free_space;
    };

    struct buffer_status status;
    status.free_space = BUFFER_SIZE;
    sendto(sockfd, (const char *)&status, sizeof(status), MSG_CONFIRM, (const struct sockaddr *) &servaddr, sizeof(servaddr));

    while (1) {
        struct packet received_pkt;
        socklen_t len = sizeof(cliaddr);
        recvfrom(sockfd, (char *)&received_pkt, sizeof(received_pkt), MSG_WAITALL, (const struct sockaddr *) &cliaddr, &len);
        unsigned short received_checksum = received_pkt.custom_checksum;
        unsigned short calculated_checksum = calculate_custom_checksum(received_pkt.data, strlen(received_pkt.data));
        if (received_checksum == calculated_checksum &&!received_seq[received_pkt.seq_num]) {
            received_seq[received_pkt.seq_num] = 1;
            printf("Received message: %s\n", received_pkt.data);
            char ack_message[10] = "ACK";
            sendto(sockfd, (const char *)ack_message, strlen(ack_message), MSG_CONFIRM, (const struct sockaddr *) &servaddr, sizeof(servaddr));

            // 反馈缓冲区状态
            status.free_space = get_free_buffer_space();
            sendto(sockfd, (const char *)&status, sizeof(status), MSG_CONFIRM, (const struct sockaddr *) &servaddr, sizeof(servaddr));
        }
    }
}

int main() {
    sockfd = socket(AF_INET, SOCK_DUDP, 0);
    if (sockfd < 0) {
        perror("socket creation failed");
        return -1;
    }

    memset(&servaddr, 0, sizeof(servaddr));
    memset(&cliaddr, 0, sizeof(cliaddr));

    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = inet_addr(SERVER_IP);
    servaddr.sin_port = htons(PORT);

    cliaddr.sin_family = AF_INET;
    cliaddr.sin_addr.s_addr = INADDR_ANY;
    cliaddr.sin_port = htons(0);

    if (bind(sockfd, (const struct sockaddr *)&cliaddr, sizeof(cliaddr)) < 0) {
        perror("bind failed");
        close(sockfd);
        return -1;
    }

    signal(SIGALRM, timeout_handler);

    // 这里可以根据需求选择是作为发送方还是接收方
    // 例如:
    // send_data_with_reliability("Hello, reliable UDP!");
    receive_data_with_reliability();

    close(sockfd);
    return 0;
}

通过以上方法和示例代码,在 Linux C 语言 UDP 编程中,可以在一定程度上保障数据传输的可靠性,满足不同应用场景的需求。但需要注意的是,这些方法会增加编程的复杂性和网络开销,在实际应用中需要根据具体情况进行权衡和优化。