TFTP,全称是 Trivial File Transfer Protocol(简单文件传输协议),基于 UDP 实现,该协议简单到只能从远程服务器读取数据或向远程服务器上传数据。TFTP 有三种模式:netascii,这是8位的ASCII码形式;另一种是octet,这是8位源数据类型;最后一种 mail 已经不再支持,它将返回的数据直接返回给用户而不是保存为文件。
虽然 TFTP 不具备通常的 FTP 的许多功能,但是学习 TFTP 可以帮助我们了解网络通信协议的基本工作过程和原理,对后续学习更加复杂的协议有很大的帮助作用。
首先看一下 TFTP 的包的类型,TFTP 有 5 种类型的包:
默认情况下,作为 TFTP 服务器的主机 A 会监听 69 端口,当作为客户端的主机 B 想要下载或上传文件时,会向主机 A 的 69 端口发送包含读文件(下载)请求或写文件(上传)请求的数据包。主机 A 收到读写请求后,会打开另外一个随机的端口,通过这个端口向主机 B 发送确认包、数据包或者错误包。
客户端向服务器的 69 端口(通常情况下)发送一个读请求,服务器收到这个读请求以后,会打开另外一个随机的端口(假设端口号是 59509),然后在它默认的路径下寻找这个文件,找到这个文件以后,每次读入文件的 512 个字节,通过端口 59509 将这 512 个字节放入数据包中发送给客户端,数据包中还包含了操作码和数据块的编号,块编号从 1 开始计数;客户端收到数据包以后,会向服务器的 59509 端口发送一个确认包,里面包含了它收到的数据包的块编号;服务器收到确认包以后,继续发送文件的下一个 512 个字节。
如此循环往复,直到文件的末尾,最后一个数据包的数据块的大小会小于 512 个字节,这时服务器就认为传输已经结束,等他接收到这最后一个数据包的确认包之后就会主动关闭连接。而客户端收到这个小于 512 个字节的数据包后也认为传输已经结束,发送完确认包之后也会关闭连接。
也许会有一种极端情况,就是文件的大小正好是 512 字节的倍数,这样的话,最后一个数据包的大小也是 512 个字节,这时服务器发送完包含文件数据的数据包以后,还会额外发送一个包含 0 字节的数据包,作为最后一个数据包,这样就可以保证客户端收到的最后一个数据包的大小总是小于 512 个字节的。也就是说,对于客户端而言,只要它收到的数据包的大小小于 512 个字节,它就认为传输已经结束,它就会关闭连接。
客户端向服务器的 69 端口(通常情况下)发送一个写请求,服务器收到这个写请求以后,会打开另外一个随机的端口(假设端口号是 59509),向客户端发送一个确认包,其中块编号是 0,以此来告诉客户端自己已经准备好接收文件,并且告诉客户端自己接收文件的端口号。
然后客户端就开始向服务器的 59509 端口发送数据包,服务器收到数据包后向客户端发送确认包,直到整个文件发送完毕。这个过程和下载是一样的,只不过双方的角色互换了,客户端成了发数据的一方,而服务器是接收数据的一方。
TFTP 提供了一些错误机制,若出现错误,服务器会向客户端发送 ERROR 包,包格式如下:
前两个字节是操作码,值是 5,代表这是一个 ERROR 包。接下来两个字节是差错码,代表了错误的类型,下面是不同的差错码对应的错误类型:
差错码 | 含义 |
---|---|
1 | File not found. (文件未找到,服务器未找到下载请求中指定的文件) |
2 | Access violation. (访问违规,程序对于服务器的默认路径没有写权限导致的) |
3 | Disk full or allocation exceeded. (磁盘已满或超出分配,上传文件时可能会出现这个错误) |
4 | Illegal TFTP operation. (非法的 TFTP 操作,服务器无法识别 TFTP 包中的操作码) |
5 | Unknown transfer ID. (未知的传输标识) |
6 | File already exists. (文件已存在,要上传的文件已存在于服务器中) |
7 | No such user. (没有该用户) |
接下来的 n 个字节用于存放错误信息,这部分可以由程序员自己决定存放什么信息。最后一个字节是 0,用来标识结尾。
下面以下载文件为例,用 Python 实现一个 TFTP 的客户端。
下面是 Python 代码:
#filename: tftp_client_download.py
import struct
from socket import *
'''
第一个参数是要下载的文件名,类型是字符串
第二个参数是服务器的IP地址和端口号,类型是元组,
元组中有两个元素,第一个元素是IP地址,类型是字符串,第二个元素是端口号,类型是整数。
比如:('192.168.1.2', 69)
'''
def download(file_name, servAddr):
file_name_byte_array = file_name.encode('gb2312')
#组包,octet 代表TFTP协议的一种模式
sendData = struct.pack('!H'+str(len(file_name_byte_array))+'sb5sb',
1, file_name_byte_array, 0, b'octet', 0)
udpSocket = socket(AF_INET, SOCK_DGRAM)
udpSocket.sendto(sendData, servAddr)
newFile = open(file_name, 'wb')
while True:
#等待接收数据
recvInfo = udpSocket.recvfrom(1024) #1024表示本次接受的最大字节数
transPort = recvInfo[1][1] #传输端口
data = recvInfo[0] #TFTP数据包的字节流
len_data = len(data)
result = struct.unpack("!H", data[:2]) #解包
opcode = result[0] #获取操作码
if opcode == 3: #如果操作码是3,说明是DATA包
result = struct.unpack('!H'+str(len_data-4)+'s', data[2:len_data])
block = result[0] #获取块编号
fileStream = result[1] #文件字节流
newFile.write(fileStream)
#向服务器发送一个确认包
ackInfo = struct.pack('!HH', 4, block)
udpSocket.sendto(ackInfo, (servAddr[0], transPort))
if len(fileStream) < 512:
break
elif opcode == 5: #如果操作码是5,说明是ERROR包
result = struct.unpack('!H'+str(len_data-5)+'s', data[2:len_data-1])
print('传输出现异常!')
print(result[1].decode('gb2312')) #输出错误信息
break
newFile.close()
udpSocket.close()
def main():
#服务器的IP地址
serverIP = '192.168.133.135'
#服务器监听端口,默认是69,这只是用来监听客户端请求的端口,另外还有操作系统随机分配的用来传输文件的端口
serverPort = 69
servAddr = (serverIP, serverPort)
filename = 'zoro.png'
download(filename, servAddr)
if __name__ == '__main__':
main()
在虚拟机中打开 tftpd64,选择 Tftp Server 选项卡,点击右上角的 Browse 按钮,选择一个主目录,将来收到下载请求的时候,程序会在你选择的这个目录里查找请求下载的文件。
查看一下虚拟机的IP地址,将上面 Python 代码中的服务器 IP 地址改成虚拟机的IP地址,在真实的电脑中运行 Python 代码。
如图所示,若没有出现任何错误提示,就文件说明下载成功,文件已经下载到了当前目录下。