Network Programming

Overview

The need for computers and devices to communicate across a network is one of the key ingredients of enterprise programming. In its relentless goal to simplify programming, the .NET Framework includes a slew of new networking classes that are logical, efficient, and consistent.

The only drawback to networking with .NET is that no single dominant model exists. In this chapter, you'll learn how to manage network interaction using sockets (recipes 8.8 to 8.11), but you won't learn about two higher-level distributed programming frameworks—Web Services and .NET Remoting—which have their own dedicated chapters later in this book. Typically, socket-based network programming is ideal for closed systems that don't require interoperability, where developers want to have complete flexibility to tailor communication and control the data before it hits the wire.

Of course, this chapter doesn't concentrate exclusively on socket programming. You'll also learn about Web interaction, such as downloading a Web page from the Internet (recipe 8.5) or a single piece of information (recipe 8.6). You'll also learn how to retrieve Web connectivity information for the current computer, look up Internet Protocol (IP) addresses and domain names, and ping another computer to gauge its response time. At the end of this chapter, two recipes (8.13 and 8.14) show how you can build on the Transmission Control Protocol (TCP) classes included with .NET to work with higher-level protocols such as Post Office Protocol 3 (POP3) for e-mail and File Transfer Protocol (FTP) for transferring files.


Get Web Connectivity Information for the Current Computer

Problem

You need to determine programmatically if the current computer can connect to the Internet.

Solution

Use the Microsoft Windows API function InternetGetConnectedState.

Discussion

The InternetGetConnectedState function returns True if the current computer is configured to access the Internet. It also returns a dwFlags parameter that specifies the type of connection using one (or more) of a series of constants.

The following Console application defines the InternetGetConnectedState function and uses it to test the current computer's connectivity:

Public Module GetInternetState
 
 ' Declare the API function.
 Private Declare Function InternetGetConnectedState Lib "wininet" _
 (ByRef dwFlags As Long, ByVal dwReserved As Long) As Long
 
 ' Define the possible types of connections.
 Private Enum ConnectStates
 LAN = &H2
 Modem = &H1
 Proxy = &H4
 Offline = &H20
 Configured = &H40
 RasInstalled = &H10
 End Enum
 
 Public Sub Main()
 ' Get the connected status.
 Dim dwFlags As Long
 Dim Connected As Boolean = _
 (InternetGetConnectedState(dwFlags, 0&) <> 0)
 
 If Connected Then
 Console.WriteLine("This computer is connected to the Internet.")
 
 ' Display all connection flags.
 Console.Write("Connection flags:")
 Dim ConnectionType As ConnectStates
 For Each ConnectionType In _
 System.Enum.GetValues(GetType(ConnectStates))
 If (ConnectionType And dwFlags) = ConnectionType Then
 Console.Write(" " & ConnectionType.ToString())
 End If
 Next
 End If
 
 Console.ReadLine()
 End Sub
 
End Module

A sample output is shown here:

This computer is connected to the Internet.
Connection flags: LAN

Notice that the InternetGetConnectedState reflects how the computer is configured. It doesn't reflect whether the computer is configured correctly (in other words, whether the Internet connection is actually working).


Get the IP Address of the Current Computer

Problem

You want to retrieve the IP address of the current computer, perhaps to use later in networking code.

Solution

Use the System.Net.Dns class, which provides shared GetHostName and GetHostByName methods.

Discussion

The Dns class provides domain name resolution services. You can invoke its GetHostName to retrieve the host name for the current computer. You can then translate the host name into an IP address using GetHostByName.

Dim HostName As String
Dim IPAddress As String
 
' Look up the host name and IP address.
HostName = System.Net.Dns.GetHostName()
IPAddress = System.Net.Dns.GetHostByName(HostName).AddressList(0).ToString()
 
Console.WriteLine("Host name:" & HostName)
Console.WriteLine("IP address:" & IPAddress)

Be aware that the GetHostByName method returns a list of usable IP addresses. In most cases, this address list will contain only one entry.

If you run this code, you'll see something like this:

Host name: fariamat
IP address: 24.114.131.70


Look Up a Host Name for an IP Address

Problem

You want to determine the IP address for a computer based on its domain name by performing a Domain Name System (DNS) query.

Solution

Use the System.Net.Dns class, which wraps this functionality in the GetHostByName method.

Discussion

On the Web, publicly accessible IP addresses are often mapped to host names that are easier to remember using a network of DNS servers, which are a fundamental part of the Internet backbone. To perform a DNS lookup, the computer might contact its cache or a DNS sever (which might in turn forward the request to a DNS root server).

This entire process is transparent if you use the System.Net.Dns class, which allows you to retrieve the IP address for a host name by calling GetHostByName. Here's how you might retrieve the list of IP addresses mapped to http:// www.yahoo.com:

Dim IP As System.Net.IPAddress
 
For Each IP In System.Net.Dns.GetHostByName("www.yahoo.com").AddressList
 Console.WriteLine(IP.AddressFamily.ToString())
 Console.WriteLine(IP.ToString())
Next


Ping an IP Address

Problem

You want to check if a computer is online and gauge its response time.

Solution

Send a ping message.

Discussion

A ping message contacts a device at a specific IP address, sends a test message, and requests that the remote device respond by echoing back the packet. You can measure the time taken for a ping response to be received to gauge the connection latency between two computers.

Despite the simplicity of ping messages compared to other types of network communication, implementing a ping utility in .NET requires a significant amount of complex low-level networking code. The .NET class library doesn't have a prebuilt solution—instead, you must use raw sockets.

However, at least one developer has solved the ping problem. Lance Olson, a developer at Microsoft, has provided C# code that allows you to ping a host by name or IP address and measure the milliseconds taken for a response. This code has been adapted into a PingUtility component, which is available with the code in this book's sample files.

To use the ping utility, you must first add a reference to the PingUtility.dll assembly. You can then use the shared Pinger.GetPingTime method with an IP address or domain name. The GetPingTime method returns the number of milliseconds that elapse before a response is received.

Console.WriteLine("Milliseconds to contact www.yahoo.com: " & _
 PingUtility.Pinger.GetPingTime("www.yahoo.com"))
Console.WriteLine("Milliseconds to contact www.seti.org: " & _
 PingUtility.Pinger.GetPingTime("www.seti.org"))
Console.WriteLine("Milliseconds to contact the local computer: " & _
 PingUtility.Pinger.GetPingTime("127.0.0.1"))

The ping test allows you to verify that other computers are online. It can also be useful if your application needs to evaluate several different remote computers that provide the same content and to determine which one will offer the lowest network latency for communication.

  Note

A ping attempt might not succeed if a firewall forbids it. For example, many heavily trafficked sites ignore ping requests because they're wary of being swamped by a flood of simultaneous pings that will tie up the server (in essence, a denial of service attack).


Download a File Using HTTP

Problem

You want to retrieve a file from the Web.

Solution

