【翻译】RFC1350-TFTP通讯协议

此份文档为RFC1350的翻译,由于翻译此份文档的初衷是笔者希望实现一个TFTP服务器用来作为UDP协议学习中的练手项目,因此笔者在翻译时省略了原文档开头与协议本身无关的一些声明类章节。TFTP是一个非常简单易用的文件传输协议,如果希望在单片机上实现一个简单的通过网口或者串口更新用户程序的bootloader,此协议应该十分适合用于传输升级用的hex/bin文件。

1. 协议用途

TFTP是一种简单的文件传输协议,因此其被命名为简单文件传输协议(Trivial File Transfer Protocol)或者称为TFTP。其实现于互联网用户数据报协议的上层(UDP或Datagram),所以其可以在位于不同网络的计算机之间传输文件,只要这些计算机实现了互联网用户数据报协议。(此外,亦不排除在其他数据报协议之上实现TFTP协议的可能性。)此协议被设计成一种精简的、容易实现的协议。因此,与常规的FTP协议相比,TFTP协议缺少常规FTP协议中的大部分特性。TFTP唯一做的事就是从服务器读取文件或者写入文件到服务器。它不能列出目录,并且不支持用户身份认证。与其他的互联网协议一样,它传输8位长度的字节数据。

TFTP协议目前支持三种传输模式:

  • netascii(这是一种由 “USA Standard Code for INformation Interchange” 定义,在 “Telnet Protocol Specification” 中修改了部分特性的ascii码,请牢记这是一种8位长度的ascii码。“netascii” 这个词将在此文档中通篇使用,用于指代这种特殊格式的ascii码。);
  • octet(这替代了此文档之前版本的“binary”一词)原始8位字节;
  • mail,直接发送给用户的netascii字符,而不是存为一个文件。(mail模式已被废弃,并且不应该被实现和使用。)

其他模式可以通过主机之间自行协商来实现。可以参考章节4.2获知更多关于TFTP的有价值的指引和建议。

2. 协议概述

任何传输都开始于一个读文件请求或者写文件请求,读/写文件请求同时也用于建立连接。如果服务器同意连接,连接便处于打开状态,随后文件分隔为长度512字节的块进行传输。每个数据包包含一个数据块,并且每一个数据包必须被应答包应答,之后才能发送下一个数据包。一个低于512字节长度的数据包意味着一次完整传输的结束。如果一个包在网络中遗失了,这个包的接收方将会在接收超时后回发最后收到的包(可以为数据包或者应答包),这会使得发送方重发这个遗失的包。发送者必须在手边保留一个包以便重传,这样一来,这种环环相扣的应答机制确保了所有更早的包(在新的包之前)被成功接收。牢记所有参与传输的机器均同时作为接收方和发送方。一方发送数据并接收应答,另一方发送应答并接收数据。

大部分的错误会导致连接的终止。一个错误会以发送一个差错包的方式被标识。这个包不会被应答或重发(比如,一个TFTP服务器或客户端会在发送一个错误信息之后断开连接,所以连接的另一方可能不会收到这个差错包。)因此,当差错包遗失时,超时机制被用于处理此类连接中断。有三种事件会导致错误:无法响应请求(比如:文件不存在、非法访问、或者用户不存在),接受到由于网络传输延迟或者重复导致的无法识别的包(比如,一个格式错误的包),以及丢失对必要资源的访问权限(比如,磁盘满或者传输过程中的拒绝访问)。

只有在一种错误情况下,TFTP不会终止连接,即接收包的源端口不正确。在这种情况下,一个差错包将被发送给源主机。(译者注: “接收数据包的源端口不正确” 这句话可以参考其后的章节4中的建立连接的例子理解。)

3. 协议的其他相关事宜

