UDP传输大文件时(超过了UDP最大有效载荷1472字节),如何设计UDP 应用通信协议 ?

当利用UDP输出文件且文件大小超过了UDP承载的最大有效数据量(受网络MTU等因素限制,如以太网中UDP数据部分通常不宜超过1472字节)时,可以通过以下方式来定义数据包协议:

1. 自定义首部

设计一个简单的应用层首部添加在UDP数据报的数据部分之前,用于描述文件相关信息,首部中可以包含以下关键字段:

  • 文件标识字段:用于区分不同的文件,比如可以用一个唯一的编号或者文件名的哈希值等,方便接收端识别是哪个文件的数据包。
  • 数据包序号字段:由于文件会被分割成多个UDP数据包来发送,需要给每个数据包标记一个序号,便于接收端按照正确顺序重组文件,序号可以从0开始依次递增。
  • 总数据包数量字段:告知接收端整个文件被分割成了多少个UDP数据包,接收端可以据此判断是否已经接收完所有数据包,以便进行后续的文件组装操作。
  • 数据长度字段:说明当前UDP数据包中有效数据(即属于文件内容的部分)的长度,方便接收端准确提取。

例如,首部可以定义为固定长度的结构体(假设采用C语言风格描述,以下仅为示例示意):

typedef struct {
    unsigned int file_id;  // 文件标识,假设用整数表示
    unsigned int packet_seq;  // 数据包序号
    unsigned int total_packets;  // 总数据包数量
    unsigned int data_length;  // 当前数据包中数据长度
} FilePacketHeader;

2. 数据包拆分与发送

按照定义好的协议,将文件内容进行拆分,填充到各个UDP数据包的数据部分中:

  • 读取文件:以合适的方式(如二进制模式等)打开文件,根据最大有效UDP数据量限制(考虑到要预留首部空间等)来确定每次从文件中读取多少字节的数据作为一个UDP数据包的数据内容。
  • 构建数据包:先填充自定义首部的各个字段,然后将读取的文件内容紧跟在首部后面放置到UDP数据包的数据区,之后通过UDP套接字发送出去。例如在Python中(使用 socket 模块)的简单示意代码如下:
import socket

# 假设已经定义好FilePacketHeader结构体对应的Python类等相关逻辑

def send_file_udp(file_path, destination_ip, destination_port):
    sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    file_size = os.path.getsize(file_path)
    max_data_size = 1472  # 这里假设基于以太网类似的场景,可根据实际调整
    total_packets = file_size // max_data_size + (1 if file_size % max_data_size > 0 else 0)
    file_id = hash(file_path)  # 简单用文件路径哈希作为标识,可优化
    packet_seq = 0
    with open(file_path, 'rb') as file:
        while True:
            data = file.read(max_data_size)
            if not data:
                break
            header = FilePacketHeader(file_id, packet_seq, total_packets, len(data))
            packet_data = header.pack() + data  # 假设header有pack方法将结构体转为字节流
            sock.sendto(packet_data, (destination_ip, destination_port))
            packet_seq += 1
    sock.close()

3. 接收端处理

接收端收到UDP数据包后,按照定义的协议进行解析和文件组装:

  • 解析首部:从收到的UDP数据包数据部分开头提取出自定义首部,根据首部中的字段来获取文件标识、数据包序号、总数据包数量以及数据长度等关键信息。
  • 数据提取与组装:根据数据长度字段,提取出属于文件内容的部分,按照数据包序号将其依次存储到合适的缓冲区或者临时文件中,等接收到所有数据包(通过判断当前序号是否达到总数据包数量来确定)后,再将这些数据片段合并成完整的文件。以下是Python接收端的简单示意代码:
import socket

def receive_file_udp(local_port):
    sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    sock.bind(('0.0.0.0', local_port))
    received_packets = {}
    while True:
        data, addr = sock.recvfrom(65535)  # 足够大的缓冲区接收UDP数据包
        header = FilePacketHeader.unpack(data[:len(FilePacketHeader)])  # 假设能解包首部
        file_id = header.file_id
        packet_seq = header.packet_seq
        data_length = header.data_length
        file_data = data[len(FilePacketHeader):len(FilePacketHeader)+data_length]
        if file_id not in received_packets:
            received_packets[file_id] = [None] * header.total_packets
        received_packets[file_id][packet_seq] = file_data
        if all(packet is not None for packet in received_packets[file_id]):
            # 所有数据包已接收,组装文件
            file_content = b''.join(received_packets[file_id])
            # 这里可根据file_id等信息将文件内容写入对应文件,略去具体文件写入代码
            del received_packets[file_id]
    sock.close()