Use the HttpWebRequest class to create your request, the WebResponse class to retrieve the response from the Web server, and some form of reader (typically a StreamReader for HTML or text data or a BinaryReader for a binary file) to parse the response data.

Discussion

Downloading a file from the Web takes the following four basic steps:

  1. Use the shared Create method of the System.Net.WebRequest class to specify the page you want. This method returns a WebRequest-derived object, depending on the type of Uniform Resource Identifier (URI) you use. For example, if you use an HTTP URI (with the scheme http://), it will create an HttpWebRequest instance. If you use a file system URI (with the scheme file://), it will create a FileWebRequest instance.
  2. Use the GetResponse method of the HttpWebRequest object to return a WebResponse object for the page.
  3. Create a StreamReader or BinaryReader for the WebResponse stream.
  4. Perform any steps you need to with the stream, such as writing it to a file.

The following code is a test application that retrieves and displays the HTML of a Web page. For it to work, you must import both the System.Net and the System.IO namespaces.

Public Module DownloadTest
 
 Public Sub Main()
 Dim Url As String = "http://www.prosetech.com/index.html"
 
 ' Create the request.
 Dim PageRequest As HttpWebRequest = _
 CType(WebRequest.Create(Url), HttpWebRequest)
 
 ' Get the response.
 ' This takes the most significant amount of time, particularly
 ' if the file is large, because the whole response is retrieved.
 Dim PageResponse As WebResponse = PageRequest.GetResponse()
 Console.WriteLine("Response received.")
 
 ' Read the response stream.
 Dim r As New StreamReader(PageResponse.GetResponseStream())
 Dim Page As String = r.ReadToEnd()
 r.Close()
 
 ' Display the retrieved data.
 Console.Write(Page)
 
 Console.ReadLine()
 End Sub
 
End Module

To deal efficiently with large files that need to be downloaded from the Web, you might want to use asynchronous techniques, as described in Chapter 7. You can also use the WebRequest.BeginGetResponse, which doesn't block your code and calls a callback procedure when the response has been retrieved.


Retrieve a Single Piece of Information from a Web Page

Problem

You want to extract a single piece of information from a Web page.

Solution

Use the WebResponse class to retrieve the stream, copy it to a string, and use a regular expression.

Discussion

You can extract information from a Web stream in several ways. You could read through the stream, use methods of the String class such as IndexOf, or apply a regular expression. The latter of these—using a regular expression—is the most flexible and powerful.

The first step is to create a regular expression that filters out the information you need. Recipe 1.17 provides several examples and a reference to basic regular expression syntax. For example, most Web pages include a text title that is stored in a

tag. To retrieve this piece of information, you use the following regular expression:


 

(?.*?)

This expression retrieves all the text between the opening and closing

tag and places it in a named group called match. The following sample code uses this regular expression to retrieve the title from a URL the user enters. It requires three namespace imports: System.Net, System.IO, and System.Text. RegularExpressions.

Public Module ExtractTitleTest
 
 Public Sub Main()
 Console.WriteLine("Enter a URL, and press Enter.")
 Console.Write(">")
 Dim Url As String = Console.ReadLine()
 
 Dim Page As String
 Try
 ' Create the request.
 Dim PageRequest As HttpWebRequest = _
 CType(WebRequest.Create(Url), HttpWebRequest)
 
 ' Get the response.
 ' This takes the most significant amount of time, particularly
 ' if the file is large, because the whole response is retrieved.
 Dim PageResponse As WebResponse = PageRequest.GetResponse()
 Console.WriteLine("Response received.")
 
 ' Read the response stream.
 Dim r As New StreamReader(PageResponse.GetResponseStream())
 Page = r.ReadToEnd()
 r.Close()
 
 Catch Err As Exception
 Console.WriteLine(Err.ToString())
 Return
 End Try
 
 ' Define the regular expression.
 Dim TitlePattern As String = "

(?.*?)

" Dim TitleRegex As New Regex(TitlePattern, _ RegexOptions.IgnoreCase Or RegexOptions.Singleline) ' Find the title. Dim TitleMatch As Match = TitleRegex.Match(Page) ' Display the title. If TitleMatch.Success Then Console.WriteLine("Found title: " & _ TitleMatch.Groups("match").Value) End If Console.ReadLine() End Sub End Module

Here's the output for a test run that retrieves the title from the Yahoo! search engine:

Enter a URL, and press Enter.
>http://yahoo.com
Response received.
Found title: Yahoo!

If the Web page is extremely large, this approach might not be efficient because the entire stream is copied to a string in memory. Another option is to read through the stream character-by-character and try to build up a match to a search pattern. This approach requires more custom code and is demonstrated in detail with text searching in a file in recipe 5.8.

  Note

Screen scraping solutions such as this one can be quite brittle. If the user interface for the Web site changes and the expected pattern is altered, you'll no longer be able to extract the information you need. If you have control over the Web site, you can implement a much more robust approach using a Web service to return the desired information. Web services also support the full set of basic data types, which prevents another possible source of errors.


Find All Links in a Web Page

Problem

You want to retrieve all the hyperlinks in a Web page (perhaps because you want to download those pages also).

Solution

Retrieve the page using WebResponse, and use a regular expression to search for URIs.

Discussion

Retrieving links in a Web page is conceptually quite easy but often more difficult in practice. The problem is that Web pages follow a semi-standardized format and tolerate a great deal of variance. For example, a hyperlink can be added in the href attribute of an anchor, the onclick attribute of a JavaScript element such as a button, and so on. The URI itself could be relative (in which case it needs to be interpreted relative to the current page), fully qualified (in which case it can have one of countless schemes, including http:// or file:// or mailto://), or it might just be a bookmark (an anchor tag with an href that starts with the # character). Dealing with these myriad possibilities isn't easy.

The first step is to craft a suitable regular expression. In this case, we'll consider only the links that are provided in the href attribute of an anchor tag. Here's one regular expression that retrieves all href values from a Web page:

hrefs*=s*(?:"(?[^"]*)"|(?S+))

Another option is to retrieve absolute paths only. The following line of code is a slightly less complicated regular expression that matches href values that start with http://.

hrefs*=s*"(?http://.*?)"

The following sample application uses the first option. It then manually checks the retrieved URIs to see if they are bookmarks (in which case they are discarded) and to determine if they're relative or absolute. If the bookmarks are relative paths, the System.Uri class is used with the current page Uri to transform them into fully qualified paths.

Public Module ExtractURITest
 
 Public Sub Main()
 Console.WriteLine("Enter a URL, and press Enter.")
 Console.Write(">")
 Dim Url As String = Console.ReadLine()
 
 Dim BaseUri As Uri
 Dim Page As String
 Try
 BaseUri = New Uri(Url)
 
 ' Create the request.
 Dim PageRequest As HttpWebRequest = _
 CType(WebRequest.Create(Url), HttpWebRequest)
 
 ' Get the response.
 ' This takes the most significant amount of time, particularly
 ' if the file is large, because the whole response is retrieved.
 Dim PageResponse As WebResponse = PageRequest.GetResponse()
 Console.WriteLine("Response received.")
 
 ' Read the response stream.
 Dim r As New StreamReader(PageResponse.GetResponseStream())
 Page = r.ReadToEnd()
 r.Close()
 
 Catch Err As Exception
 Console.WriteLine(Err.ToString())
 Console.ReadLine()
 Return
 End Try
 
 ' Define the regular expression.
 Dim HrefPattern As String
 HrefPattern = "hrefs*=s*(?:""(?[^""]*)""|(?S+))"
 
 Dim HrefRegex As New Regex(HrefPattern, _
 RegexOptions.IgnoreCase Or RegexOptions.Compiled)
 
 ' Find and display all the href matches.
 Dim HrefMatch As Match = HrefRegex.Match(Page)
 Do While HrefMatch.Success
 Dim Link As String = HrefMatch.Groups(1).Value
 
 If Link.Substring(0, 1) = "#" Then
 ' Ignore this match, it was just a bookmark.
 Else
 ' Attempt to determine if this is a fully-qualified link
 ' by comparing it against some known schemes.
 Dim Absolute As Boolean = False
 If Link.Length > 8 Then
 Dim Scheme As String
 Scheme = Uri.UriSchemeHttp & "://"
 If Link.Substring(0, Scheme.Length) = Scheme Then _
 Absolute = True
 Scheme = Uri.UriSchemeHttps & "://"
 If Link.Substring(0, Scheme.Length) = Scheme Then _
 Absolute = True
 Scheme = Uri.UriSchemeFile & "://"
 If Link.Substring(0, Scheme.Length) = Scheme Then _
 Absolute = True
 End If
 
 ' (You could compare it against additional schemes here.)
 
 If Absolute Then
 Console.WriteLine(Link)
 Else
 Console.WriteLine(New Uri(BaseUri, Link).ToString())
 End If
 End If
 HrefMatch = HrefMatch.NextMatch()
 Loop
 
 Console.ReadLine()
 End Sub
 
End Module

This code investigates each URI by comparing it against a few common schemes. Another approach would be to try to instantiate a new System.Uri instance using the retrieved URI string. If the string is not an absolute path, an error would occur. You could catch the resulting exception and respond accordingly.

Here's the partial output for a sample test:

Enter a URL, and press Enter.
>http://www.nytimes.com
Response received.
http://www.nytimes.com/pages/jobs/index.html
http://www.nytimes.com/pages/realestate/index.html
http://www.nytimes.com/pages/automobiles/index.html
http://www.nytimes.com/pages/world/index.html
http://www.nytimes.com/pages/national/index.html
http://www.nytimes.com/pages/politics/index.html
http://www.nytimes.com/pages/business/index.html
http://www.nytimes.com/pages/technology/index.html
http://www.nytimes.com/pages/science/index.html
...


Communicate Using TCP

Problem

You need to send data between two computers on a network using a TCP/IP connection.

Solution

Use the TcpClient and TcpListener classes.

Discussion

TCP is a reliable, connection-based protocol that allows two computers to communicate over a network. It provides built-in flow-control, sequencing, and error handling, which makes it very reliable and easy to program with.

To create a TCP connection, one computer must act as the server and start listening on a specific endpoint. (An endpoint is defined as an IP address, which identifies the computer and port number.) The other computer must act as a client and send a connection request to the endpoint where the first computer is listening. Once the connection is established, the two computers can take turns exchanging messages. .NET makes this process easy through its stream abstraction. Both computers simply write to and read from a NetworkStream to transmit data.

  Note

Even though a TCP connection always requires a server and a client, there's no reason an individual application can't be both. For example, in a peer-to-peer application, one thread is dedicated to listening for incoming requests (acting as a server) while another thread is dedicated to initiate outgoing connections (acting as a client).

Once a TCP connection is established, the two computers can send any type of data by writing it to the NetworkStream. However, it's a good idea to begin designing a networked application by defining constants that represent the allowable commands. Doing so ensures that your application code doesn't need to hardcode communication strings.

Public Class ServerMessages
 
 Public Const AcknowledgeOK As String = "OK"
 Public Const AcknowledgeCancel As String = "Cancel"
 Public Const Disconnect As String = "Bye"
 
End Class
 
Public Class ClientMessages
 
 Public Const RequestConnect As String = "Hello"
 Public Const Disconnect As String = "Bye"
 
End Class

In this example, the defined vocabulary is very basic. You would add more constants depending on the type of application. For example, in a file transfer application, you might include a client message for requesting a file. The server might then respond with an acknowledgment and return file details such as the file size. These constants must be compiled into a separate class library assembly, which must be referenced by both the client and server. Both the client and the server will also need to import the System.Net, System.Net.Sockets, and System.IO namespaces.

The following code is a template for a basic TCP server. It listens at a fixed port, accepts the first incoming connection, and then waits for the client to request a disconnect. At this point, the server could call the AcceptTcpClient method again to wait for the next client, but instead, it simply shuts down.

Public Module TcpServerTest
 
 Public Sub Main()
 ' Create a new listener on port 8000.
 Dim Listener As New TcpListener(8000)
 
 Console.WriteLine("About to initialize port.")
 Listener.Start()
 
 Console.WriteLine("Listening for a connection...")
 
 Try
 ' Wait for a connection request, 
 ' and return a TcpClient initialized for communication. 
 Dim Client As TcpClient = Listener.AcceptTcpClient()
 Console.WriteLine("Connection accepted.")
 
 ' Retrieve the network stream.
 Dim Stream As NetworkStream = Client.GetStream()
 
 ' Create a BinaryWriter for writing to the stream.
 Dim w As New BinaryWriter(Stream)
 
 ' Create a BinaryReader for reading from the stream.
 Dim r As New BinaryReader(Stream)
 
 If r.ReadString() = ClientMessages.RequestConnect Then
 w.Write(ServerMessages.AcknowledgeOK)
 Console.WriteLine("Connection completed.")
 Do
 Loop Until r.ReadString() = ClientMessages.Disconnect
 Console.WriteLine()
 Console.WriteLine("Disconnect request received.")
 w.Write(ServerMessages.Disconnect)
 Else
 Console.WriteLine("Could not complete connection.")
 End If
 
 ' Close the connection socket.
 Client.Close()
 Console.WriteLine("Connection closed.")
 
 ' Close the underlying socket (stop listening for new requests).
 Listener.Stop()
 Console.WriteLine("Listener stopped.")
 
 Catch Err As Exception
 Console.WriteLine(Err.ToString())
 End Try
 
 Console.ReadLine()
 End Sub
 
End Module

The following code is a template for a basic TCP client. It contacts the server at the specified IP address and port. In this example, the loopback address (127.0.0.1) is used, which always points to the current computer. Keep in mind that a TCP connection requires two ports: one at the server end, and one at the client end. However, only the server port needs to be specified. The client port can be chosen dynamically at runtime from the available ports, which is what the TcpClient class will do by default.

Public Module TcpClientTest
 
 Public Sub Main()
 Dim Client As New TcpClient()
 Try
 Console.WriteLine("Attempting to connect to the server " & _
 "on port 8000.")
 Client.Connect(IPAddress.Parse("127.0.0.1"), 8000)
 Console.WriteLine("Connection established.")
 
 ' Retrieve the network stream.
 Dim Stream As NetworkStream = Client.GetStream()
 
 ' Create a BinaryWriter for writing to the stream.
 Dim w As New BinaryWriter(Stream)
 
 ' Create a BinaryReader for reading from the stream.
 Dim r As New BinaryReader(Stream)
 
 ' Start a dialogue.
 w.Write(ClientMessages.RequestConnect)
 If r.ReadString() = ServerMessages.AcknowledgeOK Then
 Console.WriteLine("Connected.")
 Console.WriteLine("Press Enter to disconnect.")
 Console.ReadLine()
 Console.WriteLine("Disconnecting...")
 w.Write(ClientMessages.Disconnect)
 Else
 Console.WriteLine("Connection not completed.")
 End If
 
 ' Close the connection socket.
 Client.Close()
 Console.WriteLine("Port closed.")
 
 Catch Err As Exception
 Console.WriteLine(Err.ToString())
 End Try
 
 Console.ReadLine()
 End Sub
 
End Module

Here's a sample connection transcript on the server side:

About to initialize port.
Listening for a connection...
Connection accepted.
Connection completed.
 
Disconnect request received.
Connection closed.
Listener stopped.

And here's a sample connection transcript on the client side:

Attempting to connect to the server on port 8000.
Connection established.
Connected.
Press Enter to disconnect.
 
Disconnecting...
Port closed.


Create a Multithreaded TCP Network Server

Problem

You want to create a TCP server that can simultaneously handle multiple TCP clients.

Solution

Every time a new client connects, start a new thread to handle the request.

Discussion

A single TCP endpoint (IP address and port) can serve multiple connections. In fact, the operating system takes care of most of the work for you. All you need to do is launch a worker object on the server that will handle the connection on a new thread.

For example, consider the basic TCP client and server classes shown in recipe 8.8. You can convert the server into a multithreaded server that supports multiple simultaneous connections quite easily. First create a class that will interact with an individual client:

Public Class ClientHandler
 
 Private Client As TcpClient
 Private ID As String
 
 Public Sub New(ByVal client As TcpClient, ByVal ID As String)
 Me.Client = client
 Me.ID = ID
 End Sub
 
 Public Sub Start()
 ' Retrieve the network stream.
 Dim Stream As NetworkStream = Client.GetStream()
 
 ' Create a BinaryWriter for writing to the stream.
 Dim w As New BinaryWriter(Stream)
 
 ' Create a BinaryReader for reading from the stream.
 Dim r As New BinaryReader(Stream)
 
 If r.ReadString() = ClientMessages.RequestConnect Then
 w.Write(ServerMessages.AcknowledgeOK)
 Console.WriteLine(ID & ": Connection completed.")
 Do
 Loop Until r.ReadString() = ClientMessages.Disconnect
 Console.WriteLine(ID & ": Disconnect request received.")
 w.Write(ServerMessages.Disconnect)
 Else
 Console.WriteLine(ID & ": Could not complete connection.")
 End If
 
 ' Close the connection socket.
 Client.Close()
 Console.WriteLine(ID & ": Client connection closed.")
 
 Console.ReadLine()
 End Sub
 
End Class

Next modify the server code so that it loops continuously, creating new ClientHandler instances as required and launching them on new threads. Here's the revised code:

Dim ClientNum As Integer
Do
 Try
 ' Wait for a connection request, 
 ' and return a TcpClient initialized for communication. 
 Dim Client As TcpClient = Listener.AcceptTcpClient()
 Console.WriteLine("Server: Connection accepted.")
 
 ' Create a new object to handle this connection.
 ClientNum += 1
 Dim Handler As New ClientHandler(Client, "Client " & _
 ClientNum.ToString())
 
 ' Start this object working on another thread.
 Dim HandlerThread As New System.Threading.Thread( _
 AddressOf Handler.Start)
 HandlerThread.IsBackground = True
 HandlerThread.Start()
 
 ' (You could also add the Handler and HandlerThread to
 ' a collection to track client sessions.)
 
 Catch Err As Exception
 Console.WriteLine(Err.ToString())
 End Try
Loop

The following code shows the server-side transcript of a session with two clients:

Server: About to initialize port.
Server: Listening for a connection...
Server: Connection accepted.
Client 1: Connection completed.
Server: Connection accepted.
Client 2: Connection completed.
Client 2: Disconnect request received.
Client 2: Client connection closed.
Client 1: Disconnect request received.
Client 1: Client connection closed.

You might want to add additional code to the network server so that it tracks the current worker objects in a collection. Doing so would allow the server to abort these tasks if it needs to shut down and enforce a maximum number of simultaneous clients. For more information, see the multithreading recipes in Chapter 7.


Communicate Using UDP

Problem

You need to send data between two computers on a network using a User Datagram Protocol (UDP) stream.

Solution

Use the UdpClient class, and use two threads: one to send data, and the other to receive it.

Discussion

UDP is a connectionless protocol that doesn't include any flow control or error checking. Unlike TCP, UDP shouldn't be used where communication is critical. However, because of its lower overhead, UDP is often used for "chatty" applications where it's acceptable to lose some messages. For example, imagine you want to create a network where individual clients send information about the current temperature at their location to a server every few minutes. You might use UDP in this case because the communication frequency is high and the damage caused by losing a packet is trivial (because the server can just continue to use the last received temperature reading).

The UDP template application shown in the following code uses two threads: one to receive messages, and one to send them. To test this application, load two instances at the same time. On computer A, specify the IP address for computer B. On computer B, specify the address for computer A. You can then send text messages back and forth at will. (You can simulate a test on a single computer by using two different ports and the loopback IP address 127.0.0.1.)

Public Module UdpTest
 
 Private LocalPort As Integer
 
 Public Sub Main()
 ' Define endpoint where messages are sent.
 Console.WriteLine("Connect to IP: ")
 Dim IP As String = Console.ReadLine()
 Dim Port As Integer = 8800
 Dim RemoteEndPoint As New IPEndPoint(IPAddress.Parse(IP), _
 Port)
 
 ' Define local endpoint (where messages are received).
 LocalPort = 8800
 
 ' Create a new thread for receiving incoming messages.
 Dim ReceiveThread As New System.Threading.Thread( _
 AddressOf ReceiveData)
 ReceiveThread.IsBackground = True
 ReceiveThread.Start()
 
 Dim Client As New UdpClient()
 
 Try
 Dim Text As String
 Do
 Text = Console.ReadLine()
 
 ' Send the text to the remote client.
 If Text <> "" Then
 ' Encode the data to binary using UTF8 encoding.
 Dim Data() As Byte = _
 System.Text.Encoding.UTF8.GetBytes(Text)
 
 ' Send the text to the remote client.
 Client.Send(Data, Data.Length, RemoteEndPoint)
 End If
 Loop Until Text = ""
 
 Catch Err As Exception
 Console.WriteLine(Err.ToString())
 End Try
 
 Console.ReadLine()
 End Sub
 
 Private Sub ReceiveData()
 Dim Client As New UdpClient(LocalPort)
 Do
 Try
 ' Receive bytes.
 Dim Data() As Byte = Client.Receive(Nothing)
 
 ' Convert bytes to text using UTF8 encoding.
 Dim Text As String = System.Text.Encoding.UTF8.GetString(Data)
 
 ' Display the retrieved text.
 Console.WriteLine(">> " & Text)
 
 Catch Err As Exception
 Console.WriteLine(Err.ToString())
 End Try
 Loop
 End Sub
 
End Module

Note that UDP applications cannot use the NetworkStream abstraction that TCP applications can. Instead, they must convert all data to a stream of bytes using an encoding class, as described in recipe 1.15 and recipe 2.18.


Send a Broadcast Message

Problem

You want to send a message to every user on the local subnet.

Solution

Use the UdpClient class with the appropriate broadcast address.

Discussion

Broadcasts are network messages that are forwarded to all the devices on a local subnet. When a broadcast message is received, the client decides whether to discard (depending on whether any application is monitoring the appropriate port). Broadcasts can't travel beyond a subnet because routers block all broadcast messages. Otherwise, the network could be swamped in traffic.

To send a broadcast message, you use a broadcast IP address, which is the IP address that identifies the network and has all the host bits set to 1. For example, if the network is identified by the first three bytes (140.80.0), the broadcast address would be 140.80.0.255. Alternatively, you can set all bits to 1 (the address 255.255.255.255), which specifies the entire network. In this case, the broadcast message will still travel only inside the local subnet because routers won't allow it to pass.

Dim IP As String = "255.255.255.255"
Dim Port As Integer = 8800
 
Dim RemoteEndPoint As New IPEndPoint(IPAddress.Parse(IP), _
 Port)
 
Dim Client As New UdpClient()
Dim Data() As Byte = System.Text.Encoding.UTF8.GetBytes("Broadcast Message")
 
' Send the broadcast message.
Client.Send(Data, Data.Length, RemoteEndPoint)


Send E Mail Through SMTP

Problem

You want to send an e-mail address using a Simple Mail Transfer Protocol (SMTP) server.

Solution

Use the SmtpMail and MailMessage classes in the System.Web.Mail namespace.

Discussion

The classes in the System.Web.Mail namespace provide a bare-bones wrapper for the Collaboration Data Objects for Windows 2000 (CDOSYS) component. They allow you to compose and send formatted e-mail messages using SMTP.

Using these types is easy. You simply create a MailMessage object, specify the sender and recipient e-mail address, and place the message content in the Body property:

Dim MyMessage As New MailMessage()
MyMessage.To = "someone@somewhere.com"
MyMessage.From = "me@somewhere.com"
MyMessage.Subject = "Hello"
MyMessage.Priority = MailPriority.High
MyMessage.Body = "This is the message!"

If you want, you can send an HTML message by changing the message format and using HTML tags:

MyMessage.BodyFormat = MailFormat.Html
MyMessage.Body = "

" & _ "

This is the message!

"

You can even add file attachments using the MailMessage.Attachments collection and the MailAttachment class.

Dim MyAttachment As New MailAttachment("c:mypic.gif")
MyMessage.Attachments.Add(MyAttachment) 

To send the message, you simply specify the SMTP server name and call the SmtpMail.Send method.

SmtpMail.SmtpServer = "test.mailserver.com"
SmtpMail.Send(MyMessage)

However, there is a significant catch to using the SmtpMail class to send an e-mail message. This class requires a local SMTP server or relay server on your network. In addition, the SmtpMail class doesn't support authentication, so if your SMTP server requires a username and password, you won't be able to send any mail. To overcome these problems, you can use the CDOSYS component directly through COM Interop (assuming you have a server version of Windows or Microsoft Exchange). Alternatively, you might want to access Microsoft Outlook using COM Automation, as described in Chapter 19 (see recipe 19.6).

Here's an example that uses the CDOSYS component to deliver SMTP using a server that requires authentication:

Dim MyMessage As New CDO.Message()
Dim Config As New CDO.Configuration()
 
' Specify the configuration.
Config.Fields(cdoSendUsingMethod) = cdoSendUsingPort
Config.Fields(cdoSMTPServer) = "test.mailserver.com"
Config.Fields(cdoSMTPServerPort) = 25
Config.Fields(cdoSMTPAuthenticate) = cdoBasic
Config.Fields(cdoSendUserName) = "username"
Config.Fields(cdoSendPassword) = "password"
 
' Update the configuration.
Config.Fields.Update()
MyMessage.Configuration = Config
 
' Create the message.
MyMessage.To = "someone@somewhere.com"
MyMessage.From = "me@somewhere.com"
MyMessage.Subject = "Hello"
MyMessage.TextBody = "This is the message!"
 
' Send the CDOSYS Message
MyMessage.Send()
  Note

Note that the SMTP protocol can't be used to retrieve e-mail. For this task, you need the POP3 (as described in recipe 8.13) or IMAP protocol.

For more information about using and configuring your own SMTP server, you can refer to the Microsoft introduction to Internet e-mail and mail servers at http://www.microsoft.com/TechNet/prodtechnol/iis/deploy/config/mail.asp.


Retrieve E Mail Through POP3

Problem

You want to retrieve messages from a POP3 mail server.

Solution

Create a dedicated class that sends POP3 commands over a TCP connection.

Discussion

POP3 is a common e-mail protocol used to download messages from a mail server. POP3, like many Internet protocols, defines a small set of commands that are sent as simple ASCII-encoded messages over a TCP connection (typically on port 110).

Here's a listing of a typical POP3 dialogue, starting immediately after the client makes a TCP connection:

Server sends:+OK <22689.1039100760@mail.prosetech.com>
Client sends:USER 
Server sends:+OK
Client sends:PASS 
Server sends:+OK
Client sends:LIST
Server sends:+OK
Server sends:
Clients sends:RETR 
Server sends:+OK
Server sends:
Client sends:QUIT
Server sends:+OK

To add this functionality to your .NET applications, you can create a Pop3Client class that encapsulates all the logic for communicating with a POP3 server. Your application can then retrieve information about messages using the Pop3Client class. We'll explore this class piece by piece in this recipe. To see the complete code, download the recipes for this chapter in this book's sample files.

The first step is to define a basic skeleton for the Pop3Client class. It should store a TcpClient instance as a member variable, which will be used to send all network messages. You can also add generic Send and ReceiveResponse messages, which translate data from binary form into the ASCII encoding used for POP3 communication.

Public Class Pop3Client
 Inherits System.ComponentModel.Component
 
 ' The internal TCP connection.
 Private Client As New TcpClient()
 Private Stream As NetworkStream
 
 ' The connection state.
 Private _Connected As Boolean = False
 Public ReadOnly Property Connected() As Boolean
 Get
 Return _Connected
 End Get
 End Property
 
 Private Sub Send(ByVal message As String)
 ' Send the command in ASCII format.
 Dim MessageBytes() As Byte = Encoding.ASCII.GetBytes(message)
 Stream.Write(MessageBytes, 0, MessageBytes.Length)
 
 Debug.WriteLine(message)
 End Sub
 
 Private Function GetResponse() As String
 ' Build up the response string until the line termination
 ' character is found.
 Dim Character, Response As String
 Do
 Character = Chr(Stream.ReadByte()).ToString()
 Response &= Character
 Loop Until Character = Chr(13)
 
 Response = Response.Trim(New Char() {Chr(13), Chr(10)})
 Debug.WriteLine(Response)
 Return Response
 End Function
 
 ' (Other code omitted.)
 
End Class

You'll notice that the Pop3Client is derived from the Component class. This nicety allows you to add and configure an instance of the Pop3Client class at design time using Microsoft Visual Studio .NET.

You can also add constants for common POP3 commands. One easy way to add constants is to group them in a private class nested inside Pop3Client, as shown here:

' Some command constants.
Private Class Commands
 ' These constants represent client commands.
 Public Const List As String = "LIST" & vbNewLine
 Public Const User As String = "USER "
 Public Const Password As String = "PASS "
 Public Const Delete As String = "DELE "
 Public Const GetMessage As String = "RETR "
 Public Const Quit As String = "QUIT" & vbNewLine
 
 ' These two constants represent server responses.
 Public Const ServerConfirm As String = "+OK"
 Public Const ServerNoMoreData As String = "."
End Class

The next step is to create a basic method for connecting to the POP3 server and disconnecting from it. Because Pop3Client derives from Component, it indirectly implements IDisposable. Therefore, you can also override the Dispose method to ensure that connections are properly cleaned up when the class is disposed.

Public Sub Connect(ByVal serverName As String, ByVal userName As String, _
 ByVal password As String)
 If Connected Then Me.Disconnect()
 
 ' Connect to the POP3 server
 ' (which is almost always at port 110).
 Client.Connect(serverName, 110)
 Stream = Client.GetStream()
 
 ' Check if connection worked.
 CheckResponse(GetResponse())
 
 ' Send user name.
 Send(Commands.User & userName & vbNewLine)
 
 ' Check response.
 CheckResponse(GetResponse())
 
 ' Send password.
 Send(Commands.Password & password & vbNewLine)
 
 ' Check response.
 CheckResponse(GetResponse())
 
 _Connected = True
End Sub
 
Public Sub Disconnect()
 If Connected Then
 Send(Commands.Quit)
 CheckResponse(GetResponse())
 _Connected = False
 End If
End Sub
 
Protected Overloads Overrides Sub Dispose(ByVal disposing As Boolean)
 If disposing Then Disconnect()
 MyBase.Dispose(disposing)
End Sub
  Note

Some mail servers will not allow the password to be transmitted in clear text. In this case, you will need to manually encrypt the password information first using the appropriate algorithm before you submit it to the Pop3Client class.

The Pop3Client class uses a private CheckResponse procedure, which verifies that the server's message is the excepted confirmation and throws an exception if it isn't.

Private Sub CheckResponse(ByVal response As String)
 If Not (response.Substring(0, 3) = Commands.ServerConfirm) Then
 Client.Close()
 _Connected = False
 Throw New ApplicationException("Response " & response & _
 " not expected.")
 End If
End Sub

The only remaining step is to implement three higher-level methods: GetMessageList, which the client calls to retrieve a list of message headers, GetMessageContent, which the client calls to retrieve the body of a single message, and DeleteMessage, which is typically used to remove a message after its content is downloaded.

To support the GetMessageList method, you need to create a simple class for storing message header information, which includes a message number and size in bytes.

Public Class MessageHeader
 
 Private _Number As Integer
 Private _Size As Integer
 
 Public ReadOnly Property Number() As Integer
 Get
 Return _Number
 End Get
 End Property
 
 Public ReadOnly Property Size() As Integer
 Get
 Return _Size
 End Get
 End Property
 
 Public Sub New(ByVal number As Integer, ByVal size As Integer)
 Me._Number = number
 Me._Size = size
 End Sub
 
End Class

The GetMessageList method returns an array of MessageHeader objects. When the server returns a period (.) on a separate line, the list is complete.

Public Function GetMessageList() As MessageHeader()
 If Not Connected Then Throw New _
 InvalidOperationException("Not connected.")
 
 Send(Commands.List)
 CheckResponse(GetResponse())
 
 Dim Messages As New ArrayList()
 Do
 Dim Response As String = GetResponse()
 
 If Response = Commands.ServerNoMoreData Then
 ' No more messages.
 Return CType(Messages.ToArray(GetType(MessageHeader)), _
 MessageHeader())
 Else
 ' Create an EmailMessage object for this message.
 ' Include the header information.
 Dim Values() As String = Response.Split()
 Dim Message As New MessageHeader(Val(Values(0)), Val(Values(1)))
 
 Messages.Add(Message)
 End If
 Loop
End Function

To retrieve the information for a single message, the client calls GetMessageContent with the appropriate message number. The message content includes headers that indicate the sender, recipient, and path taken, along with the message subject, priority, and body. A more sophisticated version of the Pop3Client might parse this information into a class that provides separate properties for these details.

Public Function GetMessageContent(ByVal messageNumber As Integer) As String
 If Not Connected Then Throw New _
 InvalidOperationException("Not connected.")
 
 Send(Commands.GetMessage & messageNumber.ToString() & vbNewLine)
 CheckResponse(GetResponse)
 Dim Line, Content As String
 
 ' Retrieve all message text until the end point.
 Do
 Line = GetResponse()
 If Line = Commands.ServerNoMoreData Then
 Return Content
 Else
 Content &= Line & vbNewLine
 End If
 Loop
End Function

Finally DeleteMessage removes a message from the server based on its message number.

Public Sub DeleteMessage(ByVal messageNumber As Integer)
 If Not Connected Then Throw New _
 InvalidOperationException("Not connected.")
 
 Send(Commands.Delete & messageNumber.ToString() & vbNewLine)
 CheckResponse(GetResponse())
End Sub

You can test the Pop3Client class with a simple program such as the following one, which retrieves all the messages for a given account:

Public Module Pop3Test
 
 Public Sub Main()
 ' Get the connection information.
 Dim Server, Name, Password As String
 Console.Write("POP3 Server: ")
 Server = Console.ReadLine()
 Console.Write("Name: ")
 Name = Console.ReadLine()
 Console.Write("Password: ")
 Password = Console.ReadLine()
 Console.WriteLine()
 
 ' Connect.
 Dim POP3 As New Pop3Client()
 POP3.Connect(Server, Name, Password)
 
 ' Retrieve a list of message, and display the corresponding content.
 Dim Messages() As MessageHeader = POP3.GetMessageList()
 Console.WriteLine(Messages.Length().ToString() & " messages.")
 
 Dim Message As MessageHeader
 For Each Message In Messages
 Console.WriteLine(New String("-"c, 60))
 Console.WriteLine("Message Number: " & Message.Number.ToString())
 Console.WriteLine("Size: " & Message.Size.ToString())
 Console.WriteLine()
 Console.WriteLine(POP3.GetMessageContent(Message.Number))
 Console.WriteLine(New String("-"c, 60))
 Console.WriteLine()
 Next
 
 Console.WriteLine("Press Enter to disconnect.")
 Console.ReadLine()
 POP3.Disconnect()
 End Sub
 
End Module

The output for a typical session is as follows:

POP3 Server: mail.server.com
Name: matthew
Password: opensesame
 
1 messages.
------------------------------------------------------------------------------
Message Number: 1
Size: 1380
 
Return-Path: 
Delivered-To: somewhere.com%someone@somewhere.com
Received: (cpmta 15300 invoked from network); 5 Dec 2002 06:57:13 -0800
Received: from 66.185.86.71 (HELO fep01-mail.bloor.is.net.cable.rogers.com)
 by smtp.c000.snv.cp.net (209.228.32.87) with SMTP; 5 Dec 2002 06:57:13 -0800
X-Received: 5 Dec 2002 14:57:13 GMT
Received: from fariamat ([24.114.131.60])
 by fep01-mail.bloor.is.net.cable.rogers.com
 (InterMail vM.5.01.05.06 201-253-122-126-106-20020509) with ESMTP
 id <20021205145711.SJDZ4718.fep01- mail.bloor.is.net.cable.rogers.com@fariamat>
 for ; Thu, 5 Dec 2002 09:57:11 -0500
Message-ID: <004c01c29c6f$186c5150$3c837218@fariamat>
From: 
To: 
Subject: Test Message
Date: Thu, 5 Dec 2002 10:00:48 -0500
MIME-Version: 1.0
Content-Type: text/plain;
 charset="iso-8859-1"
Content-Transfer-Encoding: 7bit
X-Priority: 3
X-MSMail-Priority: Normal
X-Mailer: Microsoft Outlook Express 6.00.2600.0000
X-MIMEOLE: Produced By Microsoft MimeOLE V6.00.2600.0000
X-Authentication-Info: Submitted using SMTP AUTH LOGIN at fep01-- 
mail.bloor.is.net.cable.rogers.com from [24.114.131.60] at
Thu, 5 Dec 2002 09:57:11 -0500
Status: RO
X-UIDL: Pe9pStHkIFc7yAE
 
Hi! This is a test message!
------------------------------------------------------------------------------
 
Press Enter to disconnect.

You can see the transcript of commands sent and received in the Debug output window in Visual Studio .NET.


Access an FTP Site

Problem

You want to retrieve or upload files from an FTP server.

Solution

Use a third-party component, or create a dedicated class that sends FTP commands over a TCP connection.

Discussion

FTP (File Transfer Protocol) is a common protocol used to upload and download files from a server. FTP, like many Internet protocols, defines a small set of commands that are sent as simple ASCII-encoded messages over a TCP connection (typically on port 21).

The following is a listing of a typical FTP dialogue, starting immediately after the client makes a TCP connection. Notice that every line sent from the server begins with an FTP response code.

Server sends: 220-FTP server ready.
Server sends: 220-<<
Server sends: 220- 
Server sends: 220->>
Server sends: 220 This is a private system - No anonymous login
Client sends: USER 
Server sends: 331 User  OK. Password required
Client sends: PASS 
Server sends: 230-User authenticated.
Client sends: PASV
Server sends: 227 Entering Passive Mode (66,185,95,103,166,76)
Client sends: TYPE I
Server sends: 200 TYPE is now 8-bit binary
Client sends: RETR 
Server sends: 150 Accepted data connection
< file transferred on separate connection >
Server sends: 226-File successfully transferred
Server sends: 226 0.001 seconds (measured here), 2.09 Mbytes per second
Client sends: QUIT
Server sends: 221-Goodbye. You uploaded 0 and downloaded 3 kbytes.

FTP is a fairly detailed protocol, and implementing a successful FTP client is not a trivial task. One challenge is that FTP works in two modes: active and passive. In active mode, the FTP server attempts to transfer files to a client by treating the client as a server. In other words, the client must open a new connection and wait for an incoming server request. This configuration won't work with most firewalls, which is why most FTP use is over passive connections. In passive mode, the server provides a new server connection that the client can connect to for downloading data. To switch to passive mode, you use the PASV command.

The remainder of this recipe presents a bare-bones FTP client that can log on and download files. If you want more sophisticated FTP functionality, such as the ability to browse the directory structure and upload files, you can use a commercial component or you can extend this code. For a reference of valid FTP commands and return codes, refer to http://www.vbip.com/winsock/winsock_ftp_ref_01.asp.

The FtpClient class works on a similar principle to the Pop3Client class in recipe 8.13. The basic outline defines recognized command constants and an internal TCP connection.

Public Class FtpClient
 Inherits System.ComponentModel.Component
 
 ' The internal TCP connection.
 Private Client As New TcpClient()
 Private Stream As NetworkStream
 
 ' The connection state.
 Private _Connected As Boolean = False
 Public ReadOnly Property Connected() As Boolean
 Get
 Return _Connected
 End Get
 End Property
 
 ' Some command constants.
 Private Class Commands
 Public Const User As String = "USER "
 Public Const Password As String = "PASS "
 Public Const Quit As String = "QUIT" & vbNewLine
 Public Const GetFile As String = "RETR "
 Public Const UsePassiveMode As String = "PASV" & vbNewLine
 Public Const UseBinary As String = "TYPE I" & vbNewLine
 Public Const UseAscii As String = "TYPE A" & vbNewLine
 End Class
 
 Private Enum ReturnCodes
 ServiceReady = 220
 Accepted = 200
 PasswordRequired = 331
 UserLoggedIn = 230
 EnteringPassiveMode = 227
 StartingTransferAlreadyOpen = 125
 StartingTransferOpening = 150
 TransferComplete = 226
 End Enum
 
 ' (Other code omitted.)
 
End Class

Next we add private functions for sending and receiving data, as well as a helper function that verifies that a given response begins with an expected return code.

Private Function Send(ByVal message As String) As String
 ' Send the command in ASCII format.
 Dim MessageBytes() As Byte = Encoding.ASCII.GetBytes(message)
 Stream.Write(MessageBytes, 0, MessageBytes.Length)
 
 Debug.WriteLine(message)
 
 ' Return the response code.
 Return GetResponse()
End Function
 
Private Function GetResponse() As String
 ' Retrieve all the available lines.
 Dim Character As String
 Dim Response As String = ""
 
 Do
 Do
 Character = Chr(Stream.ReadByte()).ToString()
 Response &= Character
 Loop Until Character = Chr(10)
 Loop While Stream.DataAvailable
 
 Response = Response.Trim(New Char() {Chr(13), Chr(10)})
 Debug.WriteLine(Response)
 Return Response
End Function
 
Private Function CheckCode(ByVal response As String, _
 ByVal expectedCode As ReturnCodes) As Boolean
 Return Val(response.Substring(0, 3)) = expectedCode
End Function

The next step is to add the basic methods for connecting and disconnecting to an FTP server:

Public Sub Connect(ByVal serverName As String, ByVal userName As String, _
 ByVal password As String)
 If Connected Then Me.Disconnect()
 
 ' Connect to the POP3 server
 ' (which is almost always at port 21).
 Client.Connect(serverName, 21)
 Stream = Client.GetStream()
 
 ' Send user name.
 Dim Response As String
 Response = GetResponse()
 Response = Send(Commands.User & userName & vbNewLine)
 
 If CheckCode(Response, ReturnCodes.PasswordRequired) Then
 ' Send password.
 Response = Send(Commands.Password & password & vbNewLine)
 End If
 
 If Not (CheckCode(Response, ReturnCodes.UserLoggedIn)) _
 And Not (CheckCode(Response, ReturnCodes.ServiceReady)) Then
 Throw New ApplicationException("Could not log in.")
 End If
 
 _Connected = True
End Sub
 
Public Sub Disconnect()
 If Connected Then
 If Not TransferClient Is Nothing Then
 TransferClient.Close()
 End If
 Send(Commands.Quit)
 _Connected = False
 End If
End Sub

The most complicated part of FtpClient is the code needed for downloading files. FtpClient uses passive mode, which means it must use a separate TcpClient instance to download files.

' Second connection for retrieving a file.
Private TransferClient As TcpClient
Private TransferEndpoint As IPEndPoint

The private CreateTransferClient procedure instructs the FTP server to use passive mode, retrieves the new IP address and port that it should use, and initializes the TcpClient object accordingly:

Private Sub CreateTransferClient()
 Dim Response As String = Send(Commands.UsePassiveMode)
 
 If Not CheckCode(Response, ReturnCodes.EnteringPassiveMode) Then
 Throw New ApplicationException("Error entering passive mode.")
 End If
 
 ' The IP address and port number is appended to the response.
 ' Retrieve these details.
 Dim StartPos As Integer = Response.IndexOf("(")
 Dim EndPos As Integer = Response.IndexOf(")")
 Dim IPAndPort As String = Response.Substring(StartPos + 1, _
 EndPos - StartPos - 1)
 Dim IPParts() As String = IPAndPort.Split(","c)
 
 Dim IP As String = IPParts(0) + "." + IPParts(1) + "." + _
 IPParts(2) + "." + IPParts(3)
 Dim Port As Integer = Convert.ToInt32(IPParts(4)) * 256 + _
 Convert.ToInt32(IPParts(5))
 
 ' Create the data transfer connection.
 TransferClient = New TcpClient()
 
 TransferEndpoint = New IPEndPoint(IPAddress.Parse(IP), Port)
End Sub

In addition, the private SetMode procedure sends a message to the server indicating whether the file is binary or ASCII-based.

Private Sub SetMode(ByVal binaryMode As Boolean)
 Dim Response As String
 If binaryMode Then
 Response = Send(Commands.UseBinary)
 Else
 Response = Send(Commands.UseAscii)
 End If
 
 If Not CheckCode(Response, ReturnCodes.Accepted) Then
 Throw New ApplicationException("Could not change mode.")
 End If
End Sub

To download a file, your application calls the DownloadFile method with the filename and a Boolean variable indicating whether binary or ASCII mode should be used. The DownloadFile method uses SetMode and CreateTransferClient accordingly. Once the connection is made, the NetworkStream is returned to the client, who can read from it and close it when complete. Using a stream avoids the overhead of storing the retrieved data in memory (for example, in a byte array), which can be quite enormous if a file several megabytes large is being downloaded.

Public Function DownloadFile(ByVal filename As String, _
 ByVal binaryMode As Boolean) As NetworkStream
 ' Create a connection to the second port in passive mode.
 CreateTransferClient()
 TransferClient.Connect(TransferEndpoint)
 
 SetMode(binaryMode)
 
 Dim Response As String = Send(Commands.GetFile & filename & vbNewLine)
 
 If Not CheckCode(Response, ReturnCodes.StartingTransferAlreadyOpen) _
 And Not (CheckCode(Response, ReturnCodes.StartingTransferOpening)) Then
 Throw New ApplicationException("Could not open connection.")
 End If
 
 ' Let the client read data from the network stream.
 ' This is more efficient that creating and returning an
 ' intermediate byte array, but it also relies on the client
 ' to close the stream.
 Return TransferClient.GetStream()
End Function

The filename can be a complete relative path, as long as you use the forward slash (/) character to separate directories. For example, images/mypic.gif is valid, but imagesmypic.gif is not.

When the transfer is complete, the client must call ConfirmDownloadComplete, which reads the confirmation message from the server and frees it up to serve new requests.

Public Sub ConfirmDownloadComplete()
 Dim Response As String = GetResponse()
 CheckCode(Response, ReturnCodes.TransferComplete)
End Sub

Putting it all together is easy. The following Console application connects to an FTP server and attempts to download a file. You can watch a transcript of all sent and received messages in the Debug window.

Public Module FtpTest
 
 Public Sub Main()
 ' Connect to an FTP server.
 Dim FTP As New FtpClient()
 FTP.Connect("ftp.adobe.com", "anonymous", "me@somewhere.com")
 
 ' Request a file.
 Dim Stream As NetworkStream = FTP.DownloadFile("license.txt", True)
 
 ' Copy the data into file 1K at a time.
 Dim fs As New FileStream("c:license.txt", IO.FileMode.Create)
 Dim BytesRead As Integer
 Do
 Dim Bytes(1024) As Byte
 BytesRead = Stream.Read(Bytes, 0, Bytes.Length)
 fs.Write(Bytes, 0, BytesRead)
 Loop While BytesRead > 0
 
 ' Close the network stream and the file.
 Stream.Close()
 fs.Close()
 
 ' Retrieve the server confirmation message.
 FTP.ConfirmDownloadComplete()
 
 Console.WriteLine("File transfer complete.")
 Console.WriteLine("Press Enter to disconnect.")
 Console.ReadLine()
 
 FTP.Disconnect()
 End Sub
 
End Module




Microsoft Visual Basic. Net Programmer's Cookbook
Microsoft Visual Basic .NET Programmers Cookbook (Pro-Developer)
ISBN: 073561931X
EAN: 2147483647
Year: 2003
Pages: 376

Flylib.com © 2008-2020.
If you may any questions please contact us: flylib@qtcs.net