🔥码云GVP开源项目 12k star Uniapp+ElementUI 功能强大 支持多语言、二开方便! 广告
#### 一、TFTP协议简单介绍 ##### 1.定义 TFTP(Trivial File Transfer Protocol):简单文件传输协议)。 TFTP是TCP/IP协议族中的一个用来在客户端与服务器之间进行简单文件传输的协议,传输不复杂、开销不大的文件。端口号固定为69。 TFTP是一个传输文件的简单协议,它基于UDP协议而实现。 ##### 2.特点 简单、占用资源少、基于UDP实现、端口号为69、适合在局域网内传输小文件。 ##### 3.TFTP支持五种类型的包 opcode operation * Read request (RRQ) * Write request (WRQ) * Data (DATA) * Acknowledgment (ACK) * Error (ERROR) #### 二、TFTP数据包格式 ![](https://box.kancloud.cn/e2fcc247596b082393b9fab9fb33c14e_819x451.png) ~~~ 1、读写请求 操作码 + 文件名 + 0 + 模式 + 0 2Bytes String 1Byte String 1Byte 当操作码的取值为1时,表示RD 读请求;当操作码的取值为2时,表示WE 写请求。 2、数据包 操作码 + 块编码 + 数据 2Bytes 2Bytes 512Bytes 数据包操作码值为3。 3、ACK 操作码 + 块编码 2Bytes 2Bytes ACK 操作码值为4。 4、ERROR 操作码 + 差错码 + 差错信息 + 0 2Bytes 2Bytes String 1Byte ERROR 操作码值为5。 ~~~ >[danger] 注意 >**** >1、当客户端接收到的数据小于516字节时,表示服务器发送数据完成! 2、块编码从0开始,每次加1,它的范围是[0, 65535]。 #### 三、TFTP协议过程分析 ##### 1、下载过程 第一步:客户端给服务器发送下载请求,数据格式为(操作码1+文件名+0+模式+0)。 第二步:服务器接收到请求之后,回复客户端消息,数据格式为元组类型。如下所示:(操作码3+块编码0+数据, (IP号, 端口号))。 第三步:客户端每接受一次数据,都要回复服务器一次ACK信号。 第四步:直到客户端接收到的数据小于516个字节,才说明服务器发送完毕! ##### 2、上传过程 第一步:客户端给服务器发送上传请求,数据格式为(操作码2+文件名+0+模式+0)。 第二步:服务器接收到请求之后,回复客户端ACK消息,数据格式为元组类型。如下所示:(操作码4+块编码0, (IP号, 端口号))。 第三步:客户端每发送一次数据,服务器都要回复一次ACK信号。 第四步:直到客户端发送完数据才结束。 #### 四、TFTP具体传输数据 ##### 1、服务器回复客户端下载请求 ~~~ (b'\x00\x03\x00\x01\xff\xd8\xff\xe0\x00\x10JFIF\x00\x01\x02\x01\x00H\x00H\x00\x00\xff\xe1\x08\xbbExif\x00\x00MM\x00*\x00 \x00\x00\x08\x00\x07\x01\x12\x00\x03\x00\x00\x00\x01\x00\x01\x00\x00\x01\x1a\x00\x05\x00\x00\x00\x01\x00\x00\x00b\x01\x1b \x00\x05\x00\x00\x00\x01\x00\x00\x00j\x01(\x00\x03\x00\x00\x00\x01\x00\x02\x00\x00\x011\x00\x02\x00\x00\x00\x14\x00\x00\x00r \x012\x00\x02\x00\x00\x00\x14\x00\x00\x00\x86\x87i\x00\x04\x00\x00\x00\x01\x00\x00\x00\x9c\x00\x00\x00\xc8\x00\x00\x00H\x00 \x00\x00\x01\x00\x00\x00H\x00\x00\x00\x01Adobe Photoshop 7.0\x002004:06:15 16:14:56\x00\x00\x00\x00\x03\xa0\x01\x00\x03\x00 \x00\x00\x01\xff\xff\x00\x00\xa0\x02\x00\x04\x00\x00\x00\x01\x00\x00\x04\x00\xa0\x03\x00\x04\x00\x00\x00\x01\x00\x00\x03\x00 \x00\x00\x00\x00\x00\x00\x00\x06\x01\x03\x00\x03\x00\x00\x00\x01\x00\x06\x00\x00\x01\x1a\x00\x05\x00\x00\x00\x01\x00\x00\x01 \x16\x01\x1b\x00\x05\x00\x00\x00\x01\x00\x00\x01\x1e\x01(\x00\x03\x00\x00\x00\x01\x00\x02\x00\x00\x02\x01\x00\x04\x00\x00\x00 \x01\x00\x00\x01&\x02\x02\x00\x04\x00\x00\x00\x01\x00\x00\x07\x8d\x00\x00\x00\x00\x00\x00\x00H\x00\x00\x00\x01\x00\x00\x00H\x00 \x00\x00\x01\xff\xd8\xff\xe0\x00\x10JFIF\x00\x01\x02\x01\x00H\x00H\x00\x00\xff\xed\x00\x0cAdobe_CM\x00\x02\xff\xee\x00\x0eAdobe \x00d\x80\x00\x00\x00\x01\xff\xdb\x00\x84\x00\x0c\x08\x08\x08\t\x08\x0c\t\t\x0c\x11\x0b\n\x0b\x11\x15\x0f\x0c\x0c\x0f\x15\x18\x13 \x13\x15\x13\x13\x18\x11\x0c\x0c\x0c\x0c\x0c\x0c\x11\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c \x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x01\r\x0b\x0b\r\x0e\r\x10\x0e\x0e\x10\x14\x0e\x0e\x0e\x14\x14\x0e\x0e\x0e\x0e\x14\x11\x0c\x0c \x0c\x0c\x0c\x11\x11\x0c\x0c\x0c\x0c\x0c\x0c\x11\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c \x0c\x0c\x0c\x0c\x0c\x0c\x0c\xff\xc0\x00\x11', ('192.168.43.119', 54835)) ~~~ 由于数据太长,一行不方便显示,这里多行展示了TFTP服务器回复客户端的下载请求数据。 ##### 2、服务器回复客户端上传请求 `(b'\x00\x04\x00\x00', ('192.168.43.119', 62768)) ` #### 五、TFTP传输过程 ![](https://box.kancloud.cn/ec8f4661731fec3fa98e30de3fca24e3_539x610.png) #### 六、Python实现TFTP协议 ##### 1、客户端下载文件参考程序 ~~~ #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] #print(g_server_ip, g_downloadFileName) #主程序 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) #print(responseData) recvData, serverInfo = responseData # 解包 packetOpt = struct.unpack("!H", recvData[:2]) #操作码 packetNum = struct.unpack("!H", recvData[2:4]) #块编号 #print(packetOpt, packetNum) # 接收到数据包 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() ~~~ ##### 2、客户端上传文件程序 ~~~ #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() ~~~ ##### 3、服务器参考程序 ~~~ #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() ~~~ >[warning] 用到的软件:Wireshark,Tftpd32 >**** >Wireshark软件进行调试,也可以打印传输数据信息进行调试! >Tftpd32 一个可以当做服务器软件 > >[success] 小结------tftp可以理解为: >*** >* 1.tftp是基于udp的协议 >* 2.实现简单的tftp,首先要有tftp的协议图 >* 3.tftp默认接收端口为69,但每次有连接过来后,tftp会随机分配一个端口来专门为这个连接来服务。 >* 4.操作码:1.上传 2.下载 3.传数据 4.接收确认 5.错误码