2025.8.30项目二基于UDP的TFTP文件传输

发布于:2025-08-31 ⋅ 阅读:(22) ⋅ 点赞:(0)

        今天我们来做一下这个项目,关于一个简单文件传输协议,适用于在网络上进行文件传输的一套标准协议,使用UDP传输,与tftp32服务器互传文件,

TFTP通信过程总结
  1. 服务器在69号端口等待客户端的请求
  2. 服务器若批准此请求,则使用 临时端口 与客户端进行通信。
  3. 每个数据包的编号都有变化(从1开始)
  4. 每个数据包都要得到ACK的确认,如果出现超时,则需要重新发送最后的数据包或ACK包
  5. 数据长度以512Byte传输的,小于512Byte的数据意味着数据传输结束。
3)tftp协议分析
差错码
0 未定义,差错错误信息
1 File not found.
2 Access violation.
3 Disk full or allocation exceeded.
4 illegal TFTP operation.
5 Unknown transfer ID.
6 File already exists.
7 No such user.
8 Unsupported option(s) requested.

        上传文件的流程

        下载流程

        标准数据包格式
        所有传输的数据包严格遵循 TFTP 格式:
请求包(RRQ/WRQ):0x00 + 操作码(1/2) + 文件名 + 0x00 + 传输模式("octet") + 0x00;
数据包(DATA):0x00 + 操作码(3) + 块编号(2字节) + 数据(≤512字节);
确认包(ACK):0x00 + 操作码(4) + 块编号(2字节);
错误包(ERROR):0x00 + 操作码(5) + 错误码(2字节) + 错误信息 + 0x00;

        依旧老规矩,将我的代码奉上

#include <myhead.h>

// 常量与TFTP协议操作码定义
#define IP "192.168.0.70"    // TFTP服务器IP
#define PORT 69              // TFTP默认端口(接收初始请求)
#define N 516                // 最大数据包大小(2字节操作码+2字节块号+512字节数据)
// TFTP操作码(标识包类型,协议规定)
#define OP_RRQ   1           // 读请求(下载)
#define OP_WRQ   2           // 写请求(上传)
#define OP_DATA  3           // 数据块包
#define OP_ACK   4           // 确认包
#define OP_ERROR 5           // 错误包


// 上传文件:客户端→服务器
// 参数:oldfd(UDP套接字)、server(服务器地址)
int do_upload(int oldfd, struct sockaddr_in server)
{
    char filename[20]; 
    printf("请输入上传文件名: ");
    fgets(filename, 20, stdin);
    filename[strlen(filename)-1] = 0;  // 去除换行符

    // 检查本地文件是否存在(只读打开)
    int fd = open(filename, O_RDONLY);
    if(fd == -1) { perror("open"); printf("打开文件失败!\n"); return -1; }
    
    // 构造并发送WRQ(写请求)包
    char buf[N];
    // WRQ格式:0x00+OP_WRQ+文件名+0x00+"octet"+0x00(octet=二进制传输)
    int size = sprintf(buf, "%c%c%s%c%s%c", 0, OP_WRQ, filename, 0, "octet", 0);
    if(sendto(oldfd, buf, size, 0, (struct sockaddr*)&server, sizeof(server)) < 0)
    { perror("sendto"); close(fd); return -1; }

    // 循环传数据:收ACK→发数据块
    int recv_len;
    unsigned short num = 1;  // 数据块编号(从1开始)
    socklen_t addrlen = sizeof(server);
    
    while(1)
    {
        bzero(buf, N);
        recv_len = recvfrom(oldfd, buf, N, 0, (struct sockaddr*)&server, &addrlen);
        if(recv_len == -1) { perror("recvfrom"); close(fd); return -1; }

        if(OP_ACK == buf[1])  // 处理服务器ACK
        {
            // 提取ACK块号(网络字节序转主机字节序)
            unsigned short ack_num = ntohs(*(unsigned short*)(buf+2));
            // 校验ACK:首次需ACK=0,后续需ACK=num-1
            if((num == 1 && ack_num == 0) || (ack_num == num - 1))
            {
                // 构造DATA包(0x00+OP_DATA+块号+数据)
                buf[0] = 0; buf[1] = OP_DATA;
                *(unsigned short*)(buf+2) = htons(num);  // 块号转网络字节序

                // 读本地文件数据(跳过4字节头部,最多读512字节)
                int res = read(fd, buf+4, N-4);
                if(res == -1) { perror("read"); close(fd); return -1; }
                if(res == 0) { printf("上传完毕\n"); break; }  // 数据读完,结束

                // 发送DATA包
                if(sendto(oldfd, buf, res+4, 0, (struct sockaddr*)&server, sizeof(server))==-1)
                { perror("sendto"); close(fd); return -1; }
                num++;  // 块号自增
            } 
            else { printf("编号不匹配: 期望%d, 实际%d\n", num-1, ack_num); break; }
        }
        else if(OP_ERROR == buf[1])  // 处理服务器错误
        { printf("错误: %s\n", buf+4); break; }
    }
    
    close(fd);
    return 0;
}


