TFTP协议的简单介绍与使用socket实现TFTP下载客户端

本文介绍了TFTP协议的基本概念、工作流程和数据包结构,强调了其基于UDP的特性以及如何通过设置选项来保证传输可靠性。还展示了在Ubuntu环境下配置TFTP服务器的步骤,并给出了使用socket实现TFTP下载客户端的示例代码。
摘要由CSDN通过智能技术生成

参考:【千锋教育】

TFTP协议介绍

      TFTP(Trivial File Transfer Protocol,简单文件传输协议)是TCP/IP协议族中的一个用来在客户机与服务器之间进行简单文件传输的协议,提供不复杂、开销不大的文件传输服务。默认端口号为69。
      由于传输文件需要可靠的传输协议,但TFTP是基于UDP的,UDP是不可靠的传输协议,所以只能通过人为的手段来保证可靠性。


TFTP的客户端与服务端的通讯流程

在这里插入图片描述
      如上图所示(上图流程通过网络抓包分析得到),传输开始后,客户端发送文件操作请求给服务器端的开放端口69,服务器端若批准该请求将启用临时端口发送文件给客户端,客户端接收到每个数据包都会发送该数据包的ACK给服务器端,如果ACK传输过程中丢失,则服务器端会重发对应的数据包。每次传输的数据包中数据大小默认为512byte,最后一次的长度将 < 512byte,即发送小于512byte的数据意味着传输结束。


TFTP协议数据包结构

在这里插入图片描述
      上图(图片小的话,建议下载看大图)显示的是客户端与服务器端的操作的数据包结构,也标示了每部分所占的字节数。
      读写请求中文件传输模式 :octet(二进制模式);netascii(文本模式);
      上面我们说过服务端发送的文件数据包中数据大小限制在512 byte,从上图中可以看出操作符和编号共占4 byte,所以从完整的数据包大小可以看出当数据包小于516 byte时传输结束。而且每个数据包的编号都从1开始逐个递增
      读写请求数据包还可以附加选项,OACK是服务器端对附加选项的答复。下图中描述了大致的通讯流程。OACK的数据编号默认为0。
在这里插入图片描述

可选项含义
tsize读操作时,tsize为0,服务器会返回待读取文件的大小;写操作时,tsize为待写入文件的大小,服务器会回显该选项。
blksize修改传输文件时使用的数据块大小
timeout修改默认的数据传输超时时间,以便重发。

Ubuntu环境下配置TFTP服务器

      安装tftp服务器:sudo apt-get install tftpd-hpa
      安装tftp客户端:sudo apt-get install tftp-hpa(测试使用,最后我们会实现一个tftp下载客户端)
      配置tftp服务器:sudo vim /etc/default/tftpd-hpa

TFTP_USERNAME="tftp"
# 这个路径自行定义,服务器文件存储路径
TFTP_DIRECTORY="/home/hwlxhy/tftpboot"
#服务器地址和端口
TFTP_ADDRESS=":69"
TFTP_OPTIONS="--secure"

      保存退出后,将配置中的文件夹赋予权限:chmod -R 777 tftp文件夹
      重启:sudo service tftpd-hpa restart
      在上述目录下放置一些文件后使用tftp下载文件测试,下载的文件在运行该命令的终端的当前目录下。如果文件下载成功,说明TFTP服务器搭建成功。具体过程参考:点我


socket实现TFTP下载客户端

      TFTP基于UDP,所以其socket编码方式与UDP一致。UDPsocket编码流程与函数介绍参考:UDP的socket通讯
      下面代码需要从终端接受两个参数,例如:./a.out 127.0.0.1 info.txt

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include<sys/socket.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <fcntl.h>

int main(int argc,char *argv[])
{
    if (argc != 3)
    {
        printf("please enter IP and FilesName\n");
        return 0;
    }
    //tftp基于UDP,所以编程按照UDP的编程流程。
    int sockfd = socket(AF_INET,SOCK_DGRAM,0);
    if (sockfd < 0)
    {
        printf("socket create ERROR\n");
        return 0;
    }

    //给tftp服务器发送下载文件请求
    //tftp请求报文,组包
    unsigned char cmd[128];
    //包:1(操作符,读 = 1 ,占两个字节)文件名(n个字节)0  操作模式(octet)0
    //注意:strlen(cmd) = 0,因为0x00为0,该函数默认其为 '\0'
    //C数据类型中char占一个字节,故用char来接收一个字节的数据
    int len = sprintf(cmd,"%c%c%s%c%s%c",0x00,0x01,argv[2],0,"octet",0);
    struct sockaddr_in server;
    server.sin_family = AF_INET;
    server.sin_port = htons(69);
    inet_pton(AF_INET,argv[1],&server.sin_addr.s_addr);
    sendto(sockfd, cmd, len, 0, (struct sockaddr *)&server, sizeof(server));

    //打开本地文件,用于接受数据
    int fd = open(argv[2],O_WRONLY|O_CREAT,0666);
    if (fd < 0)
    {
        printf("file open ERROR\n");
        return 0;
    }
    unsigned short num = 0;
    //不停的读取服务器端接受的数据
    while (1)
    {
        //记住得无符号(0~255),直接char会越界(-128~128)
        unsigned char buf[1024] = "";
        //由于之后发送数据将启用临时端口,所以需要保存源地址
        struct sockaddr_in from;
        socklen_t from_len = sizeof(from_len);
        //接受数据
        int len = recvfrom(sockfd,buf,sizeof(buf),0,(struct sockaddr *)&from,&from_len);
        //判断接受的数据操作码,必须是00 03才可以接收文件数据,
        if (buf[1] = 0x03)
        {
                //写入文件,引入num以防止文件内写入重复的数据,即保证每个数据编号只下载一次。
                if ((num + 1) == ntohs(*(unsigned short *)(buf + 2)))
                {
                    write(fd,  buf + 4, len - 4);
                    num = ntohs(*(unsigned short *)(buf + 2));
                    printf("%d\n",num);
                }
                //接受一个数据包,就需要回应一个ACK
                /*
                unsigned char ack[4] = "";
                sprintf(ack,"%c%c%c%c",0x00,0x04,buf[2],buf[3]);
                */
               buf[1] = 4;
                sendto(sockfd,buf ,4 , 0, (struct sockaddr *)&from, sizeof(from));
                //最后一个数据包,break;
                if (len < 516)  break;
        }
    }
    close(sockfd);
    close(fd);
    return 0;
}
  • 2
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
TFTP协议是一种简单的文件传输协议,它可以用于在计算机之间传输文件。TFTP协议基于UDP协议,因此它不具备TCP协议的可靠传输保证。本文将介绍如何使用C语言实现TFTP协议客户端和服务端。 ## TFTP协议概述 TFTP协议是一种基于UDP协议的文件传输协议,它采用简单的请求/响应模式进行通信。TFTP协议主要有两种模式,分别是Netascii模式和Octet模式。 Netascii模式是一种ASCII码模式,它将传输的文件视为ASCII码字符流进行传输。在Netascii模式下,每个文件的换行符会被转换成Telnet的行结束符CR-LF(回车换行)。 Octet模式是一种二进制模式,它将传输的文件视为二进制数据流进行传输。在Octet模式下,每个文件的换行符会被保留。 TFTP协议定义了五种不同的请求/响应类型,分别是: - RRQ:读请求 - WRQ:写请求 - DATA:数据包 - ACK:确认包 - ERROR:错误包 ## TFTP客户端实现 TFTP客户端主要有两个功能,分别是向服务器请求文件和向服务器发送文件。下面是使用C语言实现TFTP客户端的代码示例。 ```c #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/socket.h> #include <netinet/in.h> #define SERVER_IP "127.0.0.1" #define SERVER_PORT 69 #define BUF_SIZE 512 void error(char *msg) { perror(msg); exit(1); } int main(int argc, char *argv[]) { int sockfd, n; struct sockaddr_in servaddr; char buf[BUF_SIZE]; if (argc < 3) { fprintf(stderr,"usage: %s <filename> <mode>\n", argv[0]); exit(1); } // 创建套接字 sockfd = socket(AF_INET, SOCK_DGRAM, 0); if (sockfd < 0) error("ERROR opening socket"); // 设置服务器地址 memset(&servaddr, 0, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_addr.s_addr = inet_addr(SERVER_IP); servaddr.sin_port = htons(SERVER_PORT); // 发送读请求 sprintf(buf, "%c%c%s%c%s%c", 0, 1, argv[1], 0, argv[2], 0); n = sendto(sockfd, buf, strlen(buf), 0, (struct sockaddr *)&servaddr, sizeof(servaddr)); if (n < 0) error("ERROR sending read request"); // 接收数据 FILE *fp = fopen(argv[1], "wb"); while (1) { // 接收数据包 struct sockaddr_in cliaddr; socklen_t len = sizeof(cliaddr); n = recvfrom(sockfd, buf, BUF_SIZE, 0, (struct sockaddr *)&cliaddr, &len); if (n < 0) error("ERROR receiving data"); // 解析数据包 unsigned short opcode = ntohs(*(unsigned short *)buf); unsigned short block = ntohs(*(unsigned short *)(buf + 2)); if (opcode == 3) { // 写入文件 fwrite(buf + 4, 1, n - 4, fp); // 发送确认包 sprintf(buf, "%c%c%c%c", 0, 4, buf[2], buf[3]); n = sendto(sockfd, buf, 4, 0, (struct sockaddr *)&cliaddr, len); if (n < 0) error("ERROR sending ACK"); if (n < BUF_SIZE - 4) break; } else if (opcode == 5) { // 接收到错误包 fprintf(stderr, "ERROR: %s\n", buf + 4); break; } else { // 接收到无法识别的数据包 fprintf(stderr, "ERROR: unrecognized packet\n"); break; } } fclose(fp); close(sockfd); return 0; } ``` 上面的代码实现TFTP客户端的读请求功能,它会向服务器发送一个RRQ请求,并接收服务器返回的数据包。如果接收到的数据包是一个数据包,则将数据写入指定的文件中,并发送一个ACK确认包。 ## TFTP服务端实现 TFTP服务端主要有两个功能,分别是接收客户端的文件请求和接收客户端发送的文件。下面是使用C语言实现TFTP服务端的代码示例。 ```c #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/socket.h> #include <netinet/in.h> #define SERVER_PORT 69 #define BUF_SIZE 512 void error(char *msg) { perror(msg); exit(1); } int main(int argc, char *argv[]) { int sockfd, n; struct sockaddr_in servaddr, cliaddr; socklen_t len; char buf[BUF_SIZE]; // 创建套接字 sockfd = socket(AF_INET, SOCK_DGRAM, 0); if (sockfd < 0) error("ERROR opening socket"); // 设置服务器地址 memset(&servaddr, 0, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_addr.s_addr = INADDR_ANY; servaddr.sin_port = htons(SERVER_PORT); // 绑定套接字 if (bind(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0) error("ERROR on binding"); while (1) { // 接收请求 len = sizeof(cliaddr); n = recvfrom(sockfd, buf, BUF_SIZE, 0, (struct sockaddr *)&cliaddr, &len); if (n < 0) error("ERROR receiving request"); // 解析请求 unsigned short opcode = ntohs(*(unsigned short *)buf); if (opcode != 1) { // 接收到非读请求 sprintf(buf, "%c%c%c%c%s%c", 0, 5, 0, 4, "Unsupported request", 0); n = sendto(sockfd, buf, strlen(buf), 0, (struct sockaddr *)&cliaddr, len); if (n < 0) error("ERROR sending error packet"); } else { // 发送数据 FILE *fp = fopen(buf + 2, "rb"); if (fp == NULL) { // 文件不存在 sprintf(buf, "%c%c%c%c%s%c", 0, 5, 0, 1, "File not found", 0); n = sendto(sockfd, buf, strlen(buf), 0, (struct sockaddr *)&cliaddr, len); if (n < 0) error("ERROR sending error packet"); } else { // 文件存在,发送数据包 unsigned short block = 1; while (1) { // 读取文件 n = fread(buf + 4, 1, BUF_SIZE - 4, fp); if (n < BUF_SIZE - 4) { if (feof(fp)) { // 文件读取完成 sprintf(buf, "%c%c%c%c", 0, 3, block >> 8, block & 0xFF); n += 4; break; } else { // 文件读取出错 sprintf(buf, "%c%c%c%c%s%c", 0, 5, 0, 2, "Error reading file", 0); n = sendto(sockfd, buf, strlen(buf), 0, (struct sockaddr *)&cliaddr, len); if (n < 0) error("ERROR sending error packet"); fclose(fp); break; } } else { // 发送数据包 sprintf(buf, "%c%c%c%c", 0, 3, block >> 8, block & 0xFF); n += 4; block++; } n = sendto(sockfd, buf, n, 0, (struct sockaddr *)&cliaddr, len); if (n < 0) { fclose(fp); error("ERROR sending data"); } // 接收ACK确认包 n = recvfrom(sockfd, buf, BUF_SIZE, 0, (struct sockaddr *)&cliaddr, &len); if (n < 0) { fclose(fp); error("ERROR receiving ACK"); } opcode = ntohs(*(unsigned short *)buf); unsigned short ack_block = ntohs(*(unsigned short *)(buf + 2)); if (opcode != 4 || ack_block != block - 1) { // 接收到错误的ACK确认包 sprintf(buf, "%c%c%c%c%s%c", 0, 5, 0, 0, "ACK packet error", 0); n = sendto(sockfd, buf, strlen(buf), 0, (struct sockaddr *)&cliaddr, len); if (n < 0) error("ERROR sending error packet"); fclose(fp); break; } } fclose(fp); } } } close(sockfd); return 0; } ``` 上面的代码实现TFTP服务端的读请求功能,它会接收客户端发来的RRQ请求,并将指定的文件内容发送给客户端。如果接收到的数据包是一个ACK确认包,则继续发送下一个数据包。如果接收到的ACK确认包有误,则发送一个错误包告知客户端
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

我要出家当道士

打赏是不可能,这辈子都不可能

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值