之前已经提到了TFTP协议基于数据报协议实现,并且由于数据报协议基于IP协议实现,所以一个TFTP包将包含一个IP协议首部、一个数据报协议首部、以及一个TFTP协议首部。除此之外,这个包可能还会包含一个用于实现在本地传输介质上进行传输的报头(对于目前最常用的以太网来说,这个报头就是以太网帧头,当然这个取决于底层网络的类型)。就如同图3-1所示,一个包依次包含以下内容:本地媒介报头(可能存在)、IP协议首部、数据报协议首部、TFTP协议首部,紧接着是TFTP包的其他部分(其他部分可能是数据,也可能不是,这取决于在TFTP协议首部中标明的包类型)。TFTP协议并不会影响IP协议首部中的任何字段(译者注:不像UDP或者TCP这类协议,它们会修改IP协议首部中的协议类型字段)。另一方面,TFTP协议使用了UDP协议首部中的源端口和目标端口字段(UDP协议的首部格式参考附录),同时其还使用了UDP协议中的长度字段来表明TFTP包的长度。TFTP协议中的包标记(TID,transfer identifiers)会被传给UDP协议层,作为UDP传输时的端口号,因此包标记的范围为0~65535。包标记的初始化在“协议连接初始化”一节讨论。

TFTP首部包含一个两字节长的操作码(opcode,Operation Code)字段,这个字段用于指明包的类型(比如,数据包、差错包等)。这些操作码以及不同类型包的格式将会在“TFTP包”这一章节进一步讨论。

图3-1:TFTP数据包格式

4. 初始化协议连接

通过发送请求的方式可以建立传输连接(使用WRQ请求来向远端文件系统写入文件,或者使用RRQ请求来读取文件),并且远端会进行确认应答。对于写请求,远端回复一个应答包;对于读请求,远端直接回复请求读取的第一个数据包。一般来说,在应答包中将会包含被应答的数据包的区块号。每个数据包均包含一个与之相关联的区块号;区块号从1开始并且依次递增。由于对写请求的确认应答是一个应答包,在这种特殊情况下,应答包中的区块号将会是0。(通常情况下,由于应答包用于应答数据包,因此这个应答包将会包含被应答的数据包的区块号。)如果回应的是一个差错包,这意味着请求被拒绝。

为了建立连接,每个连接的终端都会指定一个在连接期间使用的TID,TID的选择是随机的,因此不太可能发生一个终端上的多个连接同时使用同一个TID的情况(译者注:TFTP设备在建立连接时,会为每个连接分配不同的TID,这个TID同时用于指定UDP的端口,因此同一个设备不同连接使用的TID不能一致)。每个包均与连接两方的TID相关联,即源TID与目标TID。TID在UDP协议中(或者其他数据报协议)作为端口号使用。一个发起请求的主机会以如上方式选择自己的源TID,并且发送自己的初始请求到服务器的熟知TID,此熟知TID的编号以十进制表示为69(对于8进制,为105)。一般情况下,在收到请求后,服务器会选择一个新的TID作为自己在这个连接中的源TID,之后将请求包中包含的源TID作为目标TID,并将请求应答发送到这个目标TID。这两个TID会在之后的传输中一直使用。

以下这个例子说明了建立一个写文件的连接时需要进行的步骤。牢记WRQ、ACK以及DATA分别指代包类型中的 写请求类型、应答类型以及数据类型。

  1. 主机A发送一个“WRQ”包到主机B,源TID为A的TID,目标TID为69。
  2. 主机B发送一个“ACK”包(其中的区块号为0)到主机A,源TID为B为这次连接分配的TID,目标TID为A的TID。

到此为止,连接已经成功建立,序号为1的数据包可以从主机A上发送。双方主机均需要确保在之后的传输步骤中使用在步骤1、2中使用的源TID。如果在后续接收到的数据包中,任何一方发现接收的数据包中的源TID不匹配,则需要丢弃这个数据包。一个差错包将会被发送给这个数据包中包含的源TID,此时传输连接不会被终止。这种情况实际上只会在TFTP收到的数据包中包含的TID出错时发生。如果底层的协议不允许错误发生,这种错误将不会出现。(译者注:对于UDP来说,很少会发生端口冲突的情况,而校验手段也保证了传输中出现误码的报文不会被传递给上层协议,所以这种错误基本不会发生。)