// 下载文件:服务器→客户端
// 参数:oldfd(UDP套接字)、server(服务器地址)
int do_download(int oldfd, struct sockaddr_in server)
{
    char filename[20];
    printf("请输入下载文件名: ");
    fgets(filename, 20, stdin);
    filename[strlen(filename)-1] = 0;  // 去除换行符

    // 构造并发送RRQ(读请求)包
    char buf[N];
    // RRQ格式:0x00+OP_RRQ+文件名+0x00+"octet"+0x00
    int size = sprintf(buf, "%c%c%s%c%s%c", 0, OP_RRQ, filename, 0, "octet", 0);
    if(sendto(oldfd, buf, size, 0, (struct sockaddr*)&server, sizeof(server))==-1)
    { perror("sendto"); return -1; }

    // 循环收数据:收DATA→写文件→发ACK
    int fd, flag = 0;  // flag=1表示文件已创建
    ssize_t recv_len;
    unsigned short num = 1;  // 期望接收的块号
    socklen_t addrlen = sizeof(server);
    
    while(1)
    {
        bzero(buf, N);
        recv_len = recvfrom(oldfd, buf, N, 0, (struct sockaddr*)&server, &addrlen);
        if(recv_len == -1) { perror("recvfrom"); return -1; }

        if(OP_DATA == buf[1])  // 处理服务器DATA包
        {
            if(!flag)  // 首次收数据,创建本地文件
            {
                fd = open(filename, O_WRONLY|O_CREAT|O_TRUNC, 0664);
                if(fd == -1) { perror("open"); return -1; }
                flag = 1;
            }

            // 提取DATA块号,校验是否连续
            unsigned short current_num = ntohs(*(unsigned short*)(buf+2));
            if(current_num == num)
            {
                // 写数据到本地文件(跳过4字节头部)
                if(write(fd, buf+4, recv_len-4)==-1) 
                { perror("write"); close(fd); return -1; }

                // 构造并发送ACK(确认已收当前块)
                buf[0] = 0; buf[1] = OP_ACK;
                if(sendto(oldfd, buf, 4, 0, (struct sockaddr*)&server, sizeof(server))==-1)
                { perror("sendto"); }

                // 数据<512字节(包长<516),表示传输结束
                if(recv_len < 516) { printf("下载完毕\n"); break; }
                num++;  // 块号自增
            }
        }
        else if(OP_ERROR == buf[1])  // 处理服务器错误
        { printf("错误: %s\n", buf+4); break; }
    }
    
    if(flag) close(fd);  // 关闭本地文件
    return 0;
}


// 主函数:初始化套接字+用户交互
int main(int argc, const char *argv[])
{
    // 创建UDP套接字
    int oldfd = socket(AF_INET, SOCK_DGRAM, 0);
    if(oldfd == -1) { perror("socket失败"); return -1; }

    // 初始化服务器地址
    struct sockaddr_in server = {
        .sin_family = AF_INET,
        .sin_port = htons(PORT),  // 端口转网络字节序
        .sin_addr.s_addr = inet_addr(IP)  // IP转网络字节序
    };

    // 用户交互菜单
    char choose;
    while(1)
    {
        printf("******************\n");
        printf("******1.下载******\n");
        printf("******2.上传******\n");
        printf("******3.退出******\n");
        printf("******************\n");
        printf("请选择你的操作:\n");
        choose = getchar();
        while(getchar()!='\n');  // 清空输入缓冲区

        switch(choose)
        {
            case '1': do_download(oldfd, server); break;
            case '2': do_upload(oldfd, server); break;
            case '3': goto END;  // 退出循环
            default: printf("输入错误\n");
        }
    }
    
END:
    close(oldfd);  // 关闭套接字
    return 0;
}


网站公告

今日签到

点亮在社区的每一天
去签到