65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 C# 的简单 TFTP 客户端

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.71/5 (16投票s)

2007年6月23日

CPOL

6分钟阅读

viewsIcon

203325

downloadIcon

5973

一篇关于使用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的第一个公共函数。该函数设计为线程安全的。因此,它需要为每个请求创建自己的新IPEndPointSocket

 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 - 源代码链接已修复。
© . All rights reserved.