TFTP协议全称叫简单文件传输协议,是基于UDP实现的,端口默认为69,设计TFTP的初衷是轻量、简单、无认证,非常适合小型设备、嵌入式、无盘系统传输文件用。
基础特性
- 传输层:UDP(无连接、不可靠),因此TFTP上层自行实行可靠机制
- 默认端口:服务端监听69,数据传输时使用随机临时端口
- 数据块大小:固定512字节/块(不含头部)
- 安全:五用户名、密码、权限校验
- 功能:仅支持文件上传、下载,不能进行目录浏览、删除、重命名
- 可靠保障:自身实现超时重传、ACK确认
五种报文类型
所有TFTP报文格式统一:2字节操作码+后续
| 操作码 | 报文名称 | 作用 |
|---|---|---|
| 1 | RRQ 读请求 | 客户端下载文件(从服务端取文件) |
| 2 | WRQ 写请求 | 客户端上传文件(向服务端发文件) |
| 3 | DATA 数据块 | 传输文件数据 |
| 4 | ACK 确认 | 应答 DATA / WRQ / RRQ |
| 5 | ERROR 错误 | 报错、终止传输 |
1.RRQ/WRQ报文(操作码1/2)
结构如图:

- 常用模式:octet(二进制模式,网络设备用)、netascii(文本)
- 交互流程:客户端发RRQ/WRQ->服务端回ACK或DATA
2.DATA数据报文(操作码3)
结构如图:

- 块编号:占用2字节,用来标识数据块顺序,确保接收方按序重组数据;范围是1~65535,如果块编号到达了65535后,下一个块编号从1重新开始,接收方通过块编号递增规律判断是否为新块或重复块
- 数据:固定长度512B,如果DATA数据长度小于512B,表示最后一块了,传输完成
3.ACK确认报文(操作码4)
结构如题:

- 对DATA块N应答:ACK块号=N
- 对WRQ写请求应答:ACK块号=0
4.ERROR错误报文(操作码5)
结构如图:

- 差错码(错误码)也占2字节
- 出现错误直接终止整个传输
TFTP交互流程
客户端发起请求:
客户端发起RRQ(操作码1)/WRQ(操作码2)(读写请求)+文件名+模式+null到服务端的69端口
服务端响应请求:
读请求:服务器发送DATA数据块(操作码3)+块编号1+数据 到客户端临时端口
写请求:服务器发送ACK(操作码4)+块编号0 确认接受请求
数据传输:
读请求:客户端收到DATA后发送ACK+块编号1 服务器继续发送下一块
写请求:客户端发送DATA块1,服务器回复ACK块1,重复直到传输结束
结束条件:
当DATA数据块小于512字节时,表示传输结束
错误处理:
任何一方出错时发送ERROR报文(操作码5),包含错误码和错误信息
流程图如下:

