今天我们来做一下这个项目,关于一个简单文件传输协议,适用于在网络上进行文件传输的一套标准协议,使用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-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;
}