AddThis Social Bookmark Button

Print

Creating an FTP Client in .NET

by Jesse Liberty
05/10/2004

"The .NET framework provides the plumbing, allowing you to concentrate on the application you are building." At least, that's the theory. But as my old boss Pat Johnson used to say, "In theory, theory and practice are the same, but in practice, they never are." When it comes to FTP, I'm afraid the .NET framework has a bit of a gap in the pipes.

Recently, I needed to create an application with programmatic interface to FTP. I expected to turn to the help files and find an FTP class with methods like get and put and so forth. Oops. No such class. Fortunately, the FTP protocol is very simple, and the .NET framework does provide enough of the plumbing to make creating an FTP client very easy.

The Example Program

In the original edition of this article, I wrote my own FTP client code. Because that code was very close to the robust library offered at Enterprise Distributed Technologies, I have decided to rewrite this article walking through their source code to point out how their FTP implementation works. I am told by the author of this library that he used my book Programming C# when writing the library, so this seems like a nice way to close the circle.

To test their library and give us something to work with, I've created a very simple Windows client, as shown in figure 1.


Figure 1

As you can see from the figure, you are free to put in any resolvable host name, along with a port (typically 21 for FTP).

The "Remote File" name is the name under which to store your file on the remote system. It will be stored in the directory shown as "Remote Directory." The "Local file" is the name (and full path) to the file on your local machine. To make life easier, I've added a "Browse" button that opens a file dialog.

The User ID and Password fields are included because this example assumes that your FTP site does not allow anonymous access. The Transfer button begins the transfer of the file to the remote host (and then retrieves the file under a new name back to the local machine) and the Quit button quits the application. The list box in the middle provides logging information so that you can see the progress of your transfer.

Example Program

The test application consists of two classes: FTPClientTester, the Windows form, and a helper class Logger, which allows progress messages to be posted.

The Logger class has only one event, OnLoggableEvent, and a single method, Write. When a class wishes to log information, it calls the Write method, passing in a string to be logged. The Logger class raises the OnLoggableEvent, passing along a LogInfoEventArgs object that has a property called Message that contains the string to be logged. The subscribed class (in this case, FTPClientTester) can then do what it wants with that string (e.g., write it to the list box). The entire code for the Logger class and of its associated EventArgs class is shown in Listing 1.


using System;
public class LogInfoEventArgs : EventArgs
{
  private string message;

  public string Message { get { return message; } }

  public LogInfoEventArgs(string message)
  {
    this.message = message;
  }
}

public class Logger
{
  public delegate void 
    LoggableEventHandler(object o, LogInfoEventArgs e);

  public event LoggableEventHandler OnLoggableEvent = null;

  public void Write(object o, string msg)
  {
    if ( OnLoggableEvent != null )
    {
      LogInfoEventArgs e = new LogInfoEventArgs(msg);
      OnLoggableEvent(o,e);
    }
  }
}

Exercising the FTPClient with the Tester Program

The bulk of the work is done in the btnTransfer_Click method. The first steps are to create a Logger object and to register to receive OnLoggableEvents, which can be displayed in the list box:

