Microsoft & .NETVisual BasicWrite an FTP Client with VB.NET to Bridge Legacy Software

Write an FTP Client with VB.NET to Bridge Legacy Software

Developer.com content and product recommendations are editorially independent. We may make money when you click on links to our partners. Learn More.

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:

  1. Open a command prompt.
  2. Type FTP and hit enter. This starts the ftp.exe client that ships with Windows.
  3. 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.
  4. If you get a Connected response (see Figure 1), you have the FTP service running on your PC.
  5. 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:

  1. If you aren’t an old DOS user, open an instance of Internet Explorer and type ftp://localhost in the Address bar.
  2. 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.)
  3. 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:

  1. Click Start.
  2. Right-click on My Computer.
  3. Click Manage.
  4. In the Computer Management Console, expand Services and Applications, expand Internet Information Services, and click on FTP Sites.
  5. 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).
  6. 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:

  1. 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.)
  2. In the Windows Component Wizard, find Internet Information Services, select it, and click Details.
  3. Find the File Transfer Protocol Service and check it.
  4. 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:

  1. The IP Address or URL of the host
  2. The port
  3. 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.

Get the Free Newsletter!

Subscribe to Developer Insider for top news, trends & analysis

Latest Posts

Related Stories