使用 C# 的简单 TFTP 客户端






4.71/5 (16投票s)
一篇关于使用C#创建TFTP客户端的文章。
引言
许多小型网络设备使用TFTP来传输配置或固件。这个项目是在尝试获取和写入(GET和PUT)一些配置文件到网络交换机时创建的。由于网上只有商业库,我决定发布我的示例实现。由于TFTP使用UDP,创建一个简单的客户端相对容易。下一段将简要介绍TFTP协议,该协议在RFC 1350中有详细描述。
关于TFTP的几点说明
TFTP只支持两种方法:读取请求(RRQ)和写入请求(WRQ)。它既不支持目录,也没有列出所有文件的功能。用户必须知道要读取或写入哪个文件。
协议
发送到服务器的第一个数据包始终是请求(RRQ/WRQ),后面跟着数据包和确认包。如果出现问题,将发送错误包。
| 客户端 | 服务器 | 
|---|---|
| 发送到端口69的RRQ数据包(文件名和模式)。源端口将成为CTID。 | |
| 发送到端口:CTID的数据包(块号)。源端口将成为STID。 | |
| 发送到端口:STID的ACK数据包(块号)。 | |
| 下一个数据包... | 
写入请求的处理方式相同。服务器将用块号为零的ACK数据包响应请求,而不是第一个数据包。
数据包
为了区分不同的数据包类型,TFTP定义了五种操作码(Opcode)。根据操作码的不同,数据包的结构也可能不同。
| 操作码 | 操作 (Operation) | 
|---|---|
| 1 | 读取请求(RRQ) | 
| 2 | 写入请求(WRQ) | 
| 3 | 数据(DATA) | 
| 4 | 确认(ACK) | 
| 5 | 错误(ERROR) | 
RRQ/WRQ 请求
每个会话都将以客户端的请求(读/写)数据包开始,该数据包将直接发送到服务器端口(例如69)。服务器将通过第一个数据包(RRQ)或确认数据包(WRQ)进行响应。客户端数据包的源端口是客户端TID,服务器端数据包的源端口是服务器的TID(传输ID)。客户端的下一个数据包将使用服务器的TID作为目标端口发送到服务器,反之亦然。在传输活动期间,TID是恒定的。
每个请求数据包将包含操作码、以零终止的文件名以及以零终止的传输模式。
| 请求数据包 | ||||
| 2 字节 | 字符串 | 1 字节 | 字符串 | 1 字节 | 
| 操作码 | 文件名 | 0 | 模式 | 0 | 
根据请求的类型,将发送RRQ的数据包或WRQ的确认数据包。如果出现问题,服务器将发送一个错误数据包。
数据和ACK数据包
UDP本身不提供流功能。因此,服务器和客户端TID的组合将用作“虚拟通道”。TFTP将为每个数据包使用一个块号,该块号必须得到确认。如果在规定时间内(几秒)没有收到确认(某些秒),发送方将自动重发数据包,直到收到确认为止。确认数据包将收到下一个数据包的响应。由于每个数据包的有效载荷应为512字节(数据),最后一个数据包将包含0到511字节的数据。
| 数据包 | ||
| 2 字节 | 2 字节 | 数据字节 | 
| 操作码 | 块号 | Data | 
确认数据包的长度为4字节。它们只包含操作码和要确认的块号。
| ACK数据包 | |
| 2 字节 | 2 字节 | 
| 操作码 | 块号 | 
在某些情况下,服务器可能会发送一个错误数据包,其中包含操作码、错误代码和一个以一个或多个零终止的消息。许多TFTP服务器会用“\0”填充错误消息,以统一所有数据包的长度。
| 错误数据包 | |||
| 2 字节 | 2 字节 | 字符串 | 1 字节 | 
| 操作码 | 错误代码 | 错误消息 | 0 | 
实现
TFTP客户端足够小,可以放入一个类中。为了方便起见,我决定为操作码、模式定义枚举,并为TFTP故障创建一个特殊的异常类。请注意,这只是一个简单的示例实现,对于作者来说工作良好,但尚未完成(请参阅待办事项列表)。
TFTPClient有两个不同的构造函数,一个带有服务器名称,另一个带有服务器名称和端口。通常,没有必要更改服务器的端口。
  /// <summary>
  /// Initializes a new instance of the <see cref="TFTPClient"/> class.
  /// </summary>
  /// <param name="server">The server.</param>
  public TFTPClient(string server)
     : this(server, 69) {
  }
  /// <summary>
  /// Initializes a new instance of the <see cref="TFTPClient"/> class.
  /// </summary>
  /// <param name="server">The server.</param>
  /// <param name="port">The port.</param>
  public TFTPClient(string server, int port) {
    Server = server;
    Port = port;
  }