通过这样自定义数据包协议的方式,就能利用UDP来可靠地传输超过其常规最大有效数据量的文件内容了,不过要注意处理好可能出现的丢包、乱序等情况,必要时可以添加一些简单的差错控制和重传机制来提高传输的可靠性。

=对应的 C语言实现如下=============

以下是使用C语言实现基于UDP传输超过其常规最大有效数据量的文件的示例代码,包含了自定义数据包协议以及发送端和接收端的处理逻辑。

自定义首部结构体定义

首先定义一个结构体用于表示数据包的首部,包含了如文件标识、数据包序号、总数据包数量以及当前数据包中数据长度等关键信息。

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

// 自定义数据包首部结构体
typedef struct {
    unsigned int file_id;  // 文件标识,这里简单用整数表示,可根据实际优化
    unsigned int packet_seq;  // 数据包序号
    unsigned int total_packets;  // 总数据包数量
    unsigned int data_length;  // 当前数据包中数据长度
} FilePacketHeader;

发送端代码实现

发送端的主要功能是打开要发送的文件,按照定义好的协议将文件拆分成多个UDP数据包进行发送。

// 发送文件的函数
void send_file_udp(const char* file_path, const char* destination_ip, int destination_port) {
    int sockfd;
    struct sockaddr_in server_addr;

    // 创建UDP套接字
    if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) < 0) {
        perror("Socket creation failed");
        exit(1);
    }

    // 配置服务器地址结构体
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(destination_port);
    server_addr.sin_addr.s_addr = inet_addr(destination_ip);

    // 获取文件大小
    FILE* file = fopen(file_path, "rb");
    if (file == NULL) {
        perror("File open failed");
        close(sockfd);
        exit(1);
    }
    fseek(file, 0, SEEK_END);
    long int file_size = ftell(file);
    fseek(file, 0, SEEK_SET);

    // 假设基于以太网类似场景,确定每次可发送的最大数据量(需预留首部空间)
    const int max_data_size = 1472 - sizeof(FilePacketHeader);
    int total_packets = file_size / max_data_size + (file_size % max_data_size > 0? 1 : 0);
    unsigned int file_id = (unsigned int)rand();  // 简单生成文件标识,可改进

    // 发送文件内容
    FilePacketHeader header;
    char buffer[1472];  // 足够容纳一个UDP数据包(含首部和数据)
    int packet_seq = 0;
    while (!feof(file)) {
        // 填充首部信息
        header.file_id = file_id;
        header.packet_seq = packet_seq;
        header.total_packets = total_packets;
        int read_size = fread(buffer + sizeof(FilePacketHeader), 1, max_data_size, file);
        header.data_length = read_size;

        // 将首部信息复制到缓冲区开头
        memcpy(buffer, &header, sizeof(FilePacketHeader));

        // 发送UDP数据包
        if (sendto(sockfd, buffer, sizeof(FilePacketHeader) + read_size, 0, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {
            perror("Sendto failed");
            fclose(file);
            close(sockfd);
            exit(1);
        }

        packet_seq++;
    }

    fclose(file);
    close(sockfd);
}

接收端代码实现

接收端负责接收UDP数据包,按照协议解析首部并提取文件内容,然后将接收到的各个数据包按顺序组装成完整的文件。

// 接收文件的函数
void receive_file_udp(int local_port) {
    int sockfd;
    struct sockaddr_in server_addr, client_addr;
    socklen_t client_addr_len = sizeof(client_addr);

    // 创建UDP套接字并绑定端口
    if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) < 0) {
        perror("Socket creation failed");
        exit(1);
    }

    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(local_port);
    server_addr.sin_addr.s_addr = INADDR_ANY;

    if (bind(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {
        perror("Bind failed");
        close(sockfd);
        exit(1);
    }

    // 用于存储接收到的数据包,以文件标识为索引
    struct PacketBuffer {
        char** packets;
        int total_packets;
    };
    struct PacketBuffer* received_packets = NULL;
    int max_buffers = 10;  // 可同时处理的最大文件数量,可调整
    received_packets = (struct PacketBuffer*)malloc(max_buffers * sizeof(struct PacketBuffer));
    if (received_packets == NULL) {
        perror("Memory allocation failed");
        close(sockfd);
        exit(1);
    }
    for (int i = 0; i < max_buffers; i++) {
        received_packets[i].packets = NULL;
        received_packets[i].total_packets = 0;
    }

    // 接收并处理数据包
    while (1) {
        char buffer[1472];
        int n = recvfrom(sockfd, buffer, sizeof(buffer), 0, (struct sockaddr *)&client_addr, &client_addr_len);
        if (n < 0) {
            perror("Recvfrom failed");
            continue;
        }

        FilePacketHeader header;
        memcpy(&header, buffer, sizeof(FilePacketHeader));
        unsigned int file_id = header.file_id;

        // 检查是否需要为新文件分配内存空间
        if (received_packets[file_id % max_buffers].packets == NULL) {
            received_packets[file_id % max_buffers].packets = (char**)malloc(header.total_packets * sizeof(char*));
            if (received_packets[file_id % max_buffers].packets == NULL) {
                perror("Memory allocation failed");
                continue;
            }
            received_packets[file_id % max_buffers].total_packets = header.total_packets;
            for (int i = 0; i < header.total_packets; i++) {
                received_packets[file_id % max_buffers].packets[i] = NULL;
            }
        }

        // 提取文件数据并存储到对应位置
        char* data = (char*)malloc(header.data_length);
        if (data == NULL) {
            perror("Memory allocation failed");
            continue;
        }
        memcpy(data, buffer + sizeof(FilePacketHeader), header.data_length);
        received_packets[file_id % max_buffers].packets[header.packet_seq] = data;

        // 检查是否已接收完所有数据包,若是则组装文件
        bool all_received = true;
        for (int i = 0; i < received_packets[file_id % max_buffers].total_packets; i++) {
            if (received_packets[file_id % max_buffers].packets[i] == NULL) {
                all_received = false;
                break;
            }
        }
        if (all_received) {
            // 这里可根据file_id等信息将文件内容写入对应文件,暂略具体文件写入逻辑
            // 释放内存空间
            for (int i = 0; i < received_packets[file_id % max_buffers].total_packets; i++) {
                free(received_packets[file_id % max_buffers].packets[i]);
            }
            free(received_packets[file_id % max_buffers].packets);
            received_packets[file_id % max_buffers].packets = NULL;
            received_packets[file_id % max_buffers].total_packets = 0;
        }
    }

    // 释放接收端存储数据包的内存空间
    for (int i = 0; i < max_buffers; i++) {
        if (received_packets[i].packets!= NULL) {
            for (int j = 0; j < received_packets[i].total_packets; j++) {
                free(received_packets[i].packets[j]);
            }
            free(received_packets[i].packets);
        }
    }
    free(received_packets);
    close(sockfd);
}

你可以使用以下方式调用上述函数:

int main() {
    // 发送端示例调用
    send_file_udp("test.txt", "127.0.0.1", 8888);

    // 接收端示例调用
    receive_file_udp(8888);

    return 0;
}

上述代码只是一个基础示例,在实际应用中还可以进一步完善,比如添加差错控制机制(如校验和来验证数据包完整性、重传机制应对丢包情况等)、优化内存管理、更好地处理文件标识冲突以及考虑更多网络环境下的兼容性等问题。

请注意,代码中的一些逻辑(如文件写入部分在接收端只是简单提及了思路,并未完整实现等)可以根据实际需求进一步细化和完善,以适应具体的使用场景。

如何实现文件内容传输过程不丢失==

要实现基于UDP协议传输文件内容过程中尽量不丢失,可以采用以下几种方式来增强可靠性:

1. 添加校验和机制

在自定义的UDP数据包首部或者数据部分末尾添加校验和字段,发送端在构建数据包时计算文件数据部分(甚至可以包含首部等全部数据包内容)的校验和,常见的校验和算法有CRC(循环冗余校验)、MD5、SHA-1等(不过MD5、SHA-1更多用于数据完整性验证和安全领域,CRC相对更轻便用于简单错误检测)。

例如,采用简单的CRC16校验算法(以下为示例代码片段,仅示意如何在发送端和接收端添加校验和计算及验证逻辑,实际应用中可根据需求选用更合适的算法和优化代码实现):

发送端

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

// 假设之前已定义好FilePacketHeader结构体等相关代码

// CRC16校验算法函数,这里简单示意,可使用更优化的实现
unsigned short crc16(const char* data, int length) {
    unsigned short crc = 0xFFFF;
    for (int i = 0; i < length; i++) {
        crc ^= (unsigned short)data[i] << 8;
        for (int j = 0; j < 8; j++) {
            if (crc & 0x8000) {
                crc = (crc << 1) ^ 0x1021;
            } else {
                crc <<= 1;
            }
        }
    }
    return crc;
}

// 发送文件的函数,添加校验和计算逻辑
void send_file_udp(const char* file_path, const char* destination_ip, int destination_port) {
    // 前面创建套接字、配置地址等代码省略,和之前类似

    // 获取文件大小等操作省略,和之前类似

    FilePacketHeader header;
    char buffer[1472];
    int packet_seq = 0;
    while (!feof(file)) {
        // 填充首部信息等操作省略,和之前类似

        // 计算数据部分(含首部)的CRC16校验和
        unsigned short checksum = crc16(buffer, sizeof(FilePacketHeader) + header.data_length);
        // 将校验和添加到数据包合适位置,假设在首部后面紧跟添加,可根据实际协议调整
        *((unsigned short *)(buffer + sizeof(FilePacketHeader))) = checksum;

        // 发送UDP数据包操作省略,和之前类似

        packet_seq++;
    }

    fclose(file);
    close(sockfd);
}

接收端

// 接收文件的函数,添加校验和验证逻辑
void receive_file_udp(int local_port) {
    // 创建套接字、绑定端口等操作省略,和之前类似

    // 接收和处理数据包相关逻辑省略,和之前类似

    while (1) {
        char buffer[1472];
        int n = recvfrom(sockfd, buffer, sizeof(buffer), 0, (struct sockaddr *)&client_addr, &client_addr_len);
        if (n < 0) {
            perror("Recvfrom failed");
            continue;
        }

        FilePacketHeader header;
        memcpy(&header, buffer, sizeof(FilePacketHeader));
        // 提取接收到的校验和
        unsigned short received_checksum = *((unsigned short *)(buffer + sizeof(FilePacketHeader)));

        // 重新计算校验和
        unsigned short calculated_checksum = crc16(buffer, sizeof(FilePacketHeader) + header.data_length);

        // 比较校验和,若不一致则认为数据包出错,可请求重传(后续完善重传机制)
        if (received_checksum!= calculated_checksum) {
            // 这里可记录错误日志等,暂略
            continue;
        }

        // 后续正常的数据包处理、文件组装等逻辑省略,和之前类似

    }

    // 释放内存等操作省略,和之前类似
}

2. 引入确认与重传机制

  • 发送端
    • 为每个发送出去的UDP数据包启动一个定时器(可以利用操作系统提供的定时器相关函数实现,比如 setitimer 等),等待接收端的确认消息。
    • 记录已发送但未收到确认的数据包信息,比如序号、发送时间等,方便后续进行重传等操作。
  • 接收端
    • 收到数据包后,解析首部判断数据包是否完整且顺序正确,如果是则向发送端发送一个确认消息(可以是一个简单的UDP数据包,里面包含确认的文件标识和对应的数据包序号等关键信息)。
    • 如果接收到的数据包有问题(如校验和错误、序号乱序等),则丢弃该数据包,不发送确认消息,等待发送端重传。
  • 发送端超时处理
    当定时器超时后,如果还未收到对应数据包的确认消息,就认定该数据包丢失,重新发送该数据包,并且重置定时器,继续等待确认。

以下是简单的代码示例框架,展示如何在上述文件传输代码基础上添加确认与重传机制(只是示意核心逻辑,实际要完善更多细节):

发送端

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

// 假设之前定义的结构体、函数等都已存在

// 定义结构体用于记录已发送未确认的数据包信息
typedef struct {
    FilePacketHeader header;
    char* data;
    struct timespec send_time;  // 记录发送时间,用于超时判断
} UnconfirmedPacket;

// 存储已发送未确认数据包的数组,可根据实际情况调整大小
UnconfirmedPacket unconfirmed_packets[100];
int unconfirmed_count = 0;

// 定时器超时处理函数
void handle_timeout(int signum) {
    time_t current_time = time(NULL);
    for (int i = 0; i < unconfirmed_count; i++) {
        struct timespec current_ts;
        clock_gettime(CLOCK_REALTIME, &current_ts);
        // 计算时间差,判断是否超时(这里简单比较秒数,可更精确计算)
        if (current_time - unconfirmed_packets[i].send_time.tv_sec > 5) {  // 超时时间设为5秒,可调整
            // 重传数据包
            if (sendto(sockfd, unconfirmed_packets[i].data, sizeof(FilePacketHeader) + unconfirmed_packets[i].header.data_length, 0, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {
                perror("Resend failed");
            } else {
                // 重置发送时间
                clock_gettime(CLOCK_REALTIME, &unconfirmed_packets[i].send_time);
            }
        }
    }
}

// 发送文件的函数,添加确认与重传机制
void send_file_udp(const char* file_path, const char* destination_ip, int destination_port) {
    // 创建套接字、配置地址等操作省略

    // 设置定时器信号处理
    struct sigaction sa;
    memset(&sa, 0, sizeof(sa));
    sa.sa_handler = handle_timeout;
    sigaction(SIGALRM, &sa, NULL);

    // 获取文件大小等操作省略

    FilePacketHeader header;
    char buffer[1472];
    int packet_seq = 0;
    while (!feof(file)) {
        // 填充首部、计算校验和等操作省略

        // 记录已发送数据包信息
        UnconfirmedPacket packet;
        packet.header = header;
        packet.data = strdup(buffer);  // 复制数据包内容,方便重传
        clock_gettime(CLOCK_REALTIME, &packet.send_time);
        unconfirmed_packets[unconfirmed_count++] = packet;

        // 发送UDP数据包操作省略

        // 设置定时器,这里每次发送后设置,可优化为批量设置等方式
        struct itimeralarm_timer;
        memset(&alarm_timer, 0, sizeof(alarm_timer));
        alarm_timer.it_value.tv_sec = 5;  // 超时时间,和前面一致
        setitimer(ITIMER_REAL, &alarm_timer, NULL);

        packet_seq++;
    }

    fclose(file);
    close(sockfd);
}

接收端

// 接收文件的函数,添加发送确认消息逻辑
void receive_file_udp(int local_port) {
    // 创建套接字、绑定端口等操作省略

    // 接收和处理数据包相关逻辑省略

    while (1) {
        char buffer[1472];
        int n = recvfrom(sockfd, buffer, sizeof(buffer), 0, (struct sockaddr *)&client_addr, &client_addr_len);
        if (n < 0) {
            perror("Recvfrom failed");
            continue;
        }

        FilePacketHeader header;
        memcpy(&header, buffer, sizeof(FilePacketHeader));

        // 验证校验和等操作省略

        // 数据包正常,发送确认消息
        if (header.packet_seq == expected_seq && checksum_valid) {  // 假设expected_seq记录期望收到的序号
            // 构建确认消息数据包,简单包含文件标识和数据包序号
            char ack_buffer[sizeof(FilePacketHeader)];
            FilePacketHeader ack_header;
            ack_header.file_id = header.file_id;
            ack_header.packet_seq = header.packet_seq;
            memcpy(ack_buffer, &ack_header, sizeof(FilePacketHeader));
            // 发送确认消息给发送端
            if (sendto(sockfd, ack_buffer, sizeof(ack_buffer), 0, (struct sockaddr *)&client_addr, sizeof(client_addr)) < 0) {
                perror("Send ack failed");
            }
            // 更新期望序号等后续处理逻辑省略
        }

    }

    // 释放内存等操作省略
}

3. 采用可靠UDP库

市面上有一些可靠UDP库,例如UDT(UDP-based Data Transfer)等,它们在UDP协议基础上封装了一套具备可靠传输、拥塞控制等功能的机制,开发者可以直接使用这些库来进行文件传输,减少自行实现复杂可靠性机制的工作量,按照库提供的接口进行文件读取、发送以及接收端的处理等操作即可。

通过上述这些方式的综合运用,可以在UDP协议传输文件时极大地提高文件内容传输的可靠性,减少丢失情况的发生,不过要根据实际应用场景和性能要求等合理选择和优化具体的实现方法。

posted @ 2025-01-02 17:08  MarsCactus  阅读(1044)  评论(0)    收藏  举报