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

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;

}

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
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确认包有误,则发送一个错误包告知客户端

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

老K殿下

你的鼓励将是我创作的最大动力

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

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

打赏作者

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

抵扣说明:

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

余额充值