准备工作:wireshark 抓包工具、TFTP的下载和上传、pack、unpack
0、定义
TFTP(Trivial File Transfer Protocol,简单文件传输协议),是TCP/IP协议族中的一个用来在客户端与服务器(C/S)之间进行简单文件传输的协议。
1、特点:
- 简单
- 占用资源小
- 适合传递小文件
- 适合在局域网进行传递
- 端口号为69
- 基于UDP实现
2、TFTP支持五种类型的包
1.Read request (RRQ)
2.Write request (WRQ)
3.Data (DATA)
4.Acknowledgment (ACK)
5.Error (ERROR)
3、TFTP数据包格式
操作码 功能
1 读请求,即下载
2 写请求,即上传
3 表示数据包,即DATA
4 确认码,即ACK
5 错误
注:
当服务器找到需要现在的文件后,会立刻打开文件,把文件中的数据通过TFTP协议发送给客户端。
如果文件的总大小较大(比如3M),那么服务器分多次发送,每次会从文件中读取512个字节的数据发送过来。
因为发送的次数有可能会很多,所以为了让客户端对接收到的数据进行排序,所以在服务器发送那512个字节数据的时候,会多发2个字节的数据,用来存放序号,并且放在512个字节数据的前面,序号是从0开始的0,范围[0,65535]。
因为需要从服务器上下载文件时,文件可能不存在,那么此时服务器就会发送一个错误的信息过来,为了区分服务发送的是文件内容还是错误的提示信息,所以又用了2个字节 来表示这个数据包的功能(称为操作码),并且在序号的前面。
因为udp的数据包不安全,即发送方发送是否成功不能确定,所以TFTP协议中规定,为了让服务器知道客户端已经接收到了刚刚发送的那个数据包,所以当客户端接收到一个数据包的时候需要向服务器进行发送确认信息,即发送收到了,这样的包成为ACK(应答包)。
为了标记数据已经发送完毕,所以规定,当客户端接收到的数据小于516(2字节操作码+2个字节的序号+512字节数据)时,就意味着服务器发送完毕了。
4、TFTP下载过程
5、TFTP协议过程分析
1、下载过程
第一步:客户端给服务器发送下载请求,
数据格式为(操作码1+文件名+0+模式+0)。
第二步:服务器接收到请求之后,回复客户端消息,数据格式为元组类型。
如下所示:(操作码3+块编码 +数据, (IP号,端口号))。
第三步:客户端每接受一次数据,都要回复服务器一次ACK信号。
2、上传过程
第一步:客户端给服务器发送上传请求,
数据格式为(操作码2+文件名+0+模式+0)。
第二步:服务器接收到请求之后,回复客户端ACK消息,数据格式为元组类型。
如下所示:(操作码4+块编码0, (IP号, 端口号))。
第三步:客户端每发送一次数据,服务器都要回复一次ACK信号。
6、Python实现TFTP协议
0.客户端下载
#coding=utf-8
#导包
import sys
import struct
from socket import *
#全局变量
g_server_ip = ''
g_downloadFileName = ''
#运行程序格式不正确
def run_test():
"判断运行程序传入参数是否有错"
global g_server_ip
global g_downloadFileName
if len(sys.argv) != 3:
print("运行程序格式不正确")
print('-'*30)
print("tips:")
print("python3 tftp_download.py 192.168.1.1 test.jpg")
print('-'*30)
exit()
else:
g_server_ip = sys.argv[1]
g_downloadFileName = sys.argv[2]
#主程序
def main():
run_test()
# 打包
sendDataFirst = struct.pack('!H%dsb5sb'%len(g_downloadFileName), 1, g_downloadFileName.encode('gb2312'), 0, 'octet'.encode('gb2312'), 0)
# 创建UDP套接字
s = socket(AF_INET, SOCK_DGRAM)
# 发送下载文件请求数据到指定服务器
s.sendto(sendDataFirst, (g_server_ip, 69)) #第一次发送, 连接tftp服务器
downloadFlag = True #表示能够下载数据,即不擅长,如果是false那么就删除
fileNum = 0 #表示接收文件的序号
# 以二进制格式创建新文件
f = open(g_downloadFileName, 'wb')
while True:
#3. 接收服务发送回来的应答数据
responseData = s.recvfrom(1024)
recvData, serverInfo = responseData
# 解包
packetOpt = struct.unpack("!H", recvData[:2]) #操作码
packetNum = struct.unpack("!H", recvData[2:4]) #块编号
# 接收到数据包
if packetOpt[0] == 3: #optNum是一个元组(3,)
# 计算出这次文件的序号,是上一次接收到的+1。
fileNum += 1
# 文件超过了65535 那么就又从0开始计数。
if fileNum == 65536:
fileNum = 0
# 包编号是否和上次相等
if fileNum == packetNum[0]:
f.write(recvData[4:]) #写入文件
fileNum = packetNum[0]
# 整理ACK的数据包
ackData = struct.pack("!HH", 4, packetNum[0])
s.sendto(ackData, serverInfo)
# 错误应答
elif packetOpt[0] == 5:
print("sorry,没有这个文件!")
downloadFlag = False
break
else:
print(packetOpt[0])
break
# 接收完成,退出程序。
if len(recvData) < 516:
downloadFlag = True
print("%s文件下载完毕!"%g_downloadFileName)
break
if downloadFlag == True:
f.close()
else:
os.unlink(g_downloadFileName) #没有下载的文件,就删除刚创建的文件。
#调用main函数
if __name__ == '__main__':
main()
1.客户端上传
#coding=utf-8
# 导包
import sys
import struct
from socket import *
# 全局变量
g_server_ip = ''
g_uploadFileName = ''
#运行程序格式不正确
def run_test():
"判断运行程序传入参数是否有错"
global g_server_ip
global g_uploadFileName
if len(sys.argv) != 3:
print("运行程序格式不正确")
print('-'*30)
print("tips:")
print("python3 tftp_upload.py 192.168.1.1 test.jpg")
print('-'*30)
exit()
else:
g_server_ip = sys.argv[1]
g_uploadFileName = sys.argv[2]
#print(g_server_ip, g_uploadFileName)
#主程序
def main():
run_test()
# 打包
sendDataFirst = struct.pack('!H%dsb5sb'%len(g_uploadFileName), 2, g_uploadFileName.encode('gb2312'), 0, 'octet'.encode('gb2312'), 0)
# 创建UDP套接字
s = socket(AF_INET, SOCK_DGRAM)
# 发送上传文件请求到指定服务器
s.sendto(sendDataFirst, (g_server_ip, 69)) #第一次发送, 连接tftp服务器
fileNum = 0 #表示接收文件的序号
# 以二进制格式打开文件
f = open(g_uploadFileName, 'rb')
# 第一次接收数据
responseData = s.recvfrom(1024)
# print(responseData)
recvData, serverInfo = responseData
#print(recvData)
#print(serverInfo)
# 解包
packetOpt = struct.unpack("!H", recvData[:2]) #操作码
packetNum = struct.unpack("!H", recvData[2:4]) #块编号
#print(packetOpt, packetNum)
if packetOpt[0] == 5:
print("tftp服务器发生错误!")
exit()
while True:
# 从文件中读取512字节数据
readFileData = f.read(512)
# 打包
sendData = struct.pack('!HH', 3, fileNum) + readFileData
# 发送数据到tftp服务器
s.sendto(sendData, serverInfo) #第二次发给服务器的随机端口
# 接受服务器回传数据
recvData, serverInfo = s.recvfrom(1024)
#print(recvData)
# 解包
packetOpt = struct.unpack("!H", recvData[:2]) #操作码
packetNum = struct.unpack("!H", recvData[2:4]) #块编号
if packetOpt[0] == 5:
print("tftp服务器发生错误!")
exit()
if len(sendData) < 516 or packetNum[0] != fileNum:
print("%s文件上传成功!"%g_uploadFileName)
break
fileNum += 1
# 关闭文件
f.close()
# 关闭套接字
s.close()
#调用main函数
if __name__ == '__main__':
main()
2.服务器
#coding=utf-8
# 导包
import sys
import struct
from socket import *
from threading import Thread
'''''
利用多线程的机制,来实现tftp服务器同时进行上传和下载功能。
'''
# 客户端上传线程
def upload_thread(fileName, clientInfo):
"负责处理客户端上传文件"
fileNum = 0 #表示接收文件的序号
# 以二进制方式打开文件
f = open(fileName, 'wb')
# 创建UDP套接字
s = socket(AF_INET, SOCK_DGRAM)
# 打包
sendDataFirst = struct.pack("!HH", 4, fileNum)
# 回复客户端上传请求
s.sendto(sendDataFirst, clientInfo) #第一次用随机端口发送
while True:
# 接收客户端发送的数据
responseData = s.recvfrom(1024) #第二次客户连接我随机端口
# print(responseData)
recvData, clientInfo = responseData
#print(recvData, clientInfo)
# 解包
packetOpt = struct.unpack("!H", recvData[:2]) #操作码
packetNum = struct.unpack("!H", recvData[2:4]) #块编号
#print(packetOpt, packetNum)
# 客户端上传数据
if packetOpt[0] == 3 and packetNum[0] == fileNum:
# 保存数据到文件中
f.write(recvData[4:])
# 打包
sendData = struct.pack("!HH", 4, fileNum)
# 回复客户端ACK信号
s.sendto(sendData, clientInfo) #第二次用随机端口发
fileNum += 1
if len(recvData) < 516:
print("用户"+str(clientInfo), end='')
print(':上传'+fileName+'文件完成!')
break
# 关闭文件
f.close()
# 关闭UDP套接字
s.close()
# 退出上传线程
exit()
# 客户端下载线程
def download_thread(fileName, clientInfo):
"负责处理客户端下载文件"
# 创建UDP套接字
s = socket(AF_INET, SOCK_DGRAM)
fileNum = 0 #表示接收文件的序号
try:
f = open(fileName,'rb')
except:
# 打包
errorData = struct.pack('!HHHb', 5, 5, 5, fileNum)
# 发送错误信息
s.sendto(errorData, clientInfo) #文件不存在时发送
exit() #退出下载线程
while True:
# 从本地服务器中读取文件内容512字节
readFileData = f.read(512)
fileNum += 1
# 打包
sendData = struct.pack('!HH', 3, fileNum) + readFileData
# 向客户端发送文件数据
s.sendto(sendData, clientInfo) #数据第一次发送
if len(sendData) < 516:
print("用户"+str(clientInfo), end='')
print(':下载'+fileName+'文件完成!')
break
# 第二次接收数据
responseData = s.recvfrom(1024)
# print(responseData)
recvData, clientInfo = responseData
#print(recvData, clientInfo)
#解包
packetOpt = struct.unpack("!H", recvData[:2]) #操作码
packetNum = struct.unpack("!H", recvData[2:4]) #块编号
#print(packetOpt, packetNum)
if packetOpt[0] != 4 or packetNum[0] != fileNum:
print("文件传输错误!")
break
# 关闭文件
f.close()
# 关闭UDP套接字
s.close()
# 退出下载线程
exit()
# main函数
def main():
# 创建UDP套接字
s = socket(AF_INET, SOCK_DGRAM)
# 解决重复绑定端口
s.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
# 绑定任意IP,端口号69
s.bind(('', 69))
print("tftp服务器成功启动!")
print("正在运行中...")
while True:
# 接收客户端发送的消息
recvData, clientInfo = s.recvfrom(1024) # 第一次客户连接69端口
#print(clientInfo)
# 解包
if struct.unpack('!b5sb', recvData[-7:]) == (0, b'octet', 0):
opcode = struct.unpack('!H',recvData[:2]) # 操作码
fileName = recvData[2:-7].decode('gb2312') # 文件名
# 请求下载
if opcode[0] == 1:
t = Thread(target=download_thread, args=(fileName, clientInfo))
t.start() # 启动下载线程
# 请求上传
elif opcode[0] == 2:
t = Thread(target=upload_thread, args=(fileName, clientInfo))
t.start() # 启动上传线程
# 关闭UDP套接字
s.close()
# 调用main函数
if __name__ == '__main__':
main()