1. TFTP協議介紹
TFTP(Trivial File Transfer Protocol,簡單文件傳輸協議)
是TCP/IP協議族中的一個用來在客戶端與服務器之間進行簡單文件傳輸的協議
特點:
- 簡單
- 占用資源小
- 適合傳遞小文件
- 適合在局域網進行傳遞
- 端口號為69
- 基於UDP實現
2. TFTP下載過程
TFTP服務器默認監聽69號端口
當客戶端發送“下載”請求(即讀請求)時,需要向服務器的69端口發送
服務器若批准此請求,則使用一個新的、臨時的 端口進行數據傳輸
當服務器找到需要現在的文件后,會立刻打開文件,把文件中的數據通過TFTP協議發送給客戶端
如果文件的總大小較大(比如3M),那么服務器分多次發送,每次會從文件中讀取512個字節的數據發送過來
因為發送的次數有可能會很多,所以為了讓客戶端對接收到的數據進行排序,所以在服務器發送那512個字節數據的時候,會多發2個字節的數據,用來存放序號,並且放在512個字節數據的前面,序號是從1開始的
因為需要從服務器上下載文件時,文件可能不存在,那么此時服務器就會發送一個錯誤的信息過來,為了區分服務發送的是文件內容還是錯誤的提示信息,所以又用了2個字節 來表示這個數據包的功能(稱為操作碼),並且在序號的前面
操作碼 | 功能 |
---|---|
1 | 讀請求,即下載 |
2 | 寫請求,即上傳 |
3 | 表示數據包,即DATA |
4 | 確認碼,即ACK |
5 | 錯誤 |
因為udp的數據包不安全,即發送方發送是否成功不能確定,所以TFTP協議中規定,為了讓服務器知道客戶端已經接收到了剛剛發送的那個數據包,所以當客戶端接收到一個數據包的時候需要向服務器進行發送確認信息,即發送收到了,這樣的包成為ACK(應答包)
為了標記數據已經發送完畢,所以規定,當客戶端接收到的數據小於516(2字節操作碼+2個字節的序號+512字節數據)時,就意味着服務器發送完畢了
TFTP數據包的格式如下:
2. 下載文件用客戶端參考代碼如下:
#!/usr/bin/env python3 # -*- coding:utf-8 -*- # @Time: 2020/7/2 17:51 # @Author:zhangmingda # @File: tftp_client.py # @Software: PyCharm # Description: 通過socket 使用UDP協議模仿tftp客戶端下載文件 from socket import * # 網絡套接字工具 import struct #組數包的工具 import sys if len(sys.argv) != 3: print("USAGE:python %s <Tftp Server IP> <Filename>" % sys.argv[0]) exit(1) else: server_ip = sys.argv[1] filename = sys.argv[2] # 創建UDP套接字 udpSocket = socket(AF_INET, SOCK_DGRAM) # 構造下載請求數據 # '!H%ssb5sb'字段說明: # ! 表示為網絡數據,超過1字節數據,大端方式存儲/發送 # H 表示兩個字節操作碼,對應后面的1 # s 表示文件名的字節長度,表示后面的filename,一個s表示一個字節%s為字符長度數字; # b 為一個字節長度,對應表示 后面的0 print(filename) # getCmdPack = struct.pack('!H8sb5sb',1,'test.png',0,"octet",0) getCmdPack = struct.pack("!H%ssb5sb" % len(filename),1,filename.encode('utf-8'),0,b"octet",0) # 這里構建發包的字符串必須為字節碼方式bytes # print("發包數據:",getCmdPack) # 指定服務器地址 serverAddr = (server_ip,69) # 發送下載文件請求數據到服務器端 udpSocket.sendto(getCmdPack,serverAddr) # 初始化一個變量,記錄返回的包的個數 recv_pack_num = 0 # 死循環接收服務器返回的數據 while True: #recvfrom 返回兩個值,數據和服務端信息 recvData,recvAddr = udpSocket.recvfrom(1024) recvDataLen = len(recvData) # print(recvDataLen) # print(recvAddr) # 解包獲取返回的前四個字節,從中獲取操作碼, H代表每兩個字節組成一個數據,H代表2個字節的占位符 # 解包返回的是一個元組 recvCmdTuple = struct.unpack("!H",recvData[:2]) # print("返回的操作碼元組為:",recvCmdTuple) # 獲取操作碼 recvCmd = recvCmdTuple[0] if recvCmd == 3: # 獲取塊兒編號元組 recvPackNumTuple = struct.unpack("!H",recvData[2:4]) recvPackNum = recvPackNumTuple[0] print('塊兒編號:',recvPackNum) if recvPackNum == 1: recvFile = open(filename,"ab") #判斷包是否從1開始遞增,按順序接收包 if recvPackNum == recv_pack_num+1: fileData = recvData[4:] # 按順序收到的包就寫入文件 recvFile.write(fileData) recv_pack_num += 1 # 返回確認數據包 兩個H為分別兩個字節占位符 表示后的4,和收到數據包的編號 ackCmdPack = struct.pack("!HH",4,recv_pack_num) udpSocket.sendto(ackCmdPack,recvAddr) #返回的為文件數據,但是字節小於516 ,說明后面沒有數據了。傳輸結束關閉文件 if recvDataLen < 516: recvFile.close() print("%s 下載完成" % filename) break elif recvCmd == 5: errorCode = struct.unpack("!H",recvData[2:4]) errorMessage = recvData[4:-1] print("error code:%s message:%s" % (errorCode,errorMessage.decode('utf-8'))) break else: print("未知錯誤") break
下載效果
示例用的服務端windows下的綠色軟件tftpd32.exe