1.首先要大概理解整个过程
TFTP(Trivial File Transfer Protocol,简单文件传输协议)
是TCP/IP协议族中的一个用来在客户端与服务器之间进行简单文件传输的协议
特点:
1,简单
2,占用资源小
3,适合传递小文件
4,适合在局域网进行传递
5,端口号为69
6,基于UDP实现
TFTP服务器默认监听69号端口
当客户端发送“下载”请求(即读请求)时,需要向服务器的69端口发送
服务器若批准此请求,则使用一个新的、临时的 端口进行数据传输
①需求就是我想从TFTP服务器下载一个文件,我就是客户端
sendto(请求数据,地址1)
②我去发送请求,服务器回复数据包(从我的角度看就是 接收数据包)
recvfrom()
过程很简单,不过有一些细节问题需要仔细研究:
- 第一就是如果文件太大的话,服务器是不是一次传一部分更优;
- 第二我(客户端)在收到一部分数据之后是不是应该回应一下,说明我收到了,要不然中间有一部数据有错误,服务器还一直传。这时候应该注意到(参看下方示意图)端口是改变的(动态端口),所以此时的地址2 和地址1并不相同。地址2获取的方式显而易见,肯定是当客户端接收数据的时候获取动态端口。
udp_socket.sendto(应答数据,地址2)
-
第三就是传输时应该按照TFTP数据包的格式,解包时也是一样啊。
示意图如下:
2.代码实现
num01:发送请求
#发送请求 ,至于socket的用法直接百度可以了解
#server_ip就是服务器的IP地址(这个地方我用了Tftpd32,本地打开作为服务器,虚拟机作为客户端编写代码。
#[我之前用虚拟机作为服务器老是出小问题,所以就这样用了])
udpSocket=socket(AF_xx,SOCK_DGRANM)
server_addr = (server_ip,69)
#send_data需要按照TFTP数据包的格式
udp_socket.sendto(send_data,server_addr) #发送请求
关于send_data需要按照TFTP数据包的格式:
- 上图中读写请求的格式是:操作码+文件名+0+模式+0。操作码需要占2个字节(原因我也不知道,操作码下面的2Bytes就是占两个字节的意思)
- 那我们就需要处理成要求的格式;这就要借助于struct模块了(用法可参看这篇https://blog.csdn.net/lis_12/article/details/52777983 只看「格式符」就可以了)
附:操作码功能:操作码 功能 1 读请求,即下载 2 写请求,即上传 3 表示数据包,即DATA 4 确认码,即ACK 5 错误
- 读写请求:1,'test.jpg',0,'octet',0 (test.jpg是要下载的文件名字,可以是任意需要下载文件的名字;模式:octet这个是固定的,记下来就可以了)
- 处理成要求的格式:
send_data=struct.pack(fmt,1,'test.jpg'.encode('utf-8'),0,'octet'.encode('utf-8'),0)
send_data你可以自己打印试试结果是:b'\x00\x01test.jpg\x00octet\x00'1需要占两个字节,参看struct格式符 H——unsigned short——integer——2 fmt='!H' test.jpg是字符串,长度为8 fmt='!H8s' 以此类推:fmt='!H8sb5sb'
num02:接收数据并处理
数据包格式参看上图:操作码+块编号+数据
那么我们就应该进行拆包确认是不是数据(怎么判断?看前面的操作码 3 — 表示数据包,即DATA)
#接收并拆包数据
#一次接收一部分,返回的是元组(data,包含IP和端口的元组)
recv_data,peer_addr = udp_socket.recvfrom(1024)
# 拆包数据
opcode,blockNum = struct.unpack('!HH',recv_data[:4])#拆出来操作码和块编号
#如果操作码是3就是数据包,如果是5的话就是报错了
if opcode == 3: # 表示数据包
....
elif opcode == 5:# 出错
....
接下来返回的是3的话:
#1.拆出数据
data_fmt = '!%ds' % (len(recv_data) - 4) #减去4是去掉操作码+块编号
data_content = struct.unpack(data_fmt, recv_data[4:])#解包出来
#2.写入数据(这才是我们的目的)
f = open(file_name,'wb')
f.write(data_content[0]) # 拆出来是元组,bytes对象,write时候需要str字符串
#3.给个回应证明我接收到了
ack_data = struct.pack('!HH',4,blockNum)
udp_socket.sendto(ack_data,peer_addr) # 不能再给server_addr,因为端口号变了
#4.如果数据长度小于 2 + 2 + 512 传输结束 (很好理解 看TFTP数据包的格式.png)
if len(recv_data) < 516:
print('over')
f.close()
break
num03:整理代码,发现问题
from socket import *
import struct
udp_socket = socket(AF_INET, SOCK_DGRAM)
server_addr = (server_ip,69)
fmt='!H8sb5sb'
send_data=struct.pack(fmt,1,'test.jpg'.encode('utf-8'),0,'octet'.encode('utf-8'),0)
udp_socket.sendto(send_data,server_addr)
# 循环接收和应答
while True:
recv_data,peer_addr = udp_socket.recvfrom(1024)
# 拆包数据
opcode,blockNum = struct.unpack('!HH',recv_data[:4])
if opcode == 3: # 表示数据包
# 拆出数据
data_fmt = '!%ds' % (len(recv_data) - 4)
data_content = struct.unpack(data_fmt, recv_data[4:])
f = open(file_name,'wb')
f.write(data_content[0]) # 拆出来是元组,bytes对象,write时候需要str字符串
# 打包应答数据
ack_data = struct.pack('!HH',4,blockNum)
udp_socket.sendto(ack_data,peer_addr) # 不能再给server_addr,因为端口号变了
# 如果数据长度小于 2 + 2 + 512 传输结束
if len(recv_data) < 516:
print('over')
f.close()
break
elif opcode == 5:# 出错
...
- 第一个问题:f = open(file_name,'wb')
wb:以二进制格式打开一个文件只用于写入。如果该文件已存在则打开文件,并从开头开始编辑,即原有内容会被删除。如果该文件不存在,创建新文件。一般用于非文本文件如图片等。
因为我们是循环接收数据的,所以如果每次接收都运行这行代码会清空以前的数据。所以我们应该在第一次接收数据的时候运行。之后只运行写入的操作就可以了。
if blockNum == 1:
f = open(file_name,'wb')
# 拆出数据
data_fmt = '!%ds' % (len(recv_data) - 4)
data_content = struct.unpack(data_fmt, recv_data[4:])
- 第二问题:在写入数据的时候应该判断之前是否写过;同理也是通过blockNum(块编号)
if lastblockNum+1==blockNum:
f.write(data_content[0])
......
# 打包应答数据
ack_data = struct.pack('!HH',4,blockNum)
udp_socket.sendto(ack_data,peer_addr) # 不能再给server_addr,因为端口号变了
# 当应答完毕,更新lastBlockNum
lastBlockNum = blockNum
- 第三个问题:如果操作码是5如何处理
if 操作码==5:
err_num = blockNum
# 拆出错误信息
fmt = "!%ds" % (len(recv_data) - 5)
err_msg = struct.unpack(fmt,recv_data[4:-1])
print('出错信息:%s' % err_msg)
break