TFTP协议——搭建客户端,用TFTP协议下载文件

发布于:2023-01-22 ⋅ 阅读:(669) ⋅ 点赞:(0)

TFTP协议是基于UDP协议的传输协议,适用于小文件的快速传输,但是因为它基于UDP,在网络状态不佳时,传输大文件可能力有不逮。

TFTP的原理如图所示

 客户端对服务器发出一个下载或者上传的“读写请求”报文,服务器接收到后,会根据你的请求,做出回应,。一个读写请求报文的组成部分为:2个字节的操作码、文件名、'\0'、模式、'\0'。

操作码的数值为1(下载)或者2(上传),数据类型为短整型(short int),大端。数据传输模式有:octet:二进制模式(常用)以及mail:已经不再支持。

本文实现的是下载,所以服务器接到请求后,会把文件打包成数据包,发送到客户端。数据包的格式为:2字节操作码+2字节块编号+512字节的数据内容。操作码的内容为大段的短整型数字:3

当客户端成功收到数据包后,需要返回一个4字节大小的ACK包,包内是2字节操作码+2字节块编号。其中,块编号要与服务器发来的数据包内的一致,而操作码则需要把内容改为:4。服务器收到后,就会继续发送数据包,直到某次读取的数据包的数据内容不足512字节,则结束循环。

 明晰原理以后,即可开始代码的构建。服务器由软件 tftpd32.exe 担当,我们则需要搭建客户端。

首先是头文件以及打印错误信息的宏函数

#include<stdio.h>
#include<string.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include<unistd.h>
#include<pthread.h>
#include<stdlib.h>
#include <sys/stat.h>
#include <fcntl.h>
//打印错误信息的宏函数
#define ERR_MSG(msg) do{\
	fprintf(stderr, "__%d__", __LINE__);\
	perror(msg);\
}while(0)

服务器ip地址以及端口通过外部传参的方式输入,端口固定为特殊端口:69。为了防止遗忘,可以在程序的最开始,写上一个判断

if(argc < 3)
{
	fprintf(stderr, "请输入IP port(端口69)\n");
	return -1;
}
	

搭建客户端框架

//将获取到的端口号字符串转换为整形
	int port = atoi(argv[2]);

	//创建报式套接字,UDP协议的套接字,TCP为流式套接字
	int sfd = socket(AF_INET, SOCK_DGRAM, 0);
	if(sfd < 0)
	{
		ERR_MSG("socket");
		return -1;
	}
	printf("create socket success\n");
	
	/*********************以上部分和服务器相同***********/
	//绑定客户端自身的地址信息结构体--->非必须
	
	//填充服务器的IP地址以及端口号--->因为客户端要主动发送数据包给服务器
	struct sockaddr_in sin;              //发送的包
	sin.sin_family 		= AF_INET;
	sin.sin_port 		= htons(69);
	sin.sin_addr.s_addr = inet_addr(argv[1]);
    
    socklen_t addrlen = sizeof(sin);

创建读写请求的包

//读写请求
	char RD_RW[50] = "";
//填充读写请求包
	printf("请输入文件名>>>");
	char *p = RD_RW;                  //先用一个char指针p指向首地址
	short int *czm = (short int*)p;   //在用一个短整型指针指向强转类型的p,就可以在字符数组内存入短整型
	*czm = htons(1); //存入大端的1
	char filename[20];                //存放文件名
	scanf("%s", filename);            //录入文件名
	getchar();                        //吸收回车
	strcpy(p+2, filename);            //把文件名存入读写请求包,并放于短整型的操作码后
	char* pc = p+2 + strlen(p+2);     //定义一个指针指向文件名后的位置
	*pc ='\0';                        //补'\0',其实这一步并不需要,数组一开始已经初始化
	strcpy(pc+1, "octet");            //填入"octet"

具体情况如图所示

 接下来只需将包发送给服务器即可

//发送给服务器
	if(sendto(sfd, RD_RW, 2+strlen(p+2)+1+strlen("octet")+1,\
             0, (struct sockaddr*)&sin, sizeof(sin)) < 0)
	{
		ERR_MSG("sendto");
		return -1;
	}
	printf("请求已发送\n");

我们还需要打开一个文件,用来接收服务器上下载的文件,这里就是文件IO的知识

//打开文件
	int fd = open("./picture.png", O_WRONLY | O_CREAT | O_TRUNC, 0755);
	if(fd < 0)                    //  只写      创建     覆盖写  赋予权限
	{
		perror("open");
		return -1;
	}
	printf("文件成功打开\n");

接下来,我们就需要在一个死循环内不断接收信息,以及反馈结果给服务器,直到满足条件,退出循环。

//清空
		bzero(buf, sizeof(buf));         //接收服务器发送过来的数据包
		bzero(ACK_1, sizeof(ACK_1));     //四字节大小的ACK包
		int rec;
		//接收服务器发送过来的数据包
		if((rec = recvfrom(sfd, buf, sizeof(buf), 0,\       //rec为收到的消息内容字节数
                 (struct sockaddr*)&sin, &addrlen)) < 0)
		{
			ERR_MSG("recvfrom");
			return -1;
		}
		ssize_t res = read(fd, buf+4, 512);   //因为消息包前四个字节为操作码与块编号,故后移四位读取
		write(fd, buf+4, rec-4);   //同理,写入的时候,后移四位,同时也要少些四位