以下例子讨论了上文中提及的,在此协议中可能出现的错误情况的修正方法。主机A发送了一个请求到主机B。在网络传输的某一刻,这个请求被重复发送了,因此主机B为这两次请求选择了两个TID,并且用这两个TID各发送了一个ACK到主机A。当第一个ACK到达主机A,主机A继续其后的连接操作。当第二个ACK到达主机A,这个ACK应该被拒收,但是并不需要终止第一个ACK建立的连接。因此,如果由于请求重复发送导致主机B选用不同的TID建立了两个连接,主机A可以检查ACK中的源TID并保留第一个作为后续连接使用。对于第二个ACK,主机A以向第二个ACK中的源TID回发一个差错包的方式来拒收它。

5. TFTP包

TFTP支持如下所提及的5种数据包类型:

  • 读请求(RRQ,Read request)
  • 写请求(WRQ,Write request)
  • 数据(DATA,Data)
  • 应答(ACK,Acknowledgment)
  • 差错(ERROR,Error)

TFTP包的首部包含与此包相关的操作码。

图5-1:读/写请求包

读请求包(RRQ)和写请求包(WRQ)(操作码分别为1和2)的格式如图5-1所示。文件名是以0字节(译者注:即C语言中的'\0')结尾的,由natascii字符组成的一个字节序列。模式字段的内容可以为netascii格式的字符串“netascii”、“octet”或者“mail”(或者任何大小写组合,比如“NETASCII”、“OCTET”或者“MAIL”等等),其用于指明协议中规定的三种传输模式。

  • 收到netascii模式数据的主机必须将这些数据转换为自己的表示格式(比如有些计算机用7位长度表示字符,则会在接收时将8位长度的netascii字符转换为7位的)。
  • Octet模式用于传输一个从(参与通讯的)设备上发出的8位元格式的文件。出于通用性考虑,我们假设每种设备均有一个单一的8位元格式并且在通讯中使用它。(译者注:这里应该是假设每个机器文件的最小单位为8位元,即一字节,但实际中,以32位计算机为例,文件的最小单位为32位字,即4个字节。)举例来说,在一台36位的DEC-20计算机上,一个字的长度为4个8位元字节加上4位长度的多出部分。(译者注:DEC-20是上世纪流行的一种36位计算机,之所以设计为36位的,是由于当时字符在计算机中经常以6位长度进行编码表示,将计算机设计成36位的,可以很好提高将字符组合成单词时的效率。)如果一台主机收到了一个octet文件并且需要回发它,回发的文件必须保持原样。
  • Mail模式使用邮件接收方的名字代替文件名,并且这种模式必须以读请求开始。否则Mail模式会被作为netascii模式处理。邮件的接收者名称以格式“username”或者“username@hostname”表示,后者允许邮件的接收方作为中继计算机将邮件转发。

以上讨论均假设收发双方均使用一样的模式,但是其实这不是强制的。比如,一个主机可能其中构建了一个存储服务。向这样的主机就没必要将收到的natascii格式文件转换为自己的文本格式。相反,当发送者以netascii模式发送文件,存储服务器可能只是简单的将收到的数据以8位元格式存储起来(即不管发送方使用的是netascii模式还是octet模式,接收方都当作octet模式处理)。另一种情形是在DEC-20系统上会遇到的问题,即由于DEC-20系统的一个字为36位,不管是netascii模式还是octet模式都没办法完整访问一个字中的所有位。人们可以为这样的机器创建一种特殊的模式,它读取一个字中所有位,但接收者以8位格式存储信息。当从存储站点检索这种特殊格式的文件时,必须在使用前将其恢复为原始格式,因此还必须实现一个还原模式。客户端不得不通过记录一些信息的方式来实现这一点。在以上的所有例子中,发送给远程主机的请求包均告诉远程主机当前为octet模式,但是本地主机可能处于机器或者应用特定的其他模式,这些模式并没有在TFTP中规定,本地主机需要自行对octet模式的特性进行兼容。

