青未了分享 http://blog.sciencenet.cn/u/yanghang

博文

TFTP文件下载器

已有 2476 次阅读 2021-7-30 23:02 |个人分类:Python|系统分类:科研笔记

一、定义

百度:TFTP(Trivial File Transfer Protocol,简单文件传输协议)是TCP/IP协议族中的一个用来在客户机与服务器之间进行简单文件传输的协议,提供不复杂、开销不大的文件传输服务。端口号为69。它基于UDP协议而实现。

特点:简单、占用资源小、适合传递小文件、适合在局域网进行传递、端口号为69、基于UDP实现。

TFTP下载器界面:

image.png

二、下载过程

下载:从服务器上将一个文件复制到本地机上。下载过程:

  1. 在本地创建一个空文件(要与下载的文件同名)

  2. 向里面写数据(接收到一点就向空文件中写一点)

  3. 关闭(接收完毕所有数据关闭文件)


TFTP下载详细过程如下图:

image.png

TFTP服务器默认监听:TFTP服务器默认监听69号端口。当客户端发送“下载”请求(即读请求)时,需要向服务器的69端口发送。服务器若批准此请求,则使用一个新的、临时的 端口进行数据传输。

客户端发送读写请求,服务器端收到请求后执行:

1、搜索:当服务器找到需要现在的文件后,会立刻打开文件,把文件中的数据通过TFTP协议发送给客户端

2、分段:如果文件的总大小较大(比如3M),那么服务器分多次发送,每次会从文件中读取512个字节的数据发送过来。

3、添加序号:因为发送的次数有可能会很多,所以为了让客户端对接收到的数据进行排序,所以在服务器发送那512个字节数据的时候,会多发2个字节的数据,用来存放序号,并且放在512个字节数据的前面,序号是从1开始的。

4、添加操作码:因为需要从服务器上下载文件时,文件可能不存在,那么此时服务器就会发送一个错误的信息过来,为了区分服务发送的是文件内容还是错误的提示信息,所以又用了2个字节 来表示这个数据包的功能(称为操作码),并且在序号的前面

操作码功能
1读请求,即下载
2写请求,即上传
3表示数据包,即DATA
4确认码,即ACK
5错误

5、发送确认码(ACK)

因为udp的数据包不安全,即发送方发送是否成功不能确定,所以TFTP协议中规定,为了让服务器知道客户端已经接收到了刚刚发送的那个数据包,所以当客户端接收到一个数据包的时候需要向服务器进行发送确认信息,即发送收到了,这样的包成为ACK(应答包)

6.发送完毕:为了标记数据已经发送完毕,所以规定,当客户端接收到的数据小于516(2字节操作码+2个字节的序号+512字节数据)时,就意味着服务器发送完毕了。如果恰好最后一次数据的长度为516K,会再发送一个长度为0的数据包。

TFTP数据包的格式如下:

image.png

三、python内置模块—struct模块

struct模块可以按照指定格式将Python数据转换为字符串,该字符串为字节流。struct模块中最重要的三个函数是pack(),unpack(),calcsize()。

  • pack(fmt, v1, v2, ...):按照给定的格式(fmt),把数据封装成字符串(实际上是类似于c结构体的字节流)

  • unpack(fmt, string):按照给定的格式(fmt)解析字节流string,返回解析出来的元组

  • calcsize(fmt):计算给定的格式(fmt)占用多少字节的内存

四、示例

功能:构造下载请求数据:“1test.jpg0octet0”

requestFileData = struct.pack("!H8sb5sb" % len(downloadFileName), 1, downloadFileName, 0, "octet", 0)

image.png

设置:确定TFTP服务器的当前目录,IP地址,并确保该目录下有要下载的文件:

image.png

完整代码:

import struct
from socket import *

filename = 'apple.jpg'
server_ip = '192.168.1.3'
server_ip = '127.0.0.1'
send_data = struct.pack('!H%dsb5sb' % len(filename), 1, filename.encode(), 0, 'octet'.encode(), 0)
#!H8sb5sb: ! 表示按照网络传输数据要求的形式来组织数据(占位的格式)
#H 表示将后面的 1 替换成占两个字节
#8s 相当于8个s(ssssssss)占8个字节
#b 占一个字节
s = socket(AF_INET, SOCK_DGRAM)
s.sendto(send_data, (server_ip, 69))  # 第一次发送, 连接服务器69端口
f = open(filename, 'ab')  #a:以追加模式打开(必要时可以创建)append;b:表示二进制
while True:
   recv_data = s.recvfrom(1024)  # 接收数据,类型为元组,
   # print('recv_data')
   # print(recv_data)
   caozuoma, ack_num = struct.unpack('!HH', recv_data[0][:4])  # 获取数据块编号
   rand_port = recv_data[1][1]  # 获取服务器的随机端口

   if int(caozuoma) == 5:
       print('文件不存在...')
       break
   print("操作码:%d,ACK:%d,服务器随机端口:%d,数据长度:%d"%(caozuoma, ack_num, rand_port, len(recv_data[0])))
   f.write(recv_data[0][4:])#将数据写入
   if len(recv_data[0]) < 516:
       break
   ack_data = struct.pack("!HH", 4, ack_num)
   s.sendto(ack_data, (server_ip, rand_port))  # 回复ACK确认包


利用print(recv_data)命令将每次接收到的数据输出到控制台。

image.png

image.png










https://wap.sciencenet.cn/blog-346157-1297687.html

上一篇:基于UDP的Socket编程——发送/接收数据
下一篇:基于UDP的Socket编程
收藏 IP: 210.72.27.*| 热度|

0

该博文允许注册用户评论 请点击登录 评论 (0 个评论)

数据加载中...

Archiver|手机版|科学网 ( 京ICP备07017567号-12 )

GMT+8, 2024-3-29 20:34

Powered by ScienceNet.cn

Copyright © 2007- 中国科学报社

返回顶部