TFTP协议

TFTP协议全称叫简单文件传输协议,是基于UDP实现的,端口默认为69,设计TFTP的初衷是轻量、简单、无认证,非常适合小型设备、嵌入式、无盘系统传输文件用。

基础特性

  1. 传输层:UDP(无连接、不可靠),因此TFTP上层自行实行可靠机制
  2. 默认端口:服务端监听69,数据传输时使用随机临时端口
  3. 数据块大小:固定512字节/块(不含头部)
  4. 安全:五用户名、密码、权限校验
  5. 功能:仅支持文件上传、下载,不能进行目录浏览、删除、重命名
  6. 可靠保障:自身实现超时重传、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;
}
相关推荐
优信电子1 小时前
STM32/C51驱动 DHTC11 温湿度传感器
stm32·单片机·嵌入式硬件·c51·温湿度传感器·dhtc11·环境测量
QiLinkOS2 小时前
【从实验室到商业战场:发明专利如何重塑科技与企业的共生生态】
大数据·c语言·数据结构·c++·人工智能·单片机·算法
周周记笔记2 小时前
【元器件专题】三极管-如果B极给一个方波信号,那么V0输出也可以设计为一个方波信号
单片机·嵌入式硬件
潜创微科技2 小时前
IT68353:DP 1.4 + HDMI 2.0 + USB-C 三合一转 HDMI 2.0 单芯片KVM切换方案
嵌入式硬件·音视频
HPT_Lt3 小时前
ZCC10012支持100V/1.2A 超低静态电流同步降压转换器 兼容LM5164
单片机·嵌入式硬件
Industio_触觉智能3 小时前
瑞芯微RK3576车载智能场景之ADAS+DMS+NVR
嵌入式硬件·dms·adas·nvr·rk3576·车载智能
2zcode3 小时前
基于STM32的多功能万年历电子闹钟设计与实现
stm32·单片机·嵌入式硬件
一抹晴空4 小时前
Keil MDK AC6 compiler编译报错,与AC5区别
c语言·arm开发·单片机
0南城逆流04 小时前
【STM32】RTT-Studio中HAL库开发教程十四:MSMART串口组件
stm32·单片机·嵌入式硬件