In this article, I begin an implementation of an FTP client in managed code. (The basis for this article was a previously published knowledge base article from Microsoft.com, knowledge base 832670, How to Access a File Transfer Protocol Site by Using Visual Basic .NET. The code is uniquely my own, but some snippets were mined from that article.) While the implementation is not complete, the article is a good starting point that provides enough information to enable you to write a complete implementation.
Why might you want to write an FTP client in the 21st century? Simple: Many of the largest companies in the world are still using 20th century code. Best-in-class businesses use dated technology because their business processes are dependent on legacy systems that would be too costly, too labor-intensive, and too risky to migrate all at once. Thus, many of these companies are bridging the old and new in stages using intermediate techniques like FTP-ing data for batch processing.
At such companies, if a business-to-business transaction takes hours or days, a legacy system is in the way. In fact, if whatever you need cannot be done “while you wait,” data more than likely is being FTP-ed, batched, or evaluated by a person instead of a machine, and manual steps or legacy systems are in the way. Lost luggage, checks being held, and a latency between the application of a loan and its approval are all examples of dated processes and dated software.
Why is this important to know? Simple: Many mega-software systems need to be migrated, and in many instances this migration has only just begun. This makes TCP and socket programming skills useful and valuable, and it also means that much work remains to be done in the field of software development.
Building an FTP Client with .NET
The File Transfer Protocol (FTP) is a TCP protocol that is described by RFC (Request for Comment) 959. RFCs are basically white papers and every RFC that I have searched for has been posted somewhere on the Internet. This article borrows a few snippets from the aforementioned Microsoft.com knowledge base article, as well as knowledge base article 318380 and RFC 959. I encourage you to look these up, if for no other reason than to familiarize yourself with how to find them.
What you won’t find in this article is low-level TCP programming or low-level socket or RS232 (serial port) programming. The .NET framework makes many of these skills superfluous unless you are a socket or RS232 programmer.
Writing an FTP client is relatively easy with .NET. To write an FTP client, you use the System.Net.Socket namespace, an instance of the Socket class, and an instance of an IPEndPoint. The rest of the code is simply figuring out what to do with the data the FTP server sends back. Because RFC 959 guides the data an FTP server returns, you also pretty much know what the data should be and what the data means. The hardest—though this word seems almost inappropriate—part then is to translate raw codes and text into meaningful behaviors. This article focuses on converting FTP server data into a useful FTP client library.
Preparation
As is true with a lot of programming, this exercise requires a little prep-work. (If you are a Windows pro, you can skip to the section “Implementing the FTP Client.”) I prefer to test everything possible on my workstation and the same is true with an FTP client.
The word client implies that there is a server, but you don’t need to write an FTP server. You need to just install the one that ships with Windows. Installing the Microsoft FTP server on your workstation and then turning it on is all you need to do to prepare, write, and test your client.
First, verify that the FTP server is installed on your workstation. To do this, follow either one of the following set of steps:
- Open a command prompt.
- Type FTP and hit enter. This starts the ftp.exe client that ships with Windows.
- At the FTP prompt, type open localhost and hit enter. Localhost refers to the loopback IP address 127.0.0.1, which is your machine.
- If you get a Connected response (see Figure 1), you have the FTP service running on your PC.
Figure 1: An FTP Service Is Running on Your PC If You Get This Response.
The following are the alternate steps for verifying an FTP service:
- If you aren’t an old DOS user, open an instance of Internet Explorer and type ftp://localhost in the Address bar.
- If you see some files or don’t receive an error, the FTP service is running (see Figure 2; I placed the file shown in the figure there intentionally; your PC may not have any files.)
Figure 2: Use Internet Explorer to See If the FTP Service Is Running on Your PC.
The default FTP folder is c:wwwrootftproot. You can locate this folder and add some files to experiment with if you’d like.
If you receive an error, the service may be installed but not running. To check to see whether the service is installed and stopped, follow these steps:
- Click Start.
- Right-click on My Computer.
- Click Manage.
- In the Computer Management Console, expand Services and Applications, expand Internet Information Services, and click on FTP Sites.
- If there is a Default FTP Site (or other sites), the service is installed. Check the status on the right and see what the FTP service’s state is (see to Figure 3).
Figure 3: The Computer Management Console Will Show a Running FTP Site If the Service Is Installed and Running.
The state you want is Running. If the Default FTP Site is present but its state is Stopped or Paused, click Default FTP Site, right-click, and click Play. If the service is not installed, you need to install it. (By default, the FTP service is no longer installed because it can open security holes, but you are a cowboy developer unafraid of hackers.)
To install the FTP service, follow these steps:
- Select Start|Control Panel|Add or Remove Programs|Add/Remove Windows Components. This will start Windows setup. (I am using Windows XP, but the steps should be similar on other versions of Windows.)
- In the Windows Component Wizard, find Internet Information Services, select it, and click Details.
- Find the File Transfer Protocol Service and check it.
- Click OK to close this dialogue and Next to install the FTP service.
The last step installs the FTP Service. If the Windows setup files have been copied from your CD, the FTP Service will be installed from your hard drive. If not, you are prompted for your Windows CD-ROM. That’s it.
You are now ready to write and test the FTP client.
Implementing the FTP Client
Now that you have verified that we have an FTP server to test your client with, you can begin writing the FTP client. The partial client in this example involves creating a socket and an IPEndPoint, connecting to and disconnecting from the server, and authenticating. You can implement everything else, such as obtaining a file list or transferring files, by building on the example here. (In an upcoming article, I will implement a Windows Forms presentation layer and implement additional FTP features.)
Connecting to an FTP Server
To connect to an FTP server, you need to know three things:
- The IP Address or URL of the host
- The port
- The protocol
FTP is a TCP service (that’s your protocol). FTP uses port 21 for command and port 20 for data (Port 21 is your port). Because you are testing on your own workstation, your IP address is 127.0.0.1 (equivalent to the URL localhost).
Note: In a commercial quality implementation, you would probably externalize the FTP configuration data. You can do this using a Web.config or App.config file and the appSettings section, or you can write an IConfigSectionHandler and create your own .config block. Using the <appSettings> is good enough, but using an IConfigSectionHandler is much cooler. Refer to my previous article, “Objectify an XML Node with an IConfigSectionHandler,” for an example of implementing a custom XML section handler.
Knowing which server you want to connect to leaves just a few lines of code to write. In addition to writing the Connect behavior, you need to import some namespaces to make referring to sockets and endpoints shorter, and you can add fields and constructors to hold information about the server to which you would like to connect. Listing 1 contains the class stub, imports statement, a constructor, and the Connect method.
Listing 1: The Class Stub and Connect Method.
Imports System Imports System.Net Imports System.IO Imports System.Text Imports System.Text.ASCIIEncoding Imports System.Net.Sockets Imports System.Configuration Imports System.Resources Public Class FtpClient Private FRemoteHost As String = "127.0.0.1" Private FRemotePath As String = "." Private FRemoteUser As String = "anonymous" Private FRemotePassword As String = "anonymous@nowhere.com" Private FRemotePort As Integer = 21 Private clientSocket As Socket = Nothing Private response As String Public Sub New() End Sub Public Sub New(ByVal remoteHost As String, ByVal remotePath _ As String, _ ByVal remoteUser As String, ByVal remotePassword As String, _ ByVal remotePort As Integer) FRemoteHost = remoteHost FRemotePath = remotePath FRemoteUser = remoteUser FRemotePassword = remotePassword FRemotePort = remotePort End Sub Public Sub Connect() clientSocket = New Socket(AddressFamily.InterNetwork, _ SocketType.Stream, ProtocolType.Tcp) Dim endpoint As IPEndPoint = _ New IPEndPoint(Dns.Resolve(FRemoteHost).AddressList(0), _ FRemotePort) Try clientSocket.Connect(endpoint) Catch ex As Exception Throw New IOException("Connect failed", ex) End Try ReadResponse() End Sub
You can look up information about each of the imported namespaces in the help documentation. The fields represent information about the server, an instance of the Socket class, and a field to hold data you received from the FTP server. The parameterized constructor (Sub New) initializes the server fields. You also provide default values for testing. (If you implemented an IconfigSectionHandler, then you could code the default constructor to read the server fields from a .config file.)
The method you want is Connect. Connect creates an instance of a socket. The parameters you use are needed for FTP, but you can use the same socket class to connect using other protocols like UDP (User Datagram Protocol) or IPX (Internetwork Packet Exchange). (UDP is commonly used in games to send data without waiting for a response, and IPX is an older Novell netware protocol that is also commonly used in older multiplayer games and with Novell networks.)
Next, you need an IPEndPoint, which represents an IP address and port. (All of this information is easily accessible in the integrated help documentation and online, so I will leave it to you to explore further.)
Finally, you call Socket.Connect, passing the endpoint. If you receive an exception, something went wrong. Otherwise, you are ready to start sending and receiving data, which you implement as ReadResponse in your FTP client.
Reading Server Response
For each interaction with the server, you get a response. The response comes back in the form of a string. In general, the string contains a three-digit code and some text, and for FTP requests such as GET you might get some data such as the requested file. You implement ReadResponse to try to read a complete response 512 bytes at a time and convert this response into an event.
ReadResponse is implemented as a method that invokes a second method called ReadLine and then broadcasts the response to any listeners. Listeners are instances of event handlers. Because the data from the server may come back in multi-line responses, ReadLine can recur. Add the three methods in Listing 2 to the FTPClient class from Listing 1.
Listing 2: Reading Responses from the FTP Server
Private Sub ReadResponse() Dim reply As String = ReadLine() 'Figure out the response and raise that event BroadcastResponse(reply) End Sub Private Function ReadLine() As String Return ReadLine(False) End Function Private Function ReadLine(ByVal clearResponse As Boolean) _ As String Const EndLine As Char = "n" Const BUFFER_SIZE As Integer = 512 Dim data As String = "" Dim buffer(BUFFER_SIZE) As Byte Dim bytesRead As Integer = 0 If (clearResponse = True) Then response = String.Empty While (True) Array.Clear(buffer, 0, BUFFER_SIZE) bytesRead = clientSocket.Receive(buffer, _ buffer.Length, 0) data += ASCII.GetString(buffer, 0, bytesRead) If (bytesRead < buffer.Length) Then Exit While End While Dim parts() As String = data.Split(EndLine) If (parts.Length > 2) Then response = parts(parts.Length - 2) Else response = parts(0) End If If (response.Substring(3, 1).Equals(" ") = False) Then Return ReadLine(False) End If Return response End Function
When you constructed the Socket, you initialized it as a two-way byte stream with the argument SocketType.Stream. The main read loop—the While..End While loop—receives up to 512 bytes at a time. If the bytes read are less than 512, you stop reading. If more bytes are present, indicated loosely by the somewhat arbitrary presence of a space, we recurse; otherwise, we return the response to ReadResponse. ReadResponse calls a method named BroadcastResponse.
Converting a Text Response into a Typed Event
To make the client easier to use, BroadcastResponse converts the arguments into a typed event object and raises an event. Remember, because you are writing a client, you don’t know what the user may want to do with the data—you just know what the data means. This is an ideal situation for a delegate (also known as an event).
Listing 3 shows BroadcastResponse. Because BroadcastResponse reads the FTP code from the server and raises an event, you need to define these event types (as delegates) and the event argument classes that will contain the desired data. Listing 4 contains the complete listing for the FTP Client with the events you have elected to implement, useful constants, delegates, and event argument classes. (The complete listing is provided for convenience).
Listing 3: The Implementation of BroadcastResponse
Private Sub BroadcastResponse(ByVal reply As String) ' Permit handling as raw data RaiseEvent OnRawDataReceivedEvent(Me, _ New RawDataReceivedEventArgs(reply)) Dim code As Integer = Int32.Parse(reply.Substring(0, 3)) Dim data As String = reply.Substring(4) ' Permit special handling as code and data RaiseEvent OnTcpEvent(Me, New TcpEventArgs(code, data)) ' Specific handling based on code Select Case code Case SENDING_DATA_PORT_20 Case COMMAND_NOT_IMPLEMENTED Case CONNECTED RaiseEvent OnConnectionEvent(Me, _ New ConnectionEventArgs(True, code, data)) Case AUTHENTICATED RaiseEvent OnAuthentication(Me, _ New AuthenticationEventArgs(True, code, data)) Case NEED_PASSWORD Case AUTHENTICATION_FAILED RaiseEvent OnAuthentication(Me, _ New AuthenticationEventArgs(False, code, data)) End Select End Sub
Listing 4: The entire listing for the FTP client includinged delegates, event fields, and event agrument classes.
Imports System Imports System.Net Imports System.IO Imports System.Text Imports System.Text.ASCIIEncoding Imports System.Net.Sockets Imports System.Configuration Imports System.Resources Public Class FtpClient Public Shared ReadOnly SENDING_DATA_PORT_20 As Integer = 150 Public Shared ReadOnly COMMAND_NOT_IMPLEMENTED As Integer = 202 Public Shared ReadOnly CONNECTED As Integer = 220 Public Shared ReadOnly AUTHENTICATED As Integer = 230 Public Shared ReadOnly NEED_PASSWORD As Integer = 331 Public Shared ReadOnly AUTHENTICATION_FAILED As Integer = 530 Private FRemoteHost As String = "127.0.0.1" Private FRemotePath As String = "." Private FRemoteUser As String = "anonymous" Private FRemotePassword As String = "anonymous@nowhere.com" Private FRemotePort As Integer = 21 Private clientSocket As Socket = Nothing Private response As String Public Event OnAuthentication As AuthenticationEvent Public Event OnConnectionEvent As ConnectionEvent Public Event OnRawDataReceivedEvent As RawDataReceivedEvent Public Event OnTcpEvent As TcpEvent Public Sub New() End Sub Public Sub New(ByVal remoteHost As String, _ ByVal remotePath As String, _ ByVal remoteUser As String, ByVal remotePassword As String, _ ByVal remotePort As Integer) FRemoteHost = remoteHost FRemotePath = remotePath FRemoteUser = remoteUser FRemotePassword = remotePassword FRemotePort = remotePort End Sub Public Sub Connect() clientSocket = New Socket(AddressFamily.InterNetwork, _ SocketType.Stream, ProtocolType.Tcp) Dim endpoint As IPEndPoint = _ New IPEndPoint(Dns.Resolve(FRemoteHost).AddressList(0), _ FRemotePort) Try clientSocket.Connect(endpoint) Catch ex As Exception Throw New IOException("Connect failed", ex) End Try ReadResponse() End Sub Public Sub Disconnect() If (clientSocket Is Nothing = False) Then clientSocket.Disconnect(True) RaiseEvent OnConnectionEvent(Me, _ New ConnectionEventArgs(False)) End If End Sub Private Sub ReadResponse() Dim reply As String = ReadLine() 'Figure out the response and raise that event BroadcastResponse(reply) End Sub Private Function ReadLine() As String Return ReadLine(False) End Function Private Function ReadLine(ByVal clearResponse As Boolean) _ As String Const EndLine As Char = "n" Const BUFFER_SIZE As Integer = 512 Dim data As String = "" Dim buffer(BUFFER_SIZE) As Byte Dim bytesRead As Integer = 0 If (clearResponse = True) Then response = String.Empty While (True) Array.Clear(buffer, 0, BUFFER_SIZE) bytesRead = clientSocket.Receive(buffer, buffer.Length, 0) data += ASCII.GetString(buffer, 0, bytesRead) If (bytesRead < buffer.Length) Then Exit While End While Dim parts() As String = data.Split(EndLine) If (parts.Length > 2) Then response = parts(parts.Length - 2) Else response = parts(0) End If If (response.Substring(3, 1).Equals(" ") = False) Then Return ReadLine(False) End If Return response End Function Private Sub BroadcastResponse(ByVal reply As String) ' Permit handling as raw data RaiseEvent OnRawDataReceivedEvent(Me, _ New RawDataReceivedEventArgs(reply)) Dim code As Integer = Int32.Parse(reply.Substring(0, 3)) Dim data As String = reply.Substring(4) ' Permit special handling as code and data RaiseEvent OnTcpEvent(Me, New TcpEventArgs(code, data)) ' Specific handling based on code Select Case code Case SENDING_DATA_PORT_20 Case COMMAND_NOT_IMPLEMENTED Case CONNECTED RaiseEvent OnConnectionEvent(Me, _ New ConnectionEventArgs(True, code, data)) Case AUTHENTICATED RaiseEvent OnAuthentication(Me, New _ AuthenticationEventArgs(True, code, data)) Case NEED_PASSWORD Case AUTHENTICATION_FAILED RaiseEvent OnAuthentication(Me, _ New AuthenticationEventArgs(False, code, data)) End Select End Sub Public Sub Login() SendCommand("USER " + FRemoteUser) SendCommand("PASS " + FRemotePassword) End Sub Public Sub Login(ByVal user As String, ByVal password As String) FRemotePassword = password FRemoteUser = user Login() End Sub Private Sub SendCommand(ByVal command As String) command += Environment.NewLine Dim commandBytes() As Byte = ASCII.GetBytes(command) clientSocket.Send(commandBytes, commandBytes.Length, 0) ReadResponse() End Sub End Class Public Class TcpEventArgs Private FCode As Integer Private FData As String Public Sub New(ByVal code As Integer, ByVal data As String) FCode = code FData = data End Sub Public ReadOnly Property Code() As Integer Get Return FCode End Get End Property Public ReadOnly Property Data() As String Get Return FData End Get End Property End Class Public Class ConnectionEventArgs Inherits TcpEventArgs Private FConnected As Boolean = False Public Sub New(ByVal connected As Boolean) MyBase.New(-1, "") FConnected = connected End Sub Public Sub New(ByVal connected As Boolean, _ ByVal code As Integer, ByVal data As String) MyBase.New(code, data) FConnected = connected End Sub Public ReadOnly Property Connected() As Boolean Get Return FConnected End Get End Property End Class Public Class AuthenticationEventArgs Inherits TcpEventArgs Private FAuthenticated As Boolean = False Public Sub New(ByVal authenticated As Boolean) MyBase.New(-1, "") FAuthenticated = authenticated End Sub Public Sub New(ByVal authenticated As Boolean, _ ByVal code As Integer, ByVal data As String) MyBase.New(code, data) FAuthenticated = authenticated End Sub Public ReadOnly Property Authenticated() As Boolean Get Return FAuthenticated End Get End Property End Class Public Class RawDataReceivedEventArgs Inherits TcpEventArgs Private FRawData As String Public Sub New(ByVal rawData As String) MyBase.New(-1, "") FRawData = rawData End Sub Public Sub New(ByVal rawData As String, _ ByVal code As Integer, ByVal data As String) MyBase.New(code, data) FRawData = rawData End Sub Public ReadOnly Property RawData() As String Get Return FRawData End Get End Property End Class Public Delegate Sub AuthenticationEvent(ByVal sender As Object, _ ByVal e As AuthenticationEventArgs) Public Delegate Sub ConnectionEvent(ByVal sender As Object, _ ByVal e As ConnectionEventArgs) Public Delegate Sub RawDataReceivedEvent(ByVal sender As Object, _ ByVal e As RawDataReceivedEventArgs) Public Delegate Sub TcpEvent(ByVal sender As Object, _ ByVal e As TcpEventArgs)
BroadcastResponse works the same for all responses: basically, I create an EventArgs-based class for the kind of arguments I think the user might want and a delegate that uses that type. The FtpClient exposes a public event and consumers can subscribe to the events they want. You can use the same technique to support the remaining FTP behaviors.
If you are unfamiliar with delegates and events then you may have to practice a little. A delegate is like a C or C++ function pointer. In .NET, a delegate is a special class that encapsulates a multicast delegate or a list of function pointers, and it is the mechanism that supports attaching event handlers to events. (This is an instance where the framework uses the Observer design pattern.)
Testing the FTP Client
Next, you need to test the client. A simple test is creating an instance of the client, attaching some event handlers to the FtpClient’s public events, and invoking some methods. A great way to test a class library like the FtpClient is with NUnit, but I use a simple console application here (see Listing 5).
Listing 5: A Console Application That Tests Some Capabilities of the FtpClient
Imports Ftp Module Module1 Sub Main() Dim client As FtpClient = New FtpClient() AddHandler client.OnConnectionEvent, _ AddressOf OnConnection AddHandler client.onRawDataReceivedEvent, _ AddressOf OnRawDataReceived AddHandler client.OnAuthentication, _ AddressOf OnAuthenticated client.Connect() client.Login() Console.WriteLine("press enter to exit") Console.ReadLine() client.Disconnect() End Sub Public Sub OnConnection(ByVal sender As Object, _ ByVal e As ConnectionEventArgs) Console.WriteLine("Connected: " & e.Connected.ToString) End Sub Public Sub OnRawDataReceived(ByVal sender As Object, _ ByVal e As RawDataReceivedEventArgs) Console.WriteLine("Received: " & e.RawData) End Sub Public Sub OnAuthenticated(ByVal sender As Object, _ ByVal e As AuthenticationEventArgs) Console.WriteLine("Logged in: " + e.Authenticated.ToString()) End Sub End Module
Before running the console application, make sure you add a reference to the FtpClient class library and import the FTP namespace.
The sample test program tests and responds to the OnConnectionEvent, OnRawDataReceivedEvent, OnAuthenticationEvent, and invokes the Connect, Login, and Disconnect methods. If everything is working, running the console application should produce output that looks something like Figure 4.
Figure 4: Output from Our Test Console Application
The next step is implementing more of the features you need. Not all implementations need every FTP feature. For example, a general-purpose FTP client might need most of the FTP commands, but simply uploading data as part of a process may need only the ability to change directories and upload a file.
Finally, for commercial or private business implementations, additional tests are helpful. For example, you might need to add exception handlers to respond to a timeout, an unavailable server or folder, or unexpected failures. The devil (and the time) are always in the details.
Writing Connected Software Simply
This article provides a lot of juicy but pretty straightforward code. The most important thing to note is how relatively easy it is to use sockets in .NET. Create a socket, an IPEndPoint, know a little bit about the data you will be receiving from the other end point, and you are well on your way to writing connected software.
This article exemplifies the most important thing about great frameworks: They make programming complex tasks much easier. Without needing to be an RS232, TCP, or sockets expert, one can get down to the business of coding a connected solution at a higher level of abstraction. Ultimately, the .NET framework brings complex solutions—whether in TCP or sockets programming, XML Web services, or advanced graphics programming—within everyone’s grasp.
Biography
Paul Kimmel is the VB Today columnist, has written several books on .NET programming, and is a software architect. You may contact him at pkimmel@softconcepts.com if you need assistance developing software or are interested in joining the Lansing Area .NET Users Group (glugnet.org).
Copyright © 2004 by Paul Kimmel. All Rights Reserved.