Get函数是TFTPClient的第一个公共函数。该函数设计为线程安全的。因此,它需要为每个请求创建自己的新IPEndPoint和Socket。
 public void Get(string remoteFile, string localFile, Modes tftpMode) {
   int len = 0;
   int packetNr = 1;
   byte[] sndBuffer = CreateRequestPacket(Opcodes.Read, remoteFile, tftpMode);
   byte[] rcvBuffer = new byte[516];
   BinaryWriter fileStream = new BinaryWriter(new FileStream(localFile, 
       FileMode.Create, FileAccess.Write, FileShare.Read));
   IPHostEntry hostEntry = Dns.GetHostEntry(tftpServer);
   IPEndPoint serverEP = new IPEndPoint(hostEntry.AddressList[0], tftpPort);
   EndPoint dataEP = (EndPoint)serverEP;
   Socket tftpSocket = new Socket(serverEP.Address.AddressFamily, 
                                  SocketType.Dgram, ProtocolType.Udp); 
下一步,请求数据包将由一个专用函数创建并发送到套接字。检索响应后,它需要更改我们EndPoint的目标端口,因为需要使用STID。如果数据包丢失,服务器将重发数据包,直到收到确认。此实现将不等待重发;相反,它将超时并抛出SocketException!在这种情况下,用户必须重试。
 // Request and Receive first Data Packet From TFTP Server
 tftpSocket.SendTo(sndBuffer, sndBuffer.Length, SocketFlags.None, serverEP);
 tftpSocket.ReceiveTimeout = 1000 ;
 len = tftpSocket.ReceiveFrom(rcvBuffer, ref dataEP);
            
 // keep track of the TID 
 serverEP.Port = ((IPEndPoint)dataEP).Port;
现在,循环直到接收到的数据包(包括头部)长度小于516字节。检查块号,它可能是重复的数据包。将数据写入目标文件,并使用专用的数据包创建函数发送ACK数据包。
 while (true) {
   // handle any kind of error 
  if (((Opcodes)rcvBuffer[1]) == Opcodes.Error) {
    fileStream.Close();
    tftpSocket.Close();
    throw new TFTPException(
      ((rcvBuffer[2] << 8) & 0xff00) | rcvBuffer[3],
       Encoding.ASCII.GetString(rcvBuffer, 4, rcvBuffer.Length - 5).Trim('\0'));
  }
  // expect the next packet
  if ((((rcvBuffer[2] << 8) & 0xff00) | rcvBuffer[3]) == packetNr) {
     // Store to local file
     fileStream.Write(rcvBuffer, 4, len - 4);
     // Send Ack Packet to TFTP Server
     sndBuffer = CreateAckPacket(packetNr++);
     tftpSocket.SendTo(sndBuffer, sndBuffer.Length, SocketFlags.None, serverEP);
  }
  // Was it the last packet ?
  if (len < 516) {
    break;
  } else {
    // Receive Next Data Packet From TFTP Server
    len = tftpSocket.ReceiveFrom(rcvBuffer, ref dataEP);
  }
}
文件传输完成后,关闭所有句柄并退出函数。
  // Close Socket and release resources
  tftpSocket.Close();
  fileStream.Close();
}
Put函数也遵循相同的过程。唯一不同的是,我们必须发送数据并等待ACK数据包。更多细节请参阅提供的源文件。
请注意,此实现将能够处理来自服务器或客户端的数据包或ACK数据包的丢失。从我的角度来看,这不一定需要,因为TFTP主要在局域网的短距离内使用。因此,应该不会有数据包丢失。
用法
使用起来非常简单。只需创建一个TFTPClient类的实例,并使用它从服务器读取和写入。请记住,目前没有重新编码,因此您应该使用传输模式“octet”,这是默认模式。
   TFTPClient t = new TFTPClient("127.0.0.1");
   t.Put(@"test.zip", @"c:\Temp\MyDemoFileWrite.zip");
   t.Get(@"test.zip", @"c:\temp\MyDemoFileRead.zip");
关注点
好吧,实际上没有。它只是一个解决编码问题的直接解决方案。
待办事项
- 客户端应在超时后重发每个未答复的消息,直到收到确认。
- 处理不同的编码(netasci, octet)-(不确定是否真的需要)。
历史
- 2007.06.22 - 初版。
- 2007.06.24 - 源代码链接已修复。


