Networking
Before we get into the networking classes in .NET, it might be a good idea to define the network. The International Organization for Standardization (ISO) describes the network as having seven layers to it, but as programmers, we're most concerned with the transport layer, where two machines "chat" using the TCP (Transmission Control Protocol) and UDP (User Datagram Protocol) protocols.The primary difference between TCP and UDP is that TCP requires a connection between the two machines. Responses are sent to make sure that every packet of data is received successfully. UDP does not make any promises about whether data lands at its destination, and as such, it is useful for streaming media where a couple of missing packets won't hurt. This grossly simplifies what goes on between two networked machines, but like so many other things in .NET (remember Web services?), a thorough understanding of the underlying activity is not necessary to use the framework's many classes.We can send whatever data we want using these protocols, as long as the other end knows what to expect and how to respond. A number of well-established higher-level protocols have been written to make life easier. You already know about HTTP (HyperText Transfer Protocol), and you've used SMTP (Simple Mail Transfer Protocol) to send mail, FTP (File Transfer Protocol) to transfer files, and DNS (Domain Name Service) to resolve domain names.The .NET Framework has a rich set of classes to manipulate network IO. At the most basic level of TCP, the TcpClient and TcpListener classes, found in System.Net.Sockets, enable us to create a session with a remote machine or listen and wait for an incoming connection. UDP uses the UdpClient class (there is no listener equivalent because UDP doesn't require a connection).The end points of this communication are called sockets. A socket on a computer is defined by the computer's IP address and the port it is using.Think of a port as any of the driveways on your street that lead up to houses. The end of your street is like the network connection, and the driveways are all ports. These ports aren't physical objects in your computer, but they are there as discrete listening "channels." Certain high-level protocols have ports assigned to them where they listen. HTTP uses port 80, and SMTP listens on 25, for example. Often the computer that initiates the connection will listen on any port that isn't already reserved for a particular protocol.At a higher level, the framework provides us with HttpWebResponse and HttpWebRequest to represent HTTP responses and requests and WebClient to send and receive data via HTTP. A number of other classes help us deal with URIs, DNS, authentication, and permissions across a network.The most possibilities lie in the realm of sending and receiving bytes in whatever form we want. For a practical example using NetworkStream and TcpClient, let's look at the exchange of data between a client and an SMTP server when we attempt to send mail.
This is a particularly useful example because the existing mail classes in .NET are actually just wrappers around the old CDO (Collaboration Data Objects). Microsoft has been criticized for these classes that call outside of the managed code of the .NET Framework to an "old" COM object, especially considering that sending mail is a relatively easy thing to accomplish, as you'll see in a moment. |
Listing 17.3. An SMTP exchange
The chatting between the two computers is easy to understand. Every time data is sent to the server, it replies with some message, beginning with a numeric code. As long as the code doesn't start with a 4 or 5, it means that the server was OK with the data we sent to it. The only real mystery might be the period on a line by itself, but that's what we use to signal the end of the mail message, preceded and followed by a carriage return and line feed. (In case you're wondering, you can send a period on a line by itself by making it a double period.)To duplicate this behavior on our page, we'll need to create a TcpClient instance and get a reference to its NetworkStream. Because we'll be writing and reading to the stream over and over, we'll create two methods that do the work for us by taking and returning a string. These methods will be called GeTResponse() and SendRequest(). In our page load event handler, we'll create the exchange and write the server responses to the trace output (the page will need to have TRace enabled). The final code is shown in Listing 17.4.
Server: 220 someservername.com (IMail 8.12 32453-3) NT-ESMTP Server X1
Client: HELO MyServerName
Server: 250 ok
Client: MAIL FROM: <sender@somedomainname.com>
Server: ok sender
Client: RCPT TO: <someuser@somedomainname.com>
Server: 250 ok its for <someuser@somedomainname.com>
Client: DATA
Server: 354 ok, send it; end with <CRLF>.<CRLF>
Client: SUBJECT: Test subject
Client: Body of message
Client: .
Server: 250 Message queued
Client: QUIT
Server: 221 Goodbye.
Listing 17.4. Creating the SMTP exchange in code
C#
VB.NET
using System;
using System.Net.Sockets;
using System.Text;
using System.Web;
public partial class Mailer_aspx
{
public void Page_Load(object sender, EventArgs e)
{
TcpClient client = new TcpClient("someservername.com", 25);
stream = client.GetStream();
SendRequest("HELO MyServerName" + "\r\n");
Trace.Warn(GetResponse());
SendRequest("MAIL FROM: <sender@somedomainname.com>\r\n");
Trace.Warn(GetResponse());
SendRequest("RCPT TO: <someuser@somedomainname.com>\r\n");
Trace.Warn(GetResponse());
SendRequest("DATA\r\n");
Trace.Warn(GetResponse());
SendRequest("Subject: Test subject\r\n");
SendRequest("\r\nBody of the message.");
SendRequest("\r\n.\r\n");
Trace.Warn(GetResponse());
SendRequest("QUIT\r\n");
Trace.Warn(GetResponse());
}
private NetworkStream stream;
private string GetResponse()
{
byte[] readBuffer = new byte[4096];
int length = stream.Read(readBuffer, 0, readBuffer.Length);
string response = Encoding.ASCII.GetString(readBuffer, 0, length);
if (response.StartsWith("4") || response.StartsWith("5"))
{
SendRequest("QUIT\r\n");
throw new Exception("Mailer error: " + response);
}
return response;
}
private void SendRequest(string text)
{
byte[] send = Encoding.ASCII.GetBytes(text);
stream.Write(send, 0, send.Length);
}
}
To kick things off, we need to create an instance of TcpClient and pass in the name of the server we want to talk to, as well as the port number that we expect the server will be listening on. Because port 25 is reserved for SMTP, that's the one we'll use. To get a reference to the TcpClient's NetworkStream, we'll use its GetStream() method and assign the stream to our stream class member so that it's available to our helper methods.Our SendRequest() method takes a string parameter and encodes it as a byte array. That byte array is passed into the NetworkStream's Write() method, using exactly the same method we used to write to a FileStream earlier in the chapter. It's that easy to write to a network stream.Our Getresponse() method processes the data coming back from the server. Reading the data back is a little different from how it was for our FileStream object. We create a byte array that holds 4K (more than enough, in this case) and then use the stream's Read() method to populate that array. The Read() method returns an integer to let us know how many bytes were read. We need that in the next step to turn the byte array into a string we can understand. After we have the string, we can do a little error checking. If the first number of the response is a 4 or 5, it means something went wrong, so we throw an exception.Our Page_Load calls SendRequest() and Getresponse() in sequence after opening the connection. The output to trace will be something like that of the server responses in Listing 17.3.Because this is an ASP.NET book, and a listening server application generally lives outside the context of a Web app, we'll forego a tutorial on using the TcpListener class. An instance of the class waits after its Start() method is called, listening to a specified port for an incoming connection. After it gets a connection, it can begin communicating by accepting a TcpClient via its AcceptTcpClient() method. Generally you'll spawn the interaction with the calling client in a new thread because a server application will interact with several clients simultaneously.
Imports System
Imports System.Net.Sockets
Imports System.Text
Imports System.Web
Public Partial Class Mailer_aspx
Public Sub Page_Load(sender As Object, e As EventArgs)
Dim client As New TcpClient("someservername.com", 25)
stream = client.GetStream()
SendRequest("HELO MyServerName" + CrLf)
Trace.Warn(GetResponse())
SendRequest("MAIL FROM: <sender@somedomainname.com>" + CrLf)
Trace.Warn(GetResponse())
SendRequest("RCPT TO: <someuser@somedomainname.com>" + CrLf)
Trace.Warn(GetResponse())
SendRequest("DATA" + CrLf)
Trace.Warn(GetResponse())
SendRequest("Subject: Test subject" + CrLf)
SendRequest(CrLf + "Body of the message.")
SendRequest(CrLf + "." + CrLf)
Trace.Warn(GetResponse())
SendRequest("QUIT" + CrLf)
Trace.Warn(GetResponse())
End Sub
Private stream As NetworkStream
Private CrLf As String = ControlChars.Cr + ControlChars.Lf
Private Function GetResponse() As String
Dim readBuffer(4096) As Byte
Dim length As Integer = stream.Read(readBuffer,0,readBuffer.Length)
Dim response As String = _
Encoding.ASCII.GetString(readBuffer,0,length)
If response.StartsWith("4") Or response.StartsWith("5") Then
SendRequest("QUIT" + CrLf)
Throw New Exception("Mailer error: " + response)
End If
Return response
End Function
Private Sub SendRequest([text] As String)
Dim send As Byte() = Encoding.ASCII.GetBytes([text])
stream.Write(send, 0, send.Length)
End Sub
End Class