void btnTransfer_Click(object sender, EventArgs e)
{
  FTP.Logger logger = new FTP.Logger();
  logger.OnLoggableEvent += new 
         FTP.Logger.LoggableEventHandler(logger_OnLoggableEvent);

Note that the event is handled by the logger_OnLoggableEvent method, which extracts the message from the LogInfoEventArgs object and displays it in the list box:

void logger_OnLoggableEvent(object o, LogInfoEventArgs e)
{
  lbProgress.Items.Add(e.Message);
  lbProgress.SelectedIndex = lbProgress.Items.Count - 1;
  Application.DoEvents();  // force update of the UI
}

The second step in handling the transfer button-click event is to instantiate an instance of FTPClient, passing in the host name

string host = this.txtHost.Text;
com.enterprisedt.net.ftp.FTPClient ftpClient = new 
    com.enterprisedt.net.ftp.FTPClient(host);

You might want to modify the FTP Client (within the licensing restrictions) to the logger in as a parameter to the constructor, so that you can receive loggable events from the library classes. This is left as an exercise for the reader.

The FTP Client

When the FTPClient class is instantiated, a number of private members are initialized (e.g., the timeout value is initialized to 0 and the connectMode is initialized to the enumerated value FTPConnectMode.PASV). In the body of the constructor an FTPControlScocket instance is created, passing in the remoteHost parameter, along with the control port, a StreamWriter (for logging) and the timeout value. You could easily modify the logging class from the FTPClientTester to provide a StreamWriter.


public FTPClient(string remoteHost)
{
	control = new FTPControlSocket(
		remoteHost, 
		FTPControlSocket.CONTROL_PORT, 
		null, 0);
}

You will write to and read from the host using a StreamWriter and StreamREader. To create these, you must first instantiate a NetworkStream, and to do so, you'll use the socket you just created. Connecting a Socket requires an IPEndPoint. That, in turn, requires an IPAddress. You will get the IPAddress from an instance of IPHostEntry, which you will create by resolving the host name passed in to the constructor. Therefore, in order to instantiate the StreamWriter and StreamReader, you will first need to resolve the name of the remote FTP site into an IPHostEntry object.

The job of an IPHostEntry object is to associate a DNS host name with an array of IP addresses. Fortunately, the work of resolving the host name is provided by a static method of the Dns class:

IPHostEntry remoteHostEntry = Dns.Resolve(remoteHost);

The IPHostEntry has a property, AddressList which is an array of IPAddresses


IPAddress[] ipAddresses = remoteHostEntry.AddressList
All of this work is done in the FTPControlSocket constructor,

public FTPControlSocket(string remoteHost, int controlPort, 
						StreamWriter log, int timeout)
{
	// resolve remote host & take first entry
	IPHostEntry remoteHostEntry = Dns.Resolve(remoteHost);
	IPAddress[] ipAddresses = remoteHostEntry.AddressList;

	Initialize(ipAddresses[0], controlPort, log, timeout);
}

In the Initialize method an IPEndPoint is created based on the remote address and the control port

IPEndPoint ipe = new IPEndPoint(remoteAddr, controlPort);

A Socket object is instantiated passing in the appropriate enumerated types, and the socket is connected to the end point

controlSock = 
	new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
Timeout = timeout;
controlSock.Connect(ipe);

The StreamWriter and a Stream Reader are created in the InitStream method of FTPControlSocket

private void InitStreams()
{						
	NetworkStream stream = new NetworkStream(controlSock, true);
	writer = new StreamWriter(stream);
	reader = new StreamReader(stream);
}

When you create the connection you will get a reply from the host. The ReadReply method parses the reply into a string and the ValidateReply method validates that you received the reply you expect (in this case, 220, as proscribed by the ftp protocol specification.

At this point, if no exception has been thrown, you have successfully connected to the host.

The principal methods we'll exercise in the FTPClient include

  • Get (to retrieve a file)
  • Put (to send a file)
  • ChDir (to change directories on the host)
  • Login (to send your name and password)
  • Quit (to break the connection and clean up resources)

You'll start by logging in. You do so by extracting the user name and password from the user interface, and passing both to the Login method of the FTPClient object


string user = this.txtUserID.Text;
string pw = this.txtPassword.Text;
ftpClient.Login(user,pw);

The pattern that you'll see in all host interaction begins here. The FTPClient will send a command to the host (in this case User, passing in the user name) and will validate that it has received receive the expected reply (in this case, 331). Similarly, the FTP client will then pass in the password (note, the password is not encrypted) and verify the expected response.

public void Login(string user, string password)
{			
	string response = control.SendCommand("USER " + user);
	lastValidReply = control.ValidateReply(response, "331");
	response = control.SendCommand("PASS " + password);
	lastValidReply = control.ValidateReply(response, "230");
}

The SendCommand just writes the text you pass to the host using the StreamWriter, and then calls ReadReply to get the reply from the host, which is then validated. Now that you are logged in, you can change directories on the host based on the directory the user has chosen


string newDirectory = this.txtDirectory.Text;
ftpClient.Chdir(newDirectory);

The Chdir method sends the CWD command with the directory ame, and verifies that you get back the expected reply, 250.

public void Chdir(string dir)
{			
	string reply = control.SendCommand("CWD " + dir);
	lastValidReply = control.ValidateReply(reply, "250");
}

At this point you are connected to the right directory on the host, and you can write a file from your local disk to the ftp site,


string localFileName = this.txtLocalFile.Text;
string remoteFileName = this.txtRemoteFile.Text;
ftpClient.Put(localFileName, remoteFileName);

The Put method in the FTPClass calls the PutASCII or PutBinary helper method, passing in the local path to get the file from, the remote file name to write it to, and whether or not to append to or overwrite the file if it already exists (in this case, you'll overwrite). The PutASCII method, for example, creates a StreamREader based on the source stream, and calls the helper method InitPut


private void PutASCII(Stream srcStream, string remoteFile, bool append)
{			
	// need to read line by line ...
	StreamReader reader = new StreamReader(srcStream);
	
	InitPut(remoteFile, append);

InitPut creates a DataSocket based on the connect mode. It then sets up the command (append or store) and sends that command to the host, validating that it receives back either 125 or 150.

private void InitPut(string remoteFile, bool append)
{			
	// set up data channel
	data = control.CreateDataSocket(connectMode);
	
	// send the command to store
	string cmd = append ? "APPE ":"STOR ";
	string reply = control.SendCommand(cmd + remoteFile);
	
	// Can get a 125 or a 150
	string[] validCodes = new string[]{"125", "150"};
	lastValidReply = control.ValidateReply(reply, validCodes);
}

The helper method GetDataStream is used to obtain a network stream associated with the data socket, which in turn is used to initialize a new StreamWriter object.

That object is then used to write the contents of the file to the host.


StreamWriter writer = new StreamWriter(GetDataStream());
string line = null;
while ((line = reader.ReadLine()) != null)
{
	writer.Write(line, 0, line.Length);
	writer.Write(FTPControlSocket.EOL, 0, FTPControlSocket.EOL.Length);
}
reader.Close();
writer.Flush();
writer.Close();

When the file is written, the Put method calls ValidateTransfer which verifies that you've received eitehr the value 226 or 250 from the host, indicating successful transfer of the file.

Once the file is there, we'll get it back, puttiing it into the original location but under a new (hardwired) name,


const string newFileName = "NewRetrievedFile.txt";
ftpClient.Get(newFileName,remoteFileName);

The code implementing the Get method is nearly identical to the put method, calling GetASCII or GetBinary, and validating the transfer once it is comlpete. Finally, when we are done, we call the Quit method on the FTPClient, which sends the Quit command to the host and validates that it gets back either 221 or 226 (valid quit). Whether or not an exception is raised on quitting the host, the Logout method is called on the FTPControlSocket which closes the writer and reader.

Next Steps

A number of options are left as exercises to test your understanding of this code. You might, for example, want to add to the UI to exercise the Rename method of the FTPClient. You'll almost certainly want to add code to ensure that the user has entered valid data in each of the text boxes in the UI. In addition, you might want to explore more of the FTP protocol, and perhaps add drag-and-drop to the user interface.

Most important, I urge you to read through the complete source code. The folks at Enterprise Distributed Technologies have done a very thorough job, and there are boundary conditions that they handle that I've not covered in this article.

For the complete source code as used in this article (including my tester program) please click here, or download the code from my web site.

Jesse Liberty is a senior program manager for Microsoft Silverlight where he is responsible for the creation of tutorials, videos and other content to facilitate the learning and use of Silverlight. Jesse is well known in the industry in part because of his many bestselling books, including O'Reilly Media's Programming .NET 3.5, Programming C# 3.0, Learning ASP.NET with AJAX and the soon to be published Programming Silverlight.


Return to ONDotnet.com