2025.8.30项目二基于UDP的TFTP文件传输
今天我们来做一下这个项目,关于一个简单文件传输协议,适用于在网络上进行文件传输的一套标准协议,使用UDP传输,与tftp32服务器互传文件,
TFTP通信过程总结
- 服务器在69号端口等待客户端的请求
- 服务器若批准此请求,则使用 临时端口 与客户端进行通信。
- 每个数据包的编号都有变化(从1开始)
- 每个数据包都要得到ACK的确认,如果出现超时,则需要重新发送最后的数据包或ACK包
- 数据长度以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-1if((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"+0x00int 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→写文件→发ACKint 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;
}