知识点1【TFTP的概述】
学习通信的基本:通信协议(具体发送上面样的报文)、通信流程(按照什么步骤发送)
1、TFTP的概述
tftp:简单文件传输协议,**基于UDP,**不进行用户有效性验证
基于UDP:编程架构是UDP的
必须创建UDP套接字
套接字分类:UDP套接字,TCP套接字,原始套接字
数据传输格式(模式选择):
octet:二进制模式
netascii:文本模式
2、TFTP的通信过程
决定了 编程流程
我们主要写客户端
注意:
服务器中,在69号端口后,是临时端口,临时端口是只要socket,不bind。因此需要创建一个新的线程,线程中执行socket()
TFTP通信过程总结
3、TFTP协议分析
读写请求是用户自己发送的,我们现在不考虑选项部分。
数据包516个字节,当小于516个字节的时候,表示下载 结束(最后一个报文),块编号中存储的是该数据包的编号。这里注意一定要对操作码进行判断,是3才可以确认是数据包。
收到数据包 我们需要回复一个ACK,ACK中的块编号需要和数据包的ACK相同
这里有一个技巧,客户端直接把收到的内容的前4个字节发送给服务器,只需要修改操作码的第二个字节即可
错误码操作码是5,差错码是不同的错误有不同的编号,差错信息说明错误。
OACK是比如我们设置了选项部分,需要设置的,相对复杂,我们下面介绍。
想一想
传输的数据的大小一定是 512Byte 吗?
由于网络的原因,一方收不到另一方的数据怎么办?
1、不一定需要512,选项修改
2、这里服务器发送数据包后,如果没有收到ACK,它会等待,并从发送数据包处开始计时,超出 超时时间后,才会发出错误码,提示重发。
而这里的数据包长度修改,超时时间的修改,都需要借助选项来完成
4、TFTP带选项的读写报文
选项和值成对出现。
OACK是在修改了选项后,服务器向客户端发送的一个报文,是起一个确认作用的。
我们结合上图,客户端一旦修改了选项部分,客户端不再是立即发送数据,而是发送OACK,让客户端确认是否要这样修改(通过ACK,块编号中:0表示同意,这也是为什么数据编号是从1开始的),客户端给予回应后,服务器才会执行发送数据的操作。
选项的种类
tsize 选项
当读操作时,tsize 选项的参数必须为“0”,服务器会返回待读取的文件的大小
当写操作时,tsize 选项参数应为待写入文件的大小,服务器会回显该选项
blksize 选项
修改传输文件时使用的数据块的大小(范围:8~65464)
timeout 选项
修改默认的数据传输超时时间(单位:秒)
注意:
1、选项没有顺序
2、在使用tsize 读操作的时候,tsize的默认参数是0(因为读时不知道文件的大小),收到OACK的时候,会返回实际文件的大小(nB说明是字符串需要转换)
总结
1、可以通过发送带选项的读/写请求发送给 server 2、如果 server 允许修改选项则发送选项修改确认包 3、server 发送的数据、选项修改确认包都是临时 port 4、server 通过 timeout 来对丢失数据包的重新发送
知识点2【TFTP案例】
需求:编写TFTP客户端(不带选项),从客户端下载文件
流程:
1、创建UDP套接字
2、构建读请求报文,并发送到服务器69号端口
3、while 循环接收,buf是516个字节,recvform的返回值是获取到数据的长度
这里需要创建一个文件描述符取接收
4、关闭文件描述符 和 套接字
构建 读写请求报文 的方式
使用sprintf组包,当sendto时,有一个内容长度,使用sprintf的返回值:组包的总长度,目的端口写69
代码演示
#include <stdio.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <unistd.h>
#include <netinet/in.h>
#include <string.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main(int argc, char const *argv[])
{
if(argc != 3)
{
printf("error\\n");
printf("modo:./a.out 196.168.9.28 a.txt\\n");
_exit(-1);
}
//创建套接字,目的:向服务器发送数据
int fd_sock = socket(AF_INET,SOCK_DGRAM,0);
if(fd_sock < 0)
{
perror("socket");
return 0;
}
//sprintf组包,返回值使用len_sprintf接收。格式: 0 1 文件名 0 模式 0
char arr_sprintf[64] = "";
int len_sprintf = sprintf(arr_sprintf,"%c%c%s%c%s%c",0,1,argv[2],0,"octet",0);
//发送sprintf组包(读写报文)
struct sockaddr_in addr_rw;
memset(&addr_rw,0,sizeof(addr_rw));
addr_rw.sin_family = AF_INET;
addr_rw.sin_port = htons(69);
addr_rw.sin_addr.s_addr = inet_addr(argv[1]);
sendto(fd_sock,arr_sprintf,len_sprintf,0,(struct sockaddr *)&addr_rw,sizeof(addr_rw));
/* 接收服务器发出的 数据报文,报文的前四个字节:操作码(2),端口码(2),数据(512),存储在arr_recv中
数据段操作码判断,下面是3的操作
重复性判断,定义一个unsigned short num = 0,num+1与端口码比较
数据写入文件,并将端口码赋值给num
ACK 回应服务器 arr_recv前四个字节进行处理即可*/
//操作码为5操作
//关闭套接字 及 文件描述符
//输出错误原因
unsigned short num = 0;
int fd_w = open(argv[2],O_WRONLY | O_CREAT,0664);
if(fd_w < 0)
{
close(fd_sock);
perror("open");
return 0;
}
int len = 0;
unsigned char arr_recv[516] = "";
struct sockaddr_in addr_recv;
int len_recv = sizeof(addr_recv);
do
{
len = recvfrom(fd_sock,arr_recv,sizeof(arr_recv),0,(struct sockaddr *)&addr_recv,&len_recv);
if(arr_recv[1] == 3)
{
if((unsigned short)(num + 1) == ntohs(*(unsigned short *)(arr_recv +2)))
{
write(fd_w,arr_recv + 4,len - 4);
//ack应答
num = ntohs(*(unsigned short *)(arr_recv +2));
printf("%d\\n",num);
}
arr_recv[1] = 4;
sendto(fd_sock,arr_recv,4,0,(struct sockaddr *)&addr_recv,sizeof(addr_recv));
}
else if(arr_recv[1] == 5)
{
close(fd_sock);
close(fd_w);
unlink(argv[2]);
printf("%s\\n",arr_recv+4);
_exit(-1);
}
} while (len == 516);
//关闭文件描述符,套接字,关闭文件
close(fd_w);
close(fd_sock);
return 0;
}
代码运行结果
结束
代码重在练习!
代码重在练习!
代码重在练习!
今天的分享就到此结束了,希望对你有所帮助,如果你喜欢我的分享,请点赞收藏夹关注,谢谢大家!!!