示例代码
服务端:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/select.h>
#include <sys/stat.h>
#include <sys/time.h>
#include <arpa/inet.h>
#include <fcntl.h>
#include <libgen.h>
#define PORT 3456//服务器监听端口
#define SERVER_IP "127.0.0.1"
#define BUF_SIZE 512//TFTP块的大小
#define BASE_DIR "/home/hqyj/Linux学习文件/li-yihui/tftp_files" //文件存储根目录
typedef enum{
OP_RRQ = 1,//读请求
OP_WRQ = 2,//写请求
OP_DATA = 3,//数据块
OP_ACK = 4,//确认包
OP_ERROR =5//错误包
}tftp_opcode;
//目录创建函数状态
void ensure_dir_exists(const char *path)
{
//用于存储文件/目录
struct stat st = {0};
if(stat(path,&st) == -1) {
//0755 -- rexr-xr-x
//所有者可读可写可执行,其他用户可读可执行
if(mkdir(path,0755) == -1) {
perror("创建目录失败");
} else {
printf("创建目录:%s\n",path);
}
}
}
void send_ack(int socket,struct sockaddr_in *addr,uint16_t block)
{
uint8_t ack[4] = {0,OP_ACK,block >> 8,block & 0xFF};//构建ACK包
//包头结构 : 2字节操作码(0x0004)+2字节块号(大端序)
sendto(socket,ack,4,0,(struct sockaddr*)addr,sizeof(*addr));//发送UDP包
}
//下载函数
void handle_download(int socket,struct sockaddr_in *client_addr,char *filename)
{
printf("尝试下载文件(%s)\n",filename);
//提纯文件名,过滤路径,防止../遍历
char *fname_copy = strdup(filename);//复制文件名,basename会修改原字符串
char *pure_filename = basename(fname_copy);//提纯文件名("/a/b.txt" -> "b.txt")
//构建存储路径(BASE_DIR + 纯文件名)
char fullpath[256];
snprintf(fullpath,sizeof(fullpath),"%s/%s",BASE_DIR,pure_filename);//防止缓冲区溢出
free(fname_copy);//释放临时内存
printf("服务器文件路径:%s\n",fullpath);
int fd = open(fullpath,O_RDONLY);//只读模式打开文件
if(fd < 0) {
perror("文件打开失败");
//构建错误包:操作码5+错误码1(文件未找到)+消息
uint8_t err[5+sizeof("File not found")] = {0,OP_ERROR,1};
strcpy((char*)err + 4,"File not found");//错误信息是从第五个字节开始的
sendto(socket,err,sizeof(err),0,(struct sockaddr*)client_addr,sizeof(client_addr));//发送错误包
return ;
}
uint8_t buf[BUF_SIZE + 4];//缓冲区:4个包头字节 + 512字节数据
uint16_t block = 1;//块号从1开始
ssize_t bytes_read;//实际读取字节数
while((bytes_read = read(fd,buf + 4,BUF_SIZE)) > 0)
{//读取文件数据
//构建DATA包头
buf[0] = 0;//操作码高位(OP_DATA = 3,0x0003
buf[1] = OP_DATA;//操作码高位
buf[2] = block >> 8;//块号高位(大端序)
buf[3] = block & 0xFF;//块号低位
printf("发送块:%d,大小:%zd 字节\n",block,bytes_read);
//发送数据块
sendto(socket,buf,bytes_read + 4,0,(struct sockaddr*)client_addr,sizeof(*client_addr));
//设置5秒超时等待ACK
fd_set read_fds;
FD_ZERO(&read_fds);//清空文件描述符集合
FD_SET(socket,&read_fds);//添加socket到集合中
struct timeval timeout = {5,0};//设置5秒超时
//等待可读时间
int ret = select(socket + 1,&read_fds,NULL,NULL,&timeout);
if(ret <= 0) //超时(ret=0) 或者 错误(ret=-1)
{
printf("等待ACK超时,重发包%d\n",block);
continue;
}
//接收ACK包 4字节
ssize_t ack_size = recvfrom(socket,buf,4,0,NULL,NULL);
//验证ACK有效性:操作码是否为OP_ACK,块号是否匹配
if(ack_size < 4 || buf[1] != OP_ACK || ntohs(*(uint16_t*)(buf+2)) != block) {
printf("无效ACK,重发包%d\n",block);
continue;
}
printf("收到块%d的ACK\n",block);
block++;
}
//发送结束包(空数据块,4字节包头,块号为最后一块+1)
if(bytes_read == 0) {
//文件读取完毕
buf[0] = 0;
buf[1] = OP_DATA;
buf[2] = block >> 8;
buf[3] = block & 0xFF;
sendto(socket,buf,4,0,(struct sockaddr*)client_addr,sizeof(*client_addr));
printf("发送结束包\n");
//等待客户端回应结束包的ACK
recvfrom(socket,buf,4,0,NULL,NULL);
}
close(fd);
printf("文件发送完成\n");
}
//上传处理函数
void handle_upload(int socket,struct sockaddr_in *client_addr,char *filename)
{
printf("尝试上传文件(%s)\n",filename);
//提纯文件名,过滤路径,防止../遍历
char *fname_copy = strdup(filename);//复制文件名,basename会修改原字符串
char *pure_filename = basename(fname_copy);//提纯文件名("/a/b.txt" -> "b.txt")
//构建存储路径(BASE_DIR + 纯文件名)
char fullpath[256];
snprintf(fullpath,sizeof(fullpath),"%s/%s",BASE_DIR,pure_filename);//防止缓冲区溢出
free(fname_copy);//释放临时内存
//创建父目录 (a/b/c.txt -> 创建a/b)
char *dir = strdup(fullpath);
ensure_dir_exists(dirname(dir));//dirname获取目录的部分
free(dir);
//创建文件
int fd = open(fullpath,O_WRONLY | O_TRUNC | O_CREAT,0644);
if(fd < 0) {
perror("创建文件失败");
return ;
}
uint8_t buf[BUF_SIZE + 4];//接收数据块的缓冲区
uint16_t block = 1;
send_ack(socket,client_addr,0);//响应WRQ,发送初始ACK(块号0,表示准备好接收)
while(1){
socklen_t len = sizeof(*client_addr);
//接收客户端发送数据包
ssize_t nbytes = recvfrom(socket,buf,sizeof(buf),0,(struct sockaddr*)client_addr,&len);
if(nbytes < 4) //数据包过小
{
printf("接收数据包失败!\n");
break;
}
//处理数据块
if(buf[1] == OP_DATA && ntohs(*(uint16_t*)(buf+2)) == block) {
ssize_t data_size = nbytes - 4;//数据部分长度 = 总长度 - 包头长度
printf("收到块%d,大小 %zd 字节\n",block,data_size);
//写入文件
write(fd,buf+4,data_size);
send_ack(socket,client_addr,block);
//如果数据块不足512字节,则视为最后一块
if(data_size < BUF_SIZE) {
printf("收到最后一块上传完成\n");
break;
}
block++;
} else if(buf[1] == OP_ERROR) {
//处理客户端的错误包
printf("错误:%s\n",buf+4);
break;
} else {
printf("收到意外包:操作码:%d,块号%d\n",buf[1],ntohs(*(uint16_t*)(buf+2)));
}
}
close(fd);
}
int main()
{
//确保文件根目录存在
printf("基础存储目录:%s\n",BASE_DIR);
//调用目录创建函数
ensure_dir_exists(BASE_DIR);
//创建UDPsocket
int sockfd = socket(AF_INET,SOCK_DGRAM,0);
struct sockaddr_in server_addr;
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(PORT);
server_addr.sin_addr.s_addr = inet_addr(SERVER_IP);
//绑定
int ret_bind = bind(sockfd,(struct sockaddr*)&server_addr,sizeof(server_addr));
if(ret_bind == -1) {
perror("绑定失败");
close(sockfd);
return -1;
}
printf("TFTP服务器运行中...(服务器ip:%s端口号:%d)\n",SERVER_IP,PORT);
printf("等待客户端连接...\n");
//开启主循环,持续等待处理客户端请求
while(1) {
//接收缓冲区(最大的数据包512字节数据加上4字节包头)
uint8_t buf[516];
//客户端地址结构体
struct sockaddr_in client_addr;
socklen_t len = sizeof(client_addr);
//接收客户端请求
ssize_t nbytes = recvfrom(sockfd,buf,sizeof(buf),0,(struct sockaddr*)&client_addr,&len);
if(nbytes > 2) {//返回结束至少包含了操作码(2字节)+文件名(1字节)
char *filename = (char *)(buf+2);//文件名从第三个字节开始
char *mode = filename + strlen(filename) + 1;//固定模式字段("octet")
char client_ip[INET_ADDRSTRLEN];//存储客户端ip的字符串
//将客户端ip转为字符串
inet_ntop(AF_INET,&client_addr.sin_addr,client_ip,INET_ADDRSTRLEN);
printf("收到了来自客户端[%s:%d]的请求\n",client_ip,ntohs(client_addr.sin_port));
//判断操作码,并且去调用对应的处理函数
if(buf[1] == OP_RRQ) //下载请求
{
printf("下载请求:%s\n",filename);
//调用下载处理函数
handle_download(sockfd,&client_addr,filename);
printf("下载完成\n");
} else if(buf[1] == OP_WRQ) //上传请求
{
printf("上传请求:%s\n",filename);
//调用上传处理函数
handle_upload(sockfd,&client_addr,filename);
printf("上传完成\n");
} else {
printf("其他操作码:%d\n",buf[1]);
}
}
}
close(sockfd);
return 0;
}
客户端:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/select.h>
#include <sys/stat.h>
#include <sys/time.h>
#include <arpa/inet.h>
#include <fcntl.h>
#include <libgen.h>
#define BUF_SIZE 512//TFTP块的大小
typedef enum{
OP_RRQ = 1,//读请求
OP_WRQ = 2,//写请求
OP_DATA = 3,//数据块
OP_ACK = 4,//确认包
OP_ERROR =5//错误包
}tftp_opcode;
//发送请求函数
void send_request(int socket,struct sockaddr_in *addr,char *filename,int is_write)
{
//初始化缓冲区
uint8_t buf[516] = {0};
buf[1] = is_write ? OP_WRQ :OP_RRQ;//设置操作码
//is_write:1表示上传WRQ;0表示下载RRQ
//写入文件名,从第三个字节开始,第1,2字节为操作码
strcpy((char*)(buf+2),filename);
//写入模式,octet表示二进制传输,从文件名后+1字节开始
strcpy((char*)(buf+2+strlen(filename)+1),"octet");
//计算数据包长度:操作码(2)+文件名(len) + 0 + 模式(len) + 0
//数据包格式:
//[0x00][0x01/0x02][filename][0x00][octet][0x00]
int len = 2+strlen(filename)+1+strlen("octet") + 1;
sendto(socket,buf,len,0,(struct sockaddr*)addr,sizeof(*addr));//发送请求包
}
//文件下载函数
void download_file(int socket,struct sockaddr_in *addr,char *remote_filename,char *local_path)
{
struct stat st;//用于存储本地路径状态
//检查本地路径是否为目录
if(stat(local_path,&st) == 0 && S_ISDIR(st.st_mode)) {
//若是目录,使用远程文件名作为本地文件名
char *filename = basename(remote_filename);
char fullpath[256];
snprintf(fullpath,sizeof(fullpath),"%s/%s",local_path,filename);
local_path = fullpath;//更新为完整路径
}
printf("将文件保存到:%s\n",local_path);
//创建或者打开本地文件
int fd = open(local_path,O_WRONLY|O_TRUNC|O_CREAT,0644);
if(fd<0) {
perror("文件打开失败");
return ;
}
uint8_t buf[BUF_SIZE + 4];//接受数据块的缓冲区大小
uint16_t block = 1;//接受的快号从1开始
while(1) {
//服务器地址长度
socklen_t len = sizeof(*addr);
//接收服务器发送的数据块(包含服务器地址
ssize_t nbtyes = recvfrom(socket,buf,sizeof(buf),0,(struct sockaddr*)addr,&len);
//如果数据包过小直接终止
if(nbtyes < 4) {
break;
}
//处理数据块(OP_DATA)
if(buf[1] == OP_DATA && ntohs(*(uint16_t*)(buf+2)) == block) {
write(fd,buf+4,nbtyes-4);//写入数据跳过4个字节的包头
//构建ACK包(直接使用接收到的块号字节)
uint8_t ack[4] = {0,OP_ACK,buf[2],buf[3]};
sendto(socket,ack,4,0,(struct sockaddr*)addr,sizeof(*addr));
//如果数据块不足516,视为最后一块(nbtyes<516,数据部分为nbytes-4 < 512)
if(nbtyes < BUF_SIZE+4) {
break;
}
block++;
} else if(buf[1] == OP_ERROR) {
//处理错误包
printf("错误:%s\n",buf+4);
break;
}
}
//关闭文件
close(fd);
}
//文件上传函数
void upload_file(int socket,struct sockaddr_in *addr,char *local_path,char *remote_filename)
{
//以只读方式打开文件
int fd = open(local_path,O_RDONLY);
if(fd < 0) {
perror("文件打开失败");
return ;
}
//发送数据块缓冲区
uint8_t buf[BUF_SIZE + 4];
uint16_t block = 1;
ssize_t bytes_read;
//等待服务器响应WRQ的初始化ACK,块号0
recvfrom(socket,buf,4,0,NULL,NULL);//阻塞等待ACK0
while((bytes_read = read(fd,buf+4,BUF_SIZE)) > 0) {
//构建DATA包头
buf[0] = 0;
buf[1] = OP_DATA;
buf[2] = block >> 8;
buf[3] = block && 0xFF;
//发送数据块
sendto(socket,buf,bytes_read+4,0,(struct sockaddr*)addr,sizeof(*addr));
//等待服务器响应
recvfrom(socket,buf,4,0,NULL,NULL);
block++;
}
close(fd);
}
//文件传输逻辑
/*下载
1.客户端:发送RRQ(操作码1+文件名+octet)到服务器中
2.服务器:解析RRQ,检查文件存在性
存在:分块发送DATA包(块号从1开始),每发送一块等待ACK回应
不存在:返回ERROR包(操作码5+错误码1)
3.客户端:接收DATA包,验证块号后写入文件,返回对应ACK
4.服务器:发送空DATA包(4字节)表示结束,客户端发送最后一个ACK
*/
/*上传
1.客户端:发送WRQ
2.服务器:解析WRQ,创建文件并返回ACK0
3.客户端:接收ACK0,分块发送DATA包(块号从1开始),每发送一块等待ACK
4.服务器:接收DATA包,验证块号写入文件,然后ACK
5.客户端:发送完所有数据(最后一块 < 512字节),服务器接收后结束
*/
int main(int argc,const char *argv[])
{
//检查参数个数
if(argc != 6) {
//6个参数:程序名 + 5个参数
printf("参数个数错误!");
printf("usage:%s <get|put> <本地路径> <远程文件名> <服务端ip地址> <端口号>\n",argv[0]);
printf("示例用法:\n 下载:%s get /local/path remote.txt 192.168.179.100 69\n",argv[0]);
return -1;
}
//创建UDPsocket
int sockfd = socket(AF_INET,SOCK_DGRAM,0);
struct sockaddr_in server_addr;
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(atoi(argv[5]));
server_addr.sin_addr.s_addr = inet_addr(argv[4]);
//打印操作信息
printf("操作:%s\n",argv[1]);
printf("本地路径:%s\n",argv[2]);
printf("远程文件:%s\n",argv[3]);
//根据操作类型执行上传或者下载
if(strcmp(argv[1],"get") == 0) {
//下载(RRQ)
send_request(sockfd,&server_addr,argv[3],0);
//接收数据
download_file(sockfd,&server_addr,argv[3],argv[2]);
} else if(strcmp(argv[1],"put") == 0) {
//上传
//发送写请求
send_request(sockfd,&server_addr,argv[3],1);
//发送数据
upload_file(sockfd,&server_addr,argv[2],argv[3]);
} else {
printf("无效命令\n");
}
close(sockfd);
return 0;
}