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;
}