Python實現TFTP

一、TFTP協議簡單介紹

1、定義

TFTP(Trivial File Transfer Protocol):簡單文件傳輸協議)。

TFTP是TCP/IP協議族中的一個用來在客戶端與服務器之間進行簡單文件傳輸的協議,傳輸不復雜、開銷不大的文件。端口號固定爲69。

TFTP是一個傳輸文件的簡單協議,它基於UDP協議而實現。

2、特點

簡單、佔用資源少、基於UDP實現、端口號爲69、適合在局域網內傳輸小文件。

3、TFTP支持五種類型的包
opcode operation
1.Read request (RRQ)
2.Write request (WRQ)
3.Data (DATA)
4.Acknowledgment (ACK)
5.Error (ERROR)


二、TFTP數據包格式


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。

注意:

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傳輸過程



六、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()

調試可以通過Wireshark 軟件進行調試,也可以打印傳輸數據信息進行調試!


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章