采用创建读写请求包的方法,我们可以往ACK包内写入操作码,同时只需要把收到的消息包的块编号,原封不动的复制入即可。然后发送给服务器。

//返回ack包
		char *p = ACK_1;
		short int *ack = (short int*)p;
		*ack = htons(4);     //大端的4
		ACK_1[2] = buf[2];
		ACK_1[3] = buf[3];   //块编号

		//发送
		if(sendto(sfd, ACK_1, 4, 0, (struct sockaddr*)&sin, sizeof(sin)) < 0)
		{
			ERR_MSG("sendto");
			return -1;
		}

当读取的字节数小于516(2+2+512)时,退出

if(rec < 516)
{
	printf("下载完成\n");	
	break;
}

最后关闭套接字

//关闭套接字
	close(sfd);
	return 0;

附上全代码

#include<stdio.h>
#include<string.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include<unistd.h>
#include<pthread.h>
#include<stdlib.h>
#include <sys/stat.h>
#include <fcntl.h>
//打印错误信息的宏函数
#define ERR_MSG(msg) do{\
	fprintf(stderr, "__%d__", __LINE__);\
	perror(msg);\
}while(0)

int main(int argc, const char *argv[])
{
	if(argc < 3)
	{
		fprintf(stderr, "请输入IP port(端口69)\n");
		return -1;
	}
	
	//将获取到的端口号字符串转换为整形
	int port = atoi(argv[2]);

	//创建报式套接字
	int sfd = socket(AF_INET, SOCK_DGRAM, 0);
	if(sfd < 0)
	{
		ERR_MSG("socket");
		return -1;
	}
	printf("create socket success\n");
	
	/*********************以上部分和服务器相同***********/
	//绑定客户端自身的地址信息结构体--->非必须
	
	//填充服务器的IP地址以及端口号--->因为客户端要主动发送数据包给服务器
	struct sockaddr_in sin;              //发送的包
	sin.sin_family 		= AF_INET;
	sin.sin_port 		= htons(69);
	sin.sin_addr.s_addr = inet_addr(argv[1]);

	socklen_t addrlen = sizeof(sin);
	//读写请求
	char RD_RW[50] = "";
	//接受数据包
	char buf[516] = "";   //2+2+512 = 516
	
	char ACK_1[4] = "";     //返回的ACK包
/*********************************************************/
	//读写请求
	char RD_RW[50] = "";
//填充读写请求包
	printf("请输入文件名>>>");
	char *p = RD_RW;                  //先用一个char指针p指向首地址
	short int *czm = (short int*)p;   //在用一个短整型指针指向强转类型的p,就可以在字符数组内存入短整型
	*czm = htons(1); //存入大端的1
	char filename[20];                //存放文件名
	scanf("%s", filename);            //录入文件名
	getchar();                        //吸收回车
	strcpy(p+2, filename);            //把文件名存入读写请求包,并放于短整型的操作码后
	char* pc = p+2 + strlen(p+2);     //定义一个指针指向文件名后的位置
	*pc ='\0';                        //补'\0',其实这一步并不需要,数组一开始已经初始化
	strcpy(pc+1, "octet");            //填入"octet"

/*********************************************************/
	//发送给服务器
	if(sendto(sfd, RD_RW, 2+strlen(p+2)+1+strlen("octet")+1, 0, (struct sockaddr*)&sin, sizeof(sin)) < 0)
	{
		ERR_MSG("sendto");
		return -1;
	}
	printf("请求已发送\n");
	
	//打开文件
	int fd = open("./picture.png", O_WRONLY | O_CREAT | O_TRUNC, 0755);
	if(fd < 0)
	{
		perror("open");
		return -1;
	}
	printf("文件成功打开\n");

	while(1)
	{
		//接收***********************************************
		//清空
		bzero(buf, sizeof(buf));         //接收服务器发送过来的数据包
		bzero(ACK_1, sizeof(ACK_1));
		int rec;
		//接收服务器发送过来的数据包
		if((rec = recvfrom(sfd, buf, sizeof(buf), 0, (struct sockaddr*)&sin, &addrlen)) < 0)
		{
			ERR_MSG("recvfrom");
			return -1;
		}
		ssize_t res = read(fd, buf+4, 512);

		ssize_t s =	write(fd, buf+4, rec-4);
		//返回ack包
		char *p = ACK_1;
		short int *ack = (short int*)p;
		*ack = htons(4);     //大端的4
		ACK_1[2] = buf[2];
		ACK_1[3] = buf[3];   //块编号

		//发送
		if(sendto(sfd, ACK_1, 4, 0, (struct sockaddr*)&sin, sizeof(sin)) < 0)
		{
			ERR_MSG("sendto");
			return -1;
		}
		if(rec < 516)
		{
			printf("下载完成\n");
			break;
		}


	}
	//关闭套接字
	close(sfd);
	return 0;

}


网站公告

今日签到

点亮在社区的每一天
去签到