1. TFTP协议介绍
TFTP(Trivial File Transfer Protocol,简单文件传输协议)
是TCP/IP协议族中的一个用来在客户端与服务器之间进行简单文件传输的协议
特点:
- 简单
- 占用资源小
- 适合传递小文件
- 适合在局域网进行传递
- 端口号为69
- 基于UDP实现
2. TFTP下载过程
TFTP服务器默认监听69号端口
当客户端发送“下载”请求(即读请求)时,需要向服务器的69端口发送
服务器若批准此请求,则使用一个新的、临时的 端口进行数据传输
当服务器找到需要现在的文件后,会立刻打开文件,把文件中的数据通过TFTP协议发送给客户端
如果文件的总大小较大(比如3M),那么服务器分多次发送,每次会从文件中读取512个字节的数据发送过来
因为发送的次数有可能会很多,所以为了让客户端对接收到的数据进行排序,所以在服务器发送那512个字节数据的时候,会多发2个字节的数据,用来存放序号,并且放在512个字节数据的前面,序号是从1开始的
因为需要从服务器上下载文件时,文件可能不存在,那么此时服务器就会发送一个错误的信息过来,为了区分服务发送的是文件内容还是错误的提示信息,所以又用了2个字节 来表示这个数据包的功能(称为操作码),并且在序号的前面
操作码 | 功能 |
---|---|
1 | 读请求,即下载 |
2 | 写请求,即上传 |
3 | 表示数据包,即DATA |
4 | 确认码,即ACK |
5 | 错误 |
因为udp的数据包不安全,即发送方发送是否成功不能确定,所以TFTP协议中规定,为了让服务器知道客户端已经接收到了刚刚发送的那个数据包,所以当客户端接收到一个数据包的时候需要向服务器进行发送确认信息,即发送收到了,这样的包成为ACK(应答包)
为了标记数据已经发送完毕,所以规定,当客户端接收到的数据小于516(2字节操作码+2个字节的序号+512字节数据)时,就意味着服务器发送完毕了
TFTP数据包的格式如下:
2. 下载文件用客户端参考代码如下:
#!/usr/bin/env python3 # -*- coding:utf-8 -*- # @Time: 2020/7/2 17:51 # @Author:zhangmingda # @File: tftp_client.py # @Software: PyCharm # Description: 通过socket 使用UDP协议模仿tftp客户端下载文件 from socket import * # 网络套接字工具 import struct #组数包的工具 import sys if len(sys.argv) != 3: print("USAGE:python %s <Tftp Server IP> <Filename>" % sys.argv[0]) exit(1) else: server_ip = sys.argv[1] filename = sys.argv[2] # 创建UDP套接字 udpSocket = socket(AF_INET, SOCK_DGRAM) # 构造下载请求数据 # '!H%ssb5sb'字段说明: # ! 表示为网络数据,超过1字节数据,大端方式存储/发送 # H 表示两个字节操作码,对应后面的1 # s 表示文件名的字节长度,表示后面的filename,一个s表示一个字节%s为字符长度数字; # b 为一个字节长度,对应表示 后面的0 print(filename) # getCmdPack = struct.pack('!H8sb5sb',1,'test.png',0,"octet",0) getCmdPack = struct.pack("!H%ssb5sb" % len(filename),1,filename.encode('utf-8'),0,b"octet",0) # 这里构建发包的字符串必须为字节码方式bytes # print("发包数据:",getCmdPack) # 指定服务器地址 serverAddr = (server_ip,69) # 发送下载文件请求数据到服务器端 udpSocket.sendto(getCmdPack,serverAddr) # 初始化一个变量,记录返回的包的个数 recv_pack_num = 0 # 死循环接收服务器返回的数据 while True: #recvfrom 返回两个值,数据和服务端信息 recvData,recvAddr = udpSocket.recvfrom(1024) recvDataLen = len(recvData) # print(recvDataLen) # print(recvAddr) # 解包获取返回的前四个字节,从中获取操作码, H代表每两个字节组成一个数据,H代表2个字节的占位符 # 解包返回的是一个元组 recvCmdTuple = struct.unpack("!H",recvData[:2]) # print("返回的操作码元组为:",recvCmdTuple) # 获取操作码 recvCmd = recvCmdTuple[0] if recvCmd == 3: # 获取块儿编号元组 recvPackNumTuple = struct.unpack("!H",recvData[2:4]) recvPackNum = recvPackNumTuple[0] print('块儿编号:',recvPackNum) if recvPackNum == 1: recvFile = open(filename,"ab") #判断包是否从1开始递增,按顺序接收包 if recvPackNum == recv_pack_num+1: fileData = recvData[4:] # 按顺序收到的包就写入文件 recvFile.write(fileData) recv_pack_num += 1 # 返回确认数据包 两个H为分别两个字节占位符 表示后的4,和收到数据包的编号 ackCmdPack = struct.pack("!HH",4,recv_pack_num) udpSocket.sendto(ackCmdPack,recvAddr) #返回的为文件数据,但是字节小于516 ,说明后面没有数据了。传输结束关闭文件 if recvDataLen < 516: recvFile.close() print("%s 下载完成" % filename) break elif recvCmd == 5: errorCode = struct.unpack("!H",recvData[2:4]) errorMessage = recvData[4:-1] print("error code:%s message:%s" % (errorCode,errorMessage.decode('utf-8'))) break else: print("未知错误") break
下载效果
示例用的服务端windows下的绿色软件tftpd32.exe