通讯的双发自行协商制定特定的模式也是可以的,但是需要十分小心。其他主机并没有实现这些特定模式的必要,并且也没有权威的中央机构来定义并命名这些模式。

图5-2:数据包

数据实际上是以如图5-2所示的数据包(DATA Packet)形式传输的。数据包(操作码=3)包含一个区块号和数据字段。区块号从1开始,并且在生成数据包时以1递增。这样程序便可以借助区块号来区分某个数据包是新的数据包还是重复发送的数据包。数据字段的长度为0~512(包括0和512),如果数据字段长度为512字节,说明这个数据块不是最后一个数据块,如果数据字段长度不足512字节,这意味着传输的结束。(阅读后文的“正常结束传输”章节获取更多信息。)

图5-3:应答包

所有包,除了重复的应答包(ACK Packet)以及用于终止传输的包,都会被应答,除非出现通讯超时的情况。对于前一个数据包的第一个应答包来说,后一个发送的数据包就是对其的应答。写请求包以及数据包由应答包或者差错包应答,不过读请求包和应答包由数据包或者差错包应答。图5-3描述了一个应答包的格式,其操作码为4。应答包中的区块号与被应答的数据包中的区块号相同。当应答写请求包时,应答包中的区块号被设置为0。

图5-4:差错包 

差错包(ERROR Packet,操作码为5)的格式如图5-4所示。差错包可以作为其他任何类型包的应答。错误码是一个整数,用于指示错误的类型。在文后附录中列出的错误码与其对应的错误类型。(牢记本版本文档新增了一些错误码。)错误信息是给人看的,并且应该为netascii格式。与所有其他字符串相同,其以0字节结尾。

6. 正常结束连接

一个数据块长度间于0到511字节之间(包括0字节和511字节,此时整个数据包长度应该小于516)的数据包标志着传输的结束。这个包将会想其他数据包一样被应答包响应。主机在发送应答包来响应完最后一个数据包后,将会关闭自己一侧的连接。另一方面,建议进行适当的延时。这意味着主机在发送完最后一个应答后,可以延迟一会再关闭链接,以应对应答包丢失需要重新发送的情况。应答的一方如果再次收到的最后一个数据包,此时它就知道应答包遗失了。发送最后一个数据包的主机必须一直重发最后一个数据包直到这个数据包被应答或者发送超时。如果应答为应答包,意味着传输成功结束。如果数据发送方发送超时并且不打算再重发,则本次传输可能已经成功完成,这时应答方或者网络可能出现了问题。在这种情况下,传输也可能失败。无论如何,连接已经被关闭了。

7. 提前结束连接

如果一个请求没有被允许,或者某些错误在传输过程中发生,这时一个差错包(操作码为5)将会被发送。这只是出于礼貌起见,故而它将不会被重传或应答,亦可能不会被接收。超时机制必须被用于检测错误。

Ⅰ. 附录

首部排序

TFTP数据包格式
RRQ/WRQ包
DATA包
ACK包
ERROR包

读取文件的初始化连接流程

  1. 主机A发送一个“RRQ”包到主机B,源TID为A的TID,目标TID为69。
  2. 主机B发送一个“DATA”包(其中的区块号为1)到主机A,源TID为B为这次连接分配的TID,目标TID为A的TID。

错误码

码值 含义
0 未定义错误,参考错误信息(如果有)
1 文件不存在
2 非法访问
3 磁盘已满或分配溢出
4 非法的TFTP操作
5 未知传输ID
6 文件已存在
7 此用户不存在

UDP协议首部

列于此处只是出于方便,TFTP协议并不是非得基于UDP协议实现。(译者注:这里我列出了整个UDP的报文格式)

UDP报文格式

注意:TFTP将自己的传输编号(TID)作为UDP协议的源端口号以及目标端口号。