原理:用python编写tftp客户端,实现从tftp服务器上下载文件(基于UDP协议传输)
一、什么是tftp?
TFTP(Trivial File Transfer Protocol,简单文件传输协议)是TCP/IP协议族中的一个用来在客户机与服务器之间进行简单文件传输的协议,提供不复杂、开销不大的文件传输服务。端口号为69。
二、tftp服务器、客户端传输原理
1、首先,客户端发送一个读写请求的数据包给服务器
该包的格式为:操作码(2字节)+文件名(n字节)+ 0(1字节)+ 模式(固定,为octet)+0(1字节)
此处的操作码为1
操作码功能:
1读请求,即下载
2写请求,即上传
3表示数据包,即DATA
4确认码,即ACK
5错误
2、服务器收到了这个包后,会返回发送一个包
该包格式分两种,一种是服务器有这个请求的文件,所以发了所请求的数据包
该包格式为:操作码(2字节) + 块编号(2字节) + 数据(512字节)
另一种是,服务器告诉客户端并没有这个文件或者是其它的错误
该包格式为:操作码(2字节) + 差错码(2字节) + 差错信息(n字节) + 0(1字节)
3、客户端收到了服务器发送的数据包,发送一个ACK包给服务器(意思就是告诉服务器,我收到你发给我的这个包了,因为是UDP协议,所以有丢包的可能,以此来确认数据包)
4、当服务器发送给客户端的包小于516字节时,完成传输
为了标记数据已经发送完毕,所以规定,当客户端接收到的数据小于516(2字节操作码+2字节序号+512字节数据)时,就意味着服务器发送完毕了
三、下载
1、什么是下载?
"下载"文件就是从远程主机拷贝文件至自己的计算机上
2、怎样完成下载?
(1)创建一个空文件,改名为所下载的文件名
(2)向里面写数据
(3)关闭
四、各个模块的实现
关于pack与unpack
pack 负责将不同的变量打包在一起,成为一个字节字符串。
unpack 将字节字符串解包成为变量。
1、发送下载请求
import struct
import socket
def send_Request():
'''发送下载请求给服务器'''
ip = input("请输入服务器的IP地址:")
if ip != "":
#构建下载请求数据 把数据组织为网络传输所需的数据
cmd_buf=struct.pack("!H8sb5sb",1,b"test.jpg",0,b"octet",0)
'''H对应1,指1站两个字节,8s对应“test.jpg”,b对应0站一个字节,5s对应“octet”,b对应0站一个字节'''
sendAddr=(ip,69)
udpSocket=socket.socket(AF_INET,SOCK_DGRAM)
udpSocket.sendto(cmd_buf,sendAddr)
send_Request()
2、开始下载
(1)用循环接收收到的数据包
(2)先判断操作码为3还是5,为5打印异常并退出
(3)为3时,创建一个新文件,把收到的data数据写入到这个文件中,然后发送ACK包
给服务器表示我收到这个包了,然后服务器开始发送接下来的数据包
五、Tftp客户端程序源码
from socket import *
import struct
import sys
#用于存放访问服务器的IP地址
ip=""
# 创建udp套接字
udpSocket = socket(AF_INET, SOCK_DGRAM)
#用于确定包的顺序
p_num = 0
#创建文件
recvFile = ''
def print_Menu():
'''解释用法'''
global ip
print('-'*30)
print("tips:")
print("python xxxx.py 192.168.1.1")
print('-'*30)
if len(sys.argv) == 2:
ip = sys.argv[1]
else:
exit()
def send_Request():
'''发送下载请求给服务器'''
global udpSocket
if ip != "":
#构造下载请求数据 把数据组织为网络传输所需的数据
cmd_buf = struct.pack("!H8sb5sb",1,"test.jpg".encode('gbk'),0,b"octet",0)
#发送下载文件请求数据到指定服务器
sendAddr = (ip, 69)
udpSocket.sendto(cmd_buf, sendAddr)
else:
print("未输入IP地址!")
def start_Download():
'''开始下载文件'''
while True:
global recvFile
global p_num
#用两个变量接收返回 数据、(源IP地址,端口)
recvData,recvAddr = udpSocket.recvfrom(1024)
#记录数据包的长度
recvDataLen = len(recvData)
#解包,获取服务器返回数据包前四位字节。前两位字节代表操作码
'''
1读请求,即下载
2写请求,即上传
3表示数据包,即DATA
4确认码,即ACK
5错误
'''
cmdTuple = struct.unpack("!HH", recvData[:4])
#获取操作码
cmd = cmdTuple[0]
#获取块编号,用于区分数据包的排列顺序
currentPackNum = cmdTuple[1]
if cmd == 3: #是否为数据包
# 如果是第一次接收到数据,那么就创建文件
if currentPackNum == 1:
recvFile = open("test.jpg", "wb") #wb以二进制形式打开
if p_num+1 == currentPackNum:
recvFile.write(recvData[4:])
p_num+=1
print("(%d)次接收到的数据"%(p_num))
#构建ACK包,发送ACK包,告诉服务器,我收到这个包了
ackBuf = struct.pack("!HH",4,p_num)
udpSocket.sendto(ackBuf, recvAddr)
# 为了标记数据已经发送完毕,所以规定,当客户端接收到的数据小于516
#(2字节操作码+2个字节的序号+512字节数据)时,就意味着服务器发送完毕了
if recvDataLen<516:
recvFile.close()
print("已经成功下载!!!")
break
elif cmd == 5: #是否为错误应答,即表达服务器没有这个文件或其他错误
print("error num:%d"%currentPackNum)
break
udpSocket.close()
def main():
global ip
#1.打印界面
print_Menu()
#2.发送请求
send_Request()
#3.开始建立下载
start_Download()
if __name__ == '__main__':
main()