参考:【千锋教育】
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
一致。UDP
的socket
编码流程与函数介绍参考: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;
}