The Microsoft .NET Framework includes a full set of classes for network programming. These classes support low-level network programming tasks like querying the state of network interfaces and socket-based programming with Transmission Control Protocol/Internet Protocol (TCP/IP) to higher-level tasks like downloading files and HTML pages from the Web over Hypertext Transfer Protocol (HTTP). You can even build fully distributed applications using distributed objects or service-oriented approaches.
Included in the release of .NET Framework 3.0 was Windows Communication Foundation (WCF), a unified programming model for building service-oriented applications. Although earlier technologies are still available, WCF is generally seen as a replacement for technologies like .NET Remoting and ASP.NET Web Services, and also provides a flexible unified interface through which to access many other types of distributed communications, like message queues.
The recipes in this chapter describe how to do the following:
Obtain configuration and network statistic information about the network interfaces on a computer as well as detect when network configuration changes occur (recipes 10-1 and 10-2)
Download files from File Transfer Protocol (FTP) and HTTP servers (recipes 10-3, 10-4, and 10-6)
Respond to HTTP requests from within your application (recipe 10-5)
Send e-mail messages with attachments using Simple Mail Transfer Protocol (SMTP) (recipe 10-7)
Use the Domain Name System (DNS) to resolve a host name into an Internet Protocol (IP) address (recipe 10-8)
Ping an IP address to determine whether it is accessible and calculate round-trip communication speeds by sending it an Internet Control Message Protocol (ICMP) Echo
request (recipe 10-9)
Communicate between programs through the direct use of TCP in both synchronous and asynchronous communication models (recipes 10-10 and 10-11)
Communicate using User Datagram Protocol (UDP) datagrams where connection-oriented and reliable TCP represents unnecessary overhead (recipe 10-12)
Create a SOAP-based web service (recipe 10-13)
Generate a WCF service proxy dynamically (recipe 10-14)
Parse the contents of an Atom or RSS feed (recipe 10-15)
Manipulate uniform resource locators (URIs) (recipe 10-16)
A number of the recipes in this chapter include a client and a server component that must both be running for the recipe to work correctly. Where this is the case, the client and server code are contained in separate projects. To run these recipes from within Visual Studio, set the server project as the startup project and run it normally. Once the server is running, right-click the client project in Solution Explorer, click Debug on the context menu, and then click "Start new instance."
You need to obtain information about the network adapters and network configuration of the local machine.
Call the static method GetAllNetworkInterfaces
of the System.Net.NetworkInformation.NetworkInterface
class to get an array of objects derived from the abstract class NetworkInterface
. Each object represents a network interface available on the local machine. Use the members of each NetworkInterface
object to retrieve configuration information and network statistics for that interface.
The System.Net.NetworkInformation
namespace provides easy access to information about network configuration and statistics. The primary means of retrieving network information are the properties and methods of the NetworkInterface
class. You do not instantiate NetworkInterface
objects directly. Instead, you call the static method NetworkInterface.GetAllNetworkInterfaces
, which returns an array of NetworkInterface
objects. Each object represents a single network interface on the local machine. You can then obtain network information and statistics about the interface using the NetworkInterface
members described in Table 10-1.
The System.Net.NetworkInformation.IPGlobalProperties
class also provides access to useful information about the network configuration of the local computer.
Table 10.1. Members of the NetworkInterface Class
Description | |
---|---|
Properties | |
| Gets a |
| Gets a |
| Gets a |
| Gets a |
| Gets a value from the |
| Gets a value from the |
| Gets a |
| Gets a |
Methods | |
| Returns a |
| Returns a |
| Returns a |
| Returns a |
The NetworkInterface
class also provides two other static members that you will find useful:
The static property LoopbackInterfaceIndex
returns an int
identifying the index of the loopback interface within the NetworkInterface
array returned by GetAllNetworkInterfaces
.
The static method GetIsNetworkAvailable
returns a bool
indicating whether any network connection is available—that is, has an OperationalStatus
value of Up
.
The following example uses the members of the NetworkInterface
class to display information about all the network interfaces on the local machine:
using System; using System.Net.NetworkInformation; namespace Apress.VisualCSharpRecipes.Chapter10 { class Recipe10_01 { static void Main() { // Only proceed if there is a network available. if (NetworkInterface.GetIsNetworkAvailable()) { // Get the set of all NetworkInterface objects for the local // machine. NetworkInterface[] interfaces = NetworkInterface.GetAllNetworkInterfaces();
// Iterate through the interfaces and display information. foreach (NetworkInterface ni in interfaces) { // Report basic interface information. Console.WriteLine("Interface Name: {0}", ni.Name); Console.WriteLine(" Description: {0}", ni.Description); Console.WriteLine(" ID: {0}", ni.Id); Console.WriteLine(" Type: {0}", ni.NetworkInterfaceType); Console.WriteLine(" Speed: {0}", ni.Speed); Console.WriteLine(" Status: {0}", ni.OperationalStatus); // Report physical address. Console.WriteLine(" Physical Address: {0}", ni.GetPhysicalAddress().ToString()); // Report network statistics for the interface. Console.WriteLine(" Bytes Sent: {0}", ni.GetIPv4Statistics().BytesSent); Console.WriteLine(" Bytes Received: {0}", ni.GetIPv4Statistics().BytesReceived); // Report IP configuration. Console.WriteLine(" IP Addresses:"); foreach (UnicastIPAddressInformation addr in ni.GetIPProperties().UnicastAddresses) { Console.WriteLine(" - {0} (lease expires {1})", addr.Address, DateTime.Now + new TimeSpan(0, 0, (int)addr.DhcpLeaseLifetime)); } Console.WriteLine(Environment.NewLine); } } else { Console.WriteLine("No network available."); } // Wait to continue. Console.WriteLine(Environment.NewLine); Console.WriteLine("Main method complete. Press Enter"); Console.ReadLine(); } } }
You need a mechanism to check whether changes to the network occur during the life of your application.
Add handlers to the static NetworkAddressChanged
and NetworkAvailabilityChanged
events implemented by the System.Net.NetworkInformation.NetworkChange
class.
The NetworkChange
class provides an easy-to-use mechanism that allows applications to be aware of changes to network addresses and general network availability. This allows your applications to adapt dynamically to the availability and configuration of the network.
The NetworkAvailabilityChanged
event fires when a change occurs to general network availability. An instance of the NetworkAvailabilityChangedEventHandler
delegate is needed to handle this event and is passed a NetworkAvailabilityEventArgs
object when the event fires. The NetworkAvailabilityEventArgs.IsAvailable
property returns a bool
indicating whether the network is available or unavailable following the change.
The NetworkAddressChanged
event fires when the IP address of a network interface changes. An instance of the NetworkAddressChangedEventHandler
delegate is required to handle these events. No event-specific arguments are passed to the event handler, which must call NetworkInterface.GetAllNetworkInterfaces
(discussed in recipe 10-1) to determine what has changed and to take appropriate action.
The NetworkAddressChanged
and NetworkAvailabilityChanged
events work on Windows 2000 and later operating systems.
The following example demonstrates how to use handlers that catch NetworkAddressChanged
and NetworkAvailabilityChanged
events and then display status information to the console. To observe how the code handles changing network conditions, unplug your network cable while the example is running, wait a few seconds, and then plug the cable back in.
using System; using System.Net.NetworkInformation; namespace Apress.VisualCSharpRecipes.Chapter10 { class Recipe10_02 { // Declare a method to handle NetworkAvailabilityChanged events. private static void NetworkAvailabilityChanged( object sender, NetworkAvailabilityEventArgs e) { // Report whether the network is now available or unavailable. if (e.IsAvailable) { Console.WriteLine("Network Available"); } else { Console.WriteLine("Network Unavailable"); } } // Declare a method to handle NetworkAdressChanged events. private static void NetworkAddressChanged(object sender, EventArgs e) { Console.WriteLine("Current IP Addresses:"); // Iterate through the interfaces and display information. foreach (NetworkInterface ni in NetworkInterface.GetAllNetworkInterfaces()) { foreach (UnicastIPAddressInformation addr in ni.GetIPProperties().UnicastAddresses) { Console.WriteLine(" - {0} (lease expires {1})", addr.Address, DateTime.Now + new TimeSpan(0, 0, (int)addr.DhcpLeaseLifetime)); } } } static void Main(string[] args) { // Add the handlers to the NetworkChange events. NetworkChange.NetworkAvailabilityChanged += NetworkAvailabilityChanged; NetworkChange.NetworkAddressChanged += NetworkAddressChanged;
// Wait to continue. Console.WriteLine(Environment.NewLine); Console.WriteLine("Press Enter to stop waiting for network events"); Console.ReadLine(); } } }
The .NET Framework provides several mechanisms for transferring data over the Internet. One of the easiest approaches is to use the System.Net.WebClient
class. WebClient
provides many high-level methods that simplify the transfer of data by specifying the source as a URI; Table 10-2 summarizes them. The URI can specify that a file (file://
), FTP (ftp://
), or HTTP (http://
or https://
) scheme be used to download the resource.
Table 10.2. Data Download Methods of the WebClient Class
Method | Description |
---|---|
| Returns a |
| Same as |
| Returns a |
| Same as |
| Downloads data from a specified URI and saves it to a specified local file. |
| Same as |
| Returns a |
| Same as |
The asynchronous download allows you to download data as a background task using a thread from the thread pool (discussed in recipe 4-1). When the download is finished or fails, the thread calls the appropriate OnXXX
virtual methods that raise the corresponding event on the WebClient
object, which you can handle using a method that matches the signature of the System.ComponentModel.AsyncCompletedEventHandler
delegate if you don't want to derive a type from WebClient
and override the virtual method. However, the WebClient
object can handle only a single concurrent asynchronous download, making a WebClient
object suitable for the background download of large single sets of data but not for the download of many files concurrently. (You could, of course, create multiple WebClient
objects to handle multiple downloads.) You can cancel the outstanding asynchronous download using the method CancelAsync
.
The WebClient
class derives from System.ComponentModel.Component
, so you can add it to the Visual Studio Form Designer Toolbox in order to allow you to easily set the properties or define the event handlers in a Windows Forms–based application.
The following example downloads a specified resource from a URI as a string and, since it is an HTML page, parses it for any fully qualified URLs that refer to GIF files. It then downloads each of these files to the local hard drive.
using System; using System.IO; using System.Net; using System.Text.RegularExpressions; namespace Apress.VisualCSharpRecipes.Chapter10 { class Recipe10_03 {
private static void Main() { // Specify the URI of the resource to parse. string srcUriString = "http://www.apress.com"; Uri srcUri = new Uri(srcUriString); // Create a WebClient to perform the download. WebClient client = new WebClient(); Console.WriteLine("Downloading {0}", srcUri); // Perform the download getting the resource as a string. string str = client.DownloadString(srcUri); // Use a regular expression to extract all HTML <img> // elements and extract the path to any that reference // files with a gif, jpg, or jpeg extension. MatchCollection matches = Regex.Matches(str, "<img.*?src\s*=\s*["'](?<url>.*?\.(gif|jpg|jpeg)).*?>", RegexOptions.Singleline | RegexOptions.IgnoreCase); // Try to download each referenced image file. foreach(Match match in matches) { var urlGrp = match.Groups["url"]; if (urlGrp != null && urlGrp.Success) { Uri imgUri = null; // Determine the source URI. if (urlGrp.Value.StartsWith("http")) { // Absolute imgUri = new Uri(urlGrp.Value); } else if (urlGrp.Value.StartsWith("/")) { // Relative imgUri = new Uri(srcUri, urlGrp.Value); } else { // Skip it. Console.WriteLine("Skipping image {0}", urlGrp.Value); }
if (imgUri != null) { // Determine the local file name to use. string fileName = urlGrp.Value.Substring(urlGrp.Value.LastIndexOf('/')+1); try { // Download and store the file. Console.WriteLine("Downloading {0} to {1}", imgUri.AbsoluteUri, fileName); client.DownloadFile(imgUri, fileName); } catch { Console.WriteLine("Failed to download {0}", imgUri.AbsoluteUri); } } } } // Wait to continue. Console.WriteLine(Environment.NewLine); Console.WriteLine("Main method complete. Press Enter"); Console.ReadLine(); } } }
The regular expression used in the example is simple and is not designed to cater to all possible URL structures. Recipes 2-5 and 2-6 discuss regular expressions.
You may also want to upload data to resources specified as a URI, although this technique is not as commonly used. The WebClient
class also provides methods for performing uploads that are equivalent to the download methods discussed previously:
OpenWrite
OpenWriteAsync
UploadData
UploadDataAsync
UploadFile
UploadFileAsync
UploadString
UploadStringAsync
You need to retrieve a file from a web site, but you do not want or do not have permission to save it directly to the hard drive. Instead, you want to process the data in your application directly in memory.
Use the System.Net.WebRequest
class to create your request, the System.Net.WebResponse
class to retrieve the response from the web server, and some form of reader (typically a System.IO.StreamReader
for HTML or text data or a System.IO.BinaryReader
for a binary file) to parse the response data.
You could also use the OpenRead
method of the System.Net.WebClient
class to open a stream. However, the additional capabilities of the WebRequest
and WebResponse
classes give you more control over the operation of the network request.
Opening and downloading a stream of data from the Web using the WebRequest
and WebResponse
classes takes the following four basic steps:
Use the static method Create
of the WebRequest
class to specify the page you want. This method returns a WebRequest
-derived object, depending on the type of URI you specify. For example, if you use an HTTP URI (with the scheme http://
or https://
), you will create an HttpWebRequest
instance. If you use a file system URI (with the scheme file://
), you will create a FileWebRequest
instance. In the .NET Framework 2.0 and later, you can also use an FTP URL (with the scheme ftp://
), which will create an FtpWebRequest
.
Use the GetResponse
method of the WebRequest
object to return a WebResponse
object for the page. If the request times out, a System.Net.WebException
will be thrown. You can configure the timeout for the network request through the WebRequest.Timeout
property in milliseconds (the default value is 100000).
Create a StreamReader
or a BinaryReader
that wraps the stream returned by the WebResponse.GetResponseStream
method.
Perform any steps you need to with the stream contents.
The following example retrieves and displays a graphic and the HTML content of a web page. Figure 10-1 shows the output.
using System; using System.Net; using System.IO; using System.Drawing; using System.Windows.Forms; namespace Apress.VisualCSharpRecipes.Chapter10 { public partial class Recipe10_04 : Form { public Recipe10_04() { InitializeComponent(); } protected override void OnLoad(EventArgs e) { base.OnLoad(e); string picUri = "http://www.apress.com/img/img05/Hex_RGB4.jpg"; string htmlUri = "http://www.apress.com"; // Create the requests. WebRequest requestPic = WebRequest.Create(picUri); WebRequest requestHtml = WebRequest.Create(htmlUri); // Get the responses. // This takes the most significant amount of time, particularly // if the file is large, because the whole response is retrieved. WebResponse responsePic = requestPic.GetResponse(); WebResponse responseHtml = requestHtml.GetResponse(); // Read the image from the response stream. pictureBox1.Image = Image.FromStream(responsePic.GetResponseStream());
// Read the text from the response stream. using (StreamReader r = new StreamReader(responseHtml.GetResponseStream())) { textBox1.Text = r.ReadToEnd(); } } [STAThread] public static void Main(string[] args) { Application.Run(new Recipe10_04()); } } }
Use the new System.Net.HttpListener
class.
Your application must be running on Windows XP Service Pack 2 (or later) or Windows 2003 to use the HttpListener
class; otherwise, a System.PlatformNotSupportedException
will be thrown when you try to instantiate it. You should check the bool
returned by the static property HttpListener.IsSupported
to check whether support is available.
The HttpListener
class provides an easy-to-use mechanism through which your programs can accept and respond to HTTP requests. To use the HttpListener
class, follow these steps:
Instantiate an HttpListener
object.
Configure the URI prefixes that the HttpListener
object will handle using the Prefixes
property. The Prefixes
property returns a System.Net.HttpListenerPrefixCollection
collection to which you can add URI prefixes (as strings
) using the Add
method. Each prefix must end with a forward slash (/
), or else a System.ArgumentException
will be thrown. If you specify a URL prefix that is already being handled, a System.Net.HttpListenerException
will be thrown. When a client makes a request, the request will be handled by the listener configured with the prefix that most closely matches the client's requested URL.
Start the HttpListener
object by calling its Start
method. You must call Start
before the HttpListener
object can accept and process HTTP requests.
Accept client requests using the GetContext
method of the HttpListener
object. The GetContext
method will block the calling thread until a request is received and then returns a System.Net.HttpListenerContext
object. Alternatively, you can use the BeginGetContext
and EndGetContext
methods to listen for requests on a thread-pool thread. When a request is received, the System.AsyncCallback
delegate specified as the argument to the BeginGetContext
method will be called and passed the HttpListenerContext
object. Regardless of how it is obtained, the HttpListenerContext
object implements three read-only properties critical to the handling of a client request:
The Request
property returns a System.Net.HttpListenerRequest
through which you can access details of the client's request.
The Response
property returns a System.Net.HttpListenerResponse
through which you can configure the response to send to the client.
The User
property returns an instance of a type implementing System.Security.Principal.IPrincipal
, which you can use to obtain identity, authentication, and authorization information about the user associated with the request.
Configure the HTTP response through the members of the HttpListenerResponse
object accessible through the HttpListenerContext.Response
property.
Send the response by calling the Close
method of the HttpListenerResponse
object.
Once you have finished processing HTTP requests, call Stop
on the HttpListener
object to stop accepting more requests. Call Close
to shut down the HttpListener
object, which will wait until all outstanding requests have been processed, or call Abort
to terminate the HttpListener
object without waiting for requests to complete.
The following example demonstrates how to use the HttpListener
class to process HTTP requests. The example starts listening for five requests concurrently using the asynchronous BeginGetContext
method and handles the response to each request by calling the RequestHandler
method. Each time a request is handled, a new call is made to BeginGetContext
so that you always have the capacity to handle up to five requests.
To open a connection to the example from your browser, enter the URL http://localhost:19080/VisualCSharpRecipes/
or http://localhost:20000/Recipe10-05/
, and you will see the response from the appropriate request handler.
using System; using System.IO; using System.Net; using System.Text; using System.Threading; namespace Apress.VisualCSharpRecipes.Chapter10 { class Recipe10_05 { // Configure the maximum number of request that can be // handled concurrently. private static int maxRequestHandlers = 5; // An integer used to assign each HTTP request handler a unique // identifier. private static int requestHandlerID = 0; // The HttpListener is the class that provides all the capabilities // to receive and process HTTP requests. private static HttpListener listener;
// A method to asynchronously process individual requests and send // responses. private static void RequestHandler(IAsyncResult result) { Console.WriteLine("{0}: Activated.", result.AsyncState); try { // Obtain the HttpListenerContext for the new request. HttpListenerContext context = listener.EndGetContext(result); Console.WriteLine("{0}: Processing HTTP Request from {1} ({2}).", result.AsyncState, context.Request.UserHostName, context.Request.RemoteEndPoint); // Build the response using a StreamWriter feeding the // Response.OutputStream. StreamWriter sw = new StreamWriter(context.Response.OutputStream, Encoding.UTF8); sw.WriteLine("<html>"); sw.WriteLine("<head>"); sw.WriteLine("<title>Visual C# Recipes</title>"); sw.WriteLine("</head>"); sw.WriteLine("<body>"); sw.WriteLine("Recipe 10-5: " + result.AsyncState); sw.WriteLine("</body>"); sw.WriteLine("</html>"); sw.Flush(); // Configure the Response. context.Response.ContentType = "text/html"; context.Response.ContentEncoding = Encoding.UTF8; // Close the Response to send it to the client. context.Response.Close(); Console.WriteLine("{0}: Sent HTTP response.", result.AsyncState); } catch (ObjectDisposedException) { Console.WriteLine("{0}: HttpListener disposed--shutting down.", result.AsyncState); } finally {
// Start another handler if unless the HttpListener is closing. if (listener.IsListening) { Console.WriteLine("{0}: Creating new request handler.", result.AsyncState); listener.BeginGetContext(RequestHandler, "RequestHandler_" + Interlocked.Increment(ref requestHandlerID)); } } } public static void Main(string[] args) { // Quit gracefully if this feature is not supported. if (!HttpListener.IsSupported) { Console.WriteLine( "You must be running this example on Windows XP SP2, ", "Windows Server 2003, or higher to create ", "an HttpListener."); return; } // Create the HttpListener. using (listener = new HttpListener()) { // Configure the URI prefixes that will map to the HttpListener. listener.Prefixes.Add( "http://localhost:19080/VisualCSharpRecipes/"); listener.Prefixes.Add( "http://localhost:20000/Recipe10-05/"); // Start the HttpListener before listening for incoming requests. Console.WriteLine("Starting HTTP Server"); listener.Start(); Console.WriteLine("HTTP Server started"); Console.WriteLine(Environment.NewLine); // Create a number of asynchronous request handlers up to // the configurable maximum. Give each a unique identifier. for (int count = 0; count < maxRequestHandlers; count++) { listener.BeginGetContext(RequestHandler, "RequestHandler_" + Interlocked.Increment(ref requestHandlerID)); } // Wait for the user to stop the HttpListener. Console.WriteLine("Press Enter to stop the HTTP Server"); Console.ReadLine();
// Stop accepting new requests. listener.Stop(); // Terminate the HttpListener without processing current requests. listener.Abort(); } // Wait to continue. Console.WriteLine(Environment.NewLine); Console.WriteLine("Main method complete. Press Enter"); Console.ReadLine(); } } }
You need to retrieve a file from a web site, but the web site requires that you provide credentials for the purpose of authentication.
Use the System.Net.WebRequest
and System.Net.WebResponse
classes as described in recipe 10-4. Before making the request, configure the WebRequest.Credentials
and WebRequest.Certificates
properties with the necessary authentication information.
You could also use the System.Net.WebClient
class (discussed in recipe 10-3), which also has Credentials
and Certificates
properties that allow you to associate user credentials with a web request.
Some web sites require user authentication information. When connecting through a browser, this information might be submitted transparently (for example, on a local intranet site that uses Windows integrated authentication), or the browser might request this information with a login dialog box. When accessing a web page programmatically, your code needs to submit this information. The approach you use depends on the type of authentication implemented by the web site:
If the web site is using basic or digest authentication, you can transmit a username and password combination by manually creating a new System.Net.NetworkCredential
object and assigning it to the WebRequest.Credentials
property. With digest authentication, you may also supply a domain name.
If the web site is using Windows integrated authentication, you can take the same approach and manually create a new System.Net.NetworkCredential
object. Alternatively, you can retrieve the current user login information from the System.Net.CredentialCache
object using the DefaultCredentials
property.
If the web site requires a client certificate, you can load the certificate from a file using the System.Security.Cryptography.X509Certificates.X509Certificate2
class and add that to the HttpWebRequest.ClientCertificates
collection.
You can load an X.509 certificate from a certificate store using the class System.Security.Cryptography.X509Certificates.X509Store
defined in the System.Security.dll
assembly. You can either find a certificate in the store programmatically using the X509Store.Certificates.Find
method or present the user with a Windows dialog box and allow them to select the certificate. To present a dialog box, pass a collection of X.509 certificates contained in an X509Certificate2Collection
object to the SelectFromCollection
method of the System.Security.Cryptography.X509Certificates.X509Certificate2UI
class.
The following example demonstrates all four of the basic approaches described previously. Note that you need to add a reference to the System.Security.dll
assembly.
using System; using System.Net; using System.Security.Cryptography.X509Certificates; namespace Apress.VisualCSharpRecipes.Chapter10 { class Recipe10_06 { public static void Main() { // Create a WebRequest that authenticates the user with a // username and password combination over basic authentication. WebRequest requestA = WebRequest.Create("http://www.somesite.com"); requestA.Credentials = new NetworkCredential("userName", "password"); requestA.PreAuthenticate = true; // Create a WebRequest that authenticates the current user // with Windows integrated authentication. WebRequest requestB = WebRequest.Create("http://www.somesite.com"); requestB.Credentials = CredentialCache.DefaultCredentials; requestB.PreAuthenticate = true;
// Create a WebRequest that authenticates the user with a client // certificate loaded from a file. HttpWebRequest requestC = (HttpWebRequest)WebRequest.Create("http://www.somesite.com"); X509Certificate cert1 = X509Certificate.CreateFromCertFile(@"....TestCertificate.cer"); requestC.ClientCertificates.Add(cert1); // Create a WebRequest that authenticates the user with a client // certificate loaded from a certificate store. Try to find a // certificate with a specific subject, but if it is not found, // present the user with a dialog so they can select the certificate // to use from their personal store. HttpWebRequest requestD = (HttpWebRequest)WebRequest.Create("http://www.somesite.com"); X509Store store = new X509Store(); X509Certificate2Collection certs = store.Certificates.Find(X509FindType.FindBySubjectName, "Allen Jones", false); if (certs.Count == 1) { requestD.ClientCertificates.Add(certs[0]); } else { certs = X509Certificate2UI.SelectFromCollection( store.Certificates, "Select Certificate", "Select the certificate to use for authentication.", X509SelectionFlag.SingleSelection); if (certs.Count != 0) { requestD.ClientCertificates.Add(certs[0]); } } // Now issue the request and process the responses... } } }
Use the SmtpClient
and MailMessage
classes in the System.Net.Mail
namespace.
In versions 1.0 and 1.1 of the .NET Framework, you would send SMTP mail using the SmtpMail
and MailMessage
classes in the System.Web.Mail
namespace from the System.Web.dll
assembly. The SmtpClient
and MailMessage
classes discussed in this recipe were added to the System.dll
assembly in the .NET Framework 2.0, and both simplify and extend the functionality provided by earlier versions.
An instance of the SmtpClient
class provides the mechanism through which you communicate with the SMTP server. You configure the SmtpClient
using the properties described in Table 10-3.
Table 10.3. Properties of the SmtpClient Class
Property | Description |
---|---|
| Gets a |
| Gets or sets an implementation of the |
| Gets or sets a |
| Gets or sets a |
| Gets or sets an |
| Gets or sets an |
| Gets or sets a |
You can specify default settings for the SmtpClient
in the <mailSettings>
section of your machine or application configuration files. Configurable default values include the host, port, username, and password.
Mail messages are represented by MailMessage
objects, which you instantiate and then configure using the members summarized in Table 10-4.
For simple mail messages, the MailMessage
class provides a constructor that allows you to specify the from, to, subject, and body information for the mail message as string
arguments—allowing you to create a complete mail message in a single call.
Table 10.4. Properties of the MailMessage Class
Property | Description |
---|---|
| Gets or sets a |
| Gets or sets a |
| Gets or sets a |
| Gets or sets a |
| Gets or sets a |
| Gets or sets a |
| Gets or sets a |
| Gets or sets a |
| Gets or sets a |
| Gets or sets a |
| Gets or sets a |
Once you have configured the SmtpClient
, you can send your MailMessage
objects using the SmtpClient.Send
method, which will cause your code to block until the send operation is completed or fails. Alternatively, you can send mail using a thread from the thread pool by calling the SendAsync
method. When you call SendAsync
, your code will be free to continue other processing while the e-mail is sent. Add an event handler to the SendCompleted
event to receive notification that the asynchronous send has completed.
Remember that you can't use SMTP to retrieve e-mail. For this task, you need the Post Office Protocol 3 (POP3) or the Internet Message Access Protocol (IMAP), neither of which is exposed natively in the .NET Framework.
The following example demonstrates how to use the SmtpClient
class to send an e-mail message with multiple attachments to a set of recipients whose e-mail addresses are specified as command-line arguments:
using System; using System.Net; using System.Net.Mail; namespace Apress.VisualCSharpRecipes.Chapter10 { class Recipe10_07 { public static void Main(string[] args) { // Create and configure the SmtpClient that will send the mail. // Specify the host name of the SMTP server and the port used // to send mail. SmtpClient client = new SmtpClient("mail.somecompany.com", 25); // Configure the SmtpClient with the credentials used to connect // to the SMTP server. client.Credentials = new NetworkCredential("[email protected]", "password"); // Create the MailMessage to represent the e-mail being sent. using (MailMessage msg = new MailMessage()) { // Configure the e-mail sender and subject. msg.From = new MailAddress("[email protected]"); msg.Subject = "Greetings from Visual C# Recipes"; // Configure the e-mail body. msg.Body = "This is a message from Recipe 10-07 of" + " Visual C# Recipes. Attached is the source file " + " and the binary for the recipe."; // Attach the files to the e-mail message and set their MIME type. msg.Attachments.Add( new Attachment(@"....Recipe10-07.cs","text/plain")); msg.Attachments.Add( new Attachment(@".Recipe10-07.exe", "application/octet-stream")); // Iterate through the set of recipients specified on the // command line. Add all addresses with the correct structure as // recipients. foreach (string str in args) {
// Create a MailAddress from each value on the command line // and add it to the set of recipients. try { msg.To.Add(new MailAddress(str)); } catch (FormatException ex) { // Proceed to the next specified recipient. Console.WriteLine("{0}: Error -- {1}", str, ex.Message); continue; } } // Send the message. client.Send(msg); } // Wait to continue. Console.WriteLine(Environment.NewLine); Console.WriteLine("Main method complete. Press Enter"); Console.ReadLine(); } } }
You want to determine the IP address for a computer based on its fully qualified domain name by performing a DNS query.
Use the method GetHostEntry
of the System.Net.Dns
class, and pass the computer's fully qualified domain name as a string parameter.
In versions 1.0 and 1.1 of the .NET Framework, you should use the method GetHostByName
of the Dns
class, but it is marked as obsolete as of version 2.0.
On the Internet, the human-readable names that refer to computers are mapped to IP addresses, which is what TCP/IP requires in order to communicate between computers. For example, the name www.apress.com
might be mapped to the IP address 66.211.109.45. It is not unusual for the IP address of computers to change while their name remains constant, meaning that it is usually better to reference computers with their name, not their IP address. To determine the current IP address for a given name, the computer contacts a DNS server. The name or IP address of the DNS server contacted is configured as part of a computer's network configuration.
The entire process of name resolution is transparent if you use the System.Net.Dns
class, which allows you to retrieve the IP address for a host name by calling GetHostEntry
.
The Dns
class also provides the BeginGetHostEntry
and EndGetHostEntry
methods, which allow you to resolve IP addresses asynchronously. Also, the static method GetHostName
returns the computer name of the local machine.
The following example retrieves the IP addresses of all computers whose fully qualified domain names are specified as command-line arguments:
using System; using System.Net; namespace Apress.VisualCSharpRecipes.Chapter10 { class Recipe10_08 { public static void Main(string[] args) { foreach (string comp in args) { try { // Retrieve the DNS entry for the specified computer. IPAddress[] addresses = Dns.GetHostEntry(comp).AddressList; // The DNS entry may contain more than one IP address. Iterate // through them and display each one along with the type of // address (AddressFamily). foreach (IPAddress address in addresses) {
Console.WriteLine("{0} = {1} ({2})", comp, address, address.AddressFamily); } } catch (Exception ex) { Console.WriteLine("{0} = Error ({1})", comp, ex.Message); } } // Wait to continue. Console.WriteLine(Environment.NewLine); Console.WriteLine("Main method complete. Press Enter"); Console.ReadLine(); } } }
Running the example with the following command line:
recipe10-08 www.apress.com www.microsoft.com localhost somejunk
will produce output similar to the following. Notice that multiple IP addresses can be returned for some host names.
www.apress.com = 65.19.150.100 (InterNetwork) www.microsoft.com = 207.46.198.30 (InterNetwork) www.microsoft.com = 207.46.20.30 (InterNetwork) www.microsoft.com = 207.46.20.60 (InterNetwork) www.microsoft.com = 207.46.18.30 (InterNetwork) www.microsoft.com = 207.46.19.30 (InterNetwork) www.microsoft.com = 207.46.19.60 (InterNetwork) www.microsoft.com = 207.46.199.30 (InterNetwork) www.microsoft.com = 207.46.198.60 (InterNetwork) localhost = 127.0.0.1 (InterNetwork) somejunk = Error (No such host is known)
Send a ping message. This message is sent using the ICMP, accessible through the Send
method of the System.Net.NetworkInformation.Ping
class.
The Ping
class was introduced in the .NET Framework 2.0. To send a ping message in earlier versions of the .NET Framework, you had to undertake significant effort to manually create an ICMP request message using raw sockets and lengthy code.
A ping message contacts a device at a specific IP address, passing it a test packet, and requests that the remote device respond by echoing back the packet. To gauge the connection latency between two computers, you can measure the time taken for a ping response to be received.
Many commercial web sites do not respond to ping requests because they represent an unnecessary processing overhead and are often used in denial-of-service attacks. The firewall that protects the site will usually filter them out before they reach the specified destination. This will cause your ping request to time out.
The Ping
class allows you to send ping messages using the Send
method. The Send
method provides a number of overloads, which allow you to specify some or all of the following:
The IP address or host name of the target computer. You can specify this as a string
or a System.Net.IPAddress
object.
The number of milliseconds to wait for a response before the request times out (specified as an int
) with the default set to 5,000.
A byte
array of up to 65,500 data bytes that is sent with the ping request and that should be returned in the response.
A System.Net.NetworkInformation.PingOptions
object that specifies time-to-live and fragmentation options for the transmission of the ping message.
The Send
method will return a System.Net.NetworkInformation.PingReply
object. The Status
property of the PingReply
will contain a value from the System.Net.NetworkInformation.IPStatus
enumeration from which you can determine the result of the ping request. The most common values will be Success
and TimedOut
. If the host name you pass to the Send
method cannot be resolved, Send
will throw an exception, but you must look at the InnerException
to determine the cause of the problem.
The Ping
class also provides a SendAsync
method that performs the ping request using a thread-pool thread so that the calling thread does not block. When the ping is finished or fails because of a timeout, the thread raises the PingCompleted
event on the Ping
object, which you can handle using a method that matches the signature of the System.Net.NetworkInformation.PingCompletedEventHandler
delegate. However, the Ping
object can handle only a single concurrent request; otherwise, it will throw a System.InvalidOperationException
.
The Ping
class derives from System.ComponentModel.Component
, so you can add it to the Visual Studio Form Designer Toolbox in order to allow you to easily set the properties or define the event handlers in a Windows Forms–based application.
The following example pings the computers whose domain names or IP addresses are specified as command-line arguments:
using System; using System.Net.NetworkInformation; namespace Apress.VisualCSharpRecipes.Chapter10 { class Recipe10_09 { public static void Main(string[] args) { // Create an instance of the Ping class. using (Ping ping = new Ping()) { Console.WriteLine("Pinging:"); foreach (string comp in args) { try { Console.Write(" {0}...", comp); // Ping the specified computer with a timeout of 100 ms. PingReply reply = ping.Send(comp, 100);
if (reply.Status == IPStatus.Success) { Console.WriteLine("Success - IP Address:{0} Time:{1}ms", reply.Address, reply.RoundtripTime); } else { Console.WriteLine(reply.Status); } } catch (Exception ex) { Console.WriteLine("Error ({0})", ex.InnerException.Message); } } } // Wait to continue. Console.WriteLine(Environment.NewLine); Console.WriteLine("Main method complete. Press Enter"); Console.ReadLine(); } } }
Running the example with the following command line:
recipe10-09 www.apress.com www.google.com localhost somejunk
will produce output similar to the following:
Pinging: www.apress.com...TimedOut www.google.com...Success - IP Address:216.239.59.104 Time:42ms localhost...Success - IP Address:127.0.0.1 Time:0ms somejunk...Error (No such host is known)
One computer (the server) must begin listening using the System.Net.Sockets.TcpListener
class. Another computer (the client) connects to it using the System.Net.Sockets.TcpClient
class. Once a connection is established, both computers can communicate using the System.Net.Sockets.NetworkStream
class.
TCP is a reliable, connection-oriented protocol that allows two computers to communicate over a network. It provides built-in flow control, sequencing, and error handling, which makes it reliable and easy to program.
To create a TCP connection, one computer must act as the server and start listening on a specific endpoint. (An endpoint is a combination of an IP address and a port number.) The other computer must act as a client and send a connection request to the endpoint on which 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 System.Net.Sockets.NetworkStream
to transmit data.
Even though a TCP connection always requires a server and a client, an individual application could be both. For example, in a peer-to-peer application, one thread is dedicated to listening for incoming requests (acting as a server), and another thread is dedicated to initiating outgoing connections (acting as a client). In the examples provided with this chapter, the client and server are provided as separate applications and are placed in separate subdirectories.
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 the application-level protocol that clients and servers will use to communicate. This protocol includes constants that represent the allowable commands, ensuring that your application code doesn't include hard-coded communication strings.
In this example, the defined protocol is 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 should be compiled into a separate class library assembly, which must be referenced by both the client and server. Here is the code for the shared protocol:
namespace Apress.VisualCSharpRecipes.Chapter10 { public class Recipe10_10Shared { public const string AcknowledgeOK = "OK"; public const string AcknowledgeCancel = "Cancel"; public const string Disconnect = "Bye"; public const string RequestConnect = "Hello"; } }
The following code is a template for a basic TCP server. It listens on 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 TcpListener.AcceptTcpClient
method again to wait for the next client, but instead it simply shuts down.
using System; using System.IO; using System.Net; using System.Net.Sockets; namespace Apress.VisualCSharpRecipes.Chapter10 { public class Recipe10_10Server { public static void Main() { // Create a new listener on port 8000. TcpListener listener = new TcpListener(IPAddress.Parse("127.0.0.1"), 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. using (TcpClient client = listener.AcceptTcpClient()) { Console.WriteLine("Connection accepted.");
// Retrieve the network stream. NetworkStream stream = client.GetStream(); // Create a BinaryWriter for writing to the stream. using (BinaryWriter w = new BinaryWriter(stream)) { // Create a BinaryReader for reading from the stream. using (BinaryReader r = new BinaryReader(stream)) { if (r.ReadString() == Recipe10_10Shared.RequestConnect) { w.Write(Recipe10_10Shared.AcknowledgeOK); Console.WriteLine("Connection completed."); while (r.ReadString() != Recipe10_10Shared.Disconnect) { } Console.WriteLine(Environment.NewLine); Console.WriteLine("Disconnect request received."); } else { Console.WriteLine("Can't complete connection."); } } } } Console.WriteLine("Connection closed."); } catch (Exception ex) { Console.WriteLine(ex.ToString()); } finally { // Close the underlying socket (stop listening for new requests). listener.Stop(); Console.WriteLine("Listener stopped."); } // Wait to continue. Console.WriteLine(Environment.NewLine); Console.WriteLine("Main method complete. Press Enter"); Console.ReadLine(); } } }
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 local 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 to connect to needs to be specified. The outgoing client port can be chosen dynamically at runtime from the available ports, which is what the TcpClient
class will do by default.
using System; using System.IO; using System.Net; using System.Net.Sockets; namespace Apress.VisualCSharpRecipes.Chapter10 { public class Recipe10_10Client { public static void Main() { TcpClient client = 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. NetworkStream stream = client.GetStream(); // Create a BinaryWriter for writing to the stream. using (BinaryWriter w = new BinaryWriter(stream)) { // Create a BinaryReader for reading from the stream. using (BinaryReader r = new BinaryReader(stream)) { // Start a dialog. w.Write(Recipe10_10Shared.RequestConnect); if (r.ReadString() == Recipe10_10Shared.AcknowledgeOK) { Console.WriteLine("Connected."); Console.WriteLine("Press Enter to disconnect."); Console.ReadLine(); Console.WriteLine("Disconnecting..."); w.Write(Recipe10_10Shared.Disconnect); }
else { Console.WriteLine("Connection not completed."); } } } } catch (Exception err) { Console.WriteLine(err.ToString()); } finally { // Close the connection socket. client.Close(); Console.WriteLine("Port closed."); } // Wait to continue. Console.WriteLine(Environment.NewLine); Console.WriteLine("Main method complete. Press Enter"); Console.ReadLine(); } } }
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.
You need to handle multiple network requests concurrently or perform a network data transfer as a background task while your program continues with other processing.
Use the method AcceptTcpClient
of the System.Net.Sockets.TcpListener
class to accept connections. Every time a new client connects, start a new thread to handle the connection. Alternatively, use the TcpListener.BeginAcceptTcpClient
to accept a new client connection on a thread-pool thread using the asynchronous execution pattern (discussed in recipe 4-2).
To start a background task to handle the asynchronous sending of data, you can use the BeginWrite
method of the System.Net.Sockets.NetworkStream
class and supply a callback method—each time the callback is triggered, send more data.
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 create a worker object on the server that will handle each connection on a separate thread. The TcpListener.AcceptTcpClient
method returns a TcpClient
when a connection is established. This should be passed off to a threaded worker object so that the worker can communicate with the remote client.
Alternatively, call the TcpListener.BeginAcceptTcpClient
method to start an asynchronous operation using a thread-pool thread that waits in the background for a client to connect. BeginAcceptTcpClient
follows the asynchronous execution pattern, allowing you to wait for the operation to complete or specify a callback that the .NET runtime will call when a client connects. (See recipe 4-2 for details on the options available.) Whichever mechanism you use, once BeginAcceptTcpClient
has completed, call EndAcceptTcpClient
to obtain the newly created TcpClient
object.
To exchange network data asynchronously, you can use the NetworkStream
class, which includes basic support for asynchronous communication through the BeginRead
and BeginWrite
methods. Using these methods, you can send or receive a block of data on one of the threads provided by the thread pool, without blocking your code. When sending data asynchronously, you must send raw binary data (an array of bytes). It's up to you to choose the amount you want to send or receive at a time.
One advantage of this approach when sending files is that the entire content of the file does not have to be held in memory at once. Instead, it is retrieved just before a new block is sent. Another advantage is that the server can abort the transfer operation easily at any time.
The following example demonstrates various techniques for handling network connections and communications asynchronously. The server (Recipe10-11Server
) starts a thread-pool thread listening for new connections using the TcpListener.BeginAcceptTcpClient
method and specifying a callback method to handle the new connections. Every time a client connects to the server, the callback method obtains the new TcpClient
object and passes it to a new threaded ClientHandler
object to handle client communications.
The ClientHandler
object waits for the client to request data and then sends a large amount of data (read from a file) to the client. This data is sent asynchronously, which means ClientHandler
could continue to perform other tasks. In this example, it simply monitors the network stream for messages sent from the client. The client reads only a third of the data before sending a disconnect message to the server, which terminates the remainder of the file transfer and drops the client connection.
Here is the code for the shared protocol:
namespace Apress.VisualCSharpRecipes.Chapter10 { public class Recipe10_11Shared { public const string AcknowledgeOK = "OK"; public const string AcknowledgeCancel = "Cancel"; public const string Disconnect = "Bye"; public const string RequestConnect = "Hello"; public const string RequestData = "Data"; } }
Here is the server code:
using System; using System.IO; using System.Net; using System.Threading; using System.Net.Sockets; namespace Apress.VisualCSharpRecipes.Chapter10 { public class Recipe10_11Server { // A flag used to indicate whether the server is shutting down. private static bool terminate; public static bool Terminate { get { return terminate; } } // A variable to track the identity of each client connection. private static int ClientNumber = 0; // A single TcpListener will accept all incoming client connections. private static TcpListener listener; public static void Main() {
// Create a 100KB test file for use in the example. This file will be // sent to clients that connect. using (FileStream fs = new FileStream("test.bin", FileMode.Create)) { fs.SetLength(100000); } try { // Create a TcpListener that will accept incoming client // connections on port 8000 of the local machine. listener = new TcpListener(IPAddress.Parse("127.0.0.1"), 8000); Console.WriteLine("Starting TcpListener..."); // Start the TcpListener accepting connections. terminate = false; listener.Start(); // Begin asynchronously listening for client connections. When a // new connection is established, call the ConnectionHandler // method to process the new connection. listener.BeginAcceptTcpClient(ConnectionHandler, null); // Keep the server active until the user presses Enter. Console.WriteLine("Server awaiting connections. " + "Press Enter to stop server."); Console.ReadLine(); } finally { // Shut down the TcpListener. This will cause any outstanding // asynchronous requests to stop and throw an exception in // the ConnectionHandler when EndAcceptTcpClient is called. // More robust termination synchronization may be desired here, // but for the purpose of this example ClientHandler threads are // all background threads and will terminate automatically when // the main thread terminates. This is suitable for our needs. Console.WriteLine("Server stopping..."); terminate = true; if (listener != null) listener.Stop(); } // Wait to continue. Console.WriteLine(Environment.NewLine); Console.WriteLine("Server stopped. Press Enter"); Console.ReadLine(); } // A method to handle the callback when a connection is established // from a client. This is a simple way to implement a dispatcher
// but lacks the control and scalability required when implementing // full-blown asynchronous server applications. private static void ConnectionHandler(IAsyncResult result) { TcpClient client = null; // Always end the asynchronous operation to avoid leaks. try { // Get the TcpClient that represents the new client connection. client = listener.EndAcceptTcpClient(result); } catch (ObjectDisposedException) { // Server is shutting down and the outstanding asynchronous // request calls the completion method with this exception. // The exception is thrown when EndAcceptTcpClient is called. // Do nothing and return. return; } Console.WriteLine("Dispatcher: New connection accepted."); // Begin asynchronously listening for the next client // connection. listener.BeginAcceptTcpClient(ConnectionHandler, null); if (client != null) { // Determine the identifier for the new client connection. Interlocked.Increment(ref ClientNumber); string clientName = "Client " + ClientNumber.ToString(); Console.WriteLine("Dispatcher: Creating client handler ({0})." , clientName); // Create a new ClientHandler to handle this connection. new ClientHandler(client, clientName); } } } // A class that encapsulates the logic to handle a client connection. public class ClientHandler { // The TcpClient that represents the connection to the client. private TcpClient client; // An ID that uniquely identifies this ClientHandler. private string ID;
// The amount of data that will be written in one block (2KB). private int bufferSize = 2048; // The buffer that holds the data to write. private byte[] buffer; // Used to read data from the local file. private FileStream fileStream; // A signal to stop sending data to the client. private bool stopDataTransfer; internal ClientHandler(TcpClient client, string ID) { this.buffer = new byte[bufferSize]; this.client = client; this.ID = ID; // Create a new background thread to handle the client connection // so that we do not consume a thread-pool thread for a long time // and also so that it will be terminated when the main thread ends. Thread thread = new Thread(ProcessConnection); thread.IsBackground = true; thread.Start(); } private void ProcessConnection() { using (client) { // Create a BinaryReader to receive messages from the client. At // the end of the using block, it will close both the BinaryReader // and the underlying NetworkStream. using (BinaryReader reader = new BinaryReader(client.GetStream())) { if (reader.ReadString() == Recipe10_11Shared.RequestConnect) { // Create a BinaryWriter to send messages to the client. // At the end of the using block, it will close both the // BinaryWriter and the underlying NetworkStream. using (BinaryWriter writer = new BinaryWriter(client.GetStream())) { writer.Write(Recipe10_11Shared.AcknowledgeOK); Console.WriteLine(ID + ": Connection established."); string message = "";
while (message != Recipe10_11Shared.Disconnect) { try { // Read the message from the client. message = reader.ReadString(); } catch { // For the purpose of the example, any // exception should be taken as a // client disconnect. message = Recipe10_11Shared.Disconnect; } if (message == Recipe10_11Shared.RequestData) { Console.WriteLine(ID + ": Requested data. ", "Sending..."); // The filename could be supplied by the // client, but in this example a test file // is hard-coded. fileStream = new FileStream("test.bin", FileMode.Open, FileAccess.Read); // Send the file size--this is how the client // knows how much to read. writer.Write(fileStream.Length.ToString()); // Start an asynchronous send operation. stopDataTransfer = false; StreamData(null); } else if (message == Recipe10_11Shared.Disconnect) { Console.WriteLine(ID + ": Client disconnecting..."); stopDataTransfer = true; } else { Console.WriteLine(ID + ": Unknown command."); } } } }
else { Console.WriteLine(ID + ": Could not establish connection."); } } } Console.WriteLine(ID + ": Client connection closed."); } private void StreamData(IAsyncResult asyncResult) { // Always complete outstanding asynchronous operations to avoid leaks. if (asyncResult != null) { try { client.GetStream().EndWrite(asyncResult); } catch { // For the purpose of the example, any exception obtaining // or writing to the network should just terminate the // download. fileStream.Close(); return; } } if (!stopDataTransfer && !Recipe10_11Server.Terminate) { // Read the next block from the file. int bytesRead = fileStream.Read(buffer, 0, buffer.Length); // If no bytes are read, the stream is at the end of the file. if (bytesRead > 0) { Console.WriteLine(ID + ": Streaming next block."); // Write the next block to the network stream. client.GetStream().BeginWrite(buffer, 0, buffer.Length, StreamData, null); } else { // End the operation. Console.WriteLine(ID + ": File streaming complete."); fileStream.Close(); } }
else { // Client disconnected. Console.WriteLine(ID + ": Client disconnected."); fileStream.Close(); } } } }
And here is the client code:
using System; using System.Net; using System.Net.Sockets; using System.IO; namespace Apress.VisualCSharpRecipes.Chapter10 { public class Recipe10_11Client { private static void Main() { using (TcpClient client = new TcpClient()) { Console.WriteLine("Attempting to connect to the server ", "on port 8000."); // Connect to the server. client.Connect(IPAddress.Parse("127.0.0.1"), 8000); // Retrieve the network stream from the TcpClient. using (NetworkStream networkStream = client.GetStream()) { // Create a BinaryWriter for writing to the stream. using (BinaryWriter writer = new BinaryWriter(networkStream)) { // Start a dialog. writer.Write(Recipe10_11Shared.RequestConnect); // Create a BinaryReader for reading from the stream. using (BinaryReader reader = new BinaryReader(networkStream)) { if (reader.ReadString() == Recipe10_11Shared.AcknowledgeOK) { Console.WriteLine("Connection established." + "Press Enter to download data."); Console.ReadLine();
// Send message requesting data to server. writer.Write(Recipe10_11Shared.RequestData); // The server should respond with the size of // the data it will send. Assume it does. int fileSize = int.Parse(reader.ReadString()); // Only get part of the data, then carry out a // premature disconnect. for (int i = 0; i < fileSize / 3; i++) { Console.Write(networkStream.ReadByte()); } Console.WriteLine(Environment.NewLine); Console.WriteLine("Press Enter to disconnect."); Console.ReadLine(); Console.WriteLine("Disconnecting..."); writer.Write(Recipe10_11Shared.Disconnect); } else { Console.WriteLine("Connection not established."); } } } } } // Wait to continue. Console.WriteLine(Environment.NewLine); Console.WriteLine("Connection closed. Press Enter"); Console.ReadLine(); } } }
Use the System.Net.Sockets.UdpClient
class and use two threads: one to send data and the other to receive it.
UDP is a connectionless protocol that doesn't include any flow control or error checking. Unlike TCP, UDP shouldn't be used where reliable communication is required. However, because of its lower overhead, UDP is often used for "chatty" applications where it is acceptable to lose some messages. For example, imagine you want to create a network in which individual clients send information about the current temperature at their locations to a server every few seconds. 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 application shown in the following code uses two threads: one to receive messages and one to send them. The application stops when the user presses the Enter key without any text to send. Notice 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 2-2.
using System; using System.Text; using System.Net; using System.Net.Sockets; using System.Threading; namespace Apress.VisualCSharpRecipes.Chapter10 { class Recipe10_12 { private static int localPort; private static void Main() { // Define endpoint where messages are sent. Console.Write("Connect to IP: "); string IP = Console.ReadLine(); Console.Write("Connect to port: "); int port = Int32.Parse(Console.ReadLine()); IPEndPoint remoteEndPoint = new IPEndPoint(IPAddress.Parse(IP), port); // Define local endpoint (where messages are received). Console.Write("Local port for listening: "); localPort = Int32.Parse(Console.ReadLine()); // Create a new thread for receiving incoming messages. Thread receiveThread = new Thread(ReceiveData); receiveThread.IsBackground = true; receiveThread.Start();
UdpClient client = new UdpClient(); Console.WriteLine("Type message and press Enter to send:"); try { string text; do { text = Console.ReadLine(); // Send the text to the remote client. if (text.Length != 0) { // Encode the data to binary using UTF8 encoding. byte[] data = Encoding.UTF8.GetBytes(text); // Send the text to the remote client. client.Send(data, data.Length, remoteEndPoint); } } while (text.Length != 0); } catch (Exception err) { Console.WriteLine(err.ToString()); } finally { client.Close(); } // Wait to continue. Console.WriteLine(Environment.NewLine); Console.WriteLine("Main method complete. Press Enter"); Console.ReadLine(); } private static void ReceiveData() { UdpClient client = new UdpClient(localPort); while (true) { try { // Receive bytes. IPEndPoint anyIP = new IPEndPoint(IPAddress.Any, 0); byte[] data = client.Receive(ref anyIP);
// Convert bytes to text using UTF8 encoding. string text = Encoding.UTF8.GetString(data); // Display the retrieved text. Console.WriteLine(">> " + text); } catch (Exception err) { Console.WriteLine(err.ToString()); } } } } }
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 test this application with clients on the local computer using the loopback alias 127.0.0.1, provided you use different listening ports. For example, imagine a situation with two UDP clients, client A and client B. Here's a sample transcript for client A:
Connect to IP: 127.0.0.1 Connect to port: 8001 Local port for listening: 8080 Hi there!
And here's the corresponding transcript for client B (with the received message):
Connect to IP: 127.0.0.1 Connect to port: 8080 Local port for listening: 8001 >> Hi there!
You need to expose functionality as a SOAP-based web service so that it can be accessed across the Internet.
Declare an interface
that contains the methods you want your web service to expose. Identify this interface as a Windows Communication Foundation (WCF) service contract by applying the ServiceContractAttribute
attribute to the interface and the OperationContractAttribute
attribute to each of the methods you want exposed by the web service. The ServiceContractAttribute
and OperationContractAttribute
classes are members of the System.ServiceModel
namespace.
Define any complex data types passed to and from the service and identify them as WCF data contracts by applying the DataContractAttribute
attribute to the class and the DataMemberAttribute
attribute to the members that need to be passed across the network. The DataContractAttribute
and DataMemberAttribute
classes are members of the System.Runtime.Serialization
namespace.
Implement the service contract functionality by implementing the interface
on a class, configure the configuration settings that control the behavior and protocols used by the service, and host an instance of the service class in a service host.
WCF allows you to quickly create web services that are accessible across the Internet and that offer many choices in terms of protocols, security, and communication models. To create a simple SOAP-based WCF web service, you need the following key ingredients:
A service contract: This defines the functionality exposed by the web service and is usually in the form of a C# interface, where the interface is annotated with the ServiceContractAttribute
attribute and the web service methods are annotated with the OperationContractAttribute
attribute.
A service implementation: This object implements the service contract interface and defines what each of the web service methods does when called by a client.
A service host: The service host is a process that controls the life cycle of the web service. This can be a custom program that loads your service (called self-hosting), Internet Information Server (IIS), or Windows Activation Services (WAS).
There is potentially a lot of configuration information associated with a WCF web service, including things like network addresses, protocol selection, and security settings. But the beauty of WCF is that you really only need to worry about those bits of functionality that you are actively using and ignore the rest. You can also choose whether to manage your configuration in code or through the application config files. However, unless you need to make configuration decisions at runtime, it is usually best to use declarative configuration so that you can change settings without having to change your code.
Once you have created a SOAP-based web service, the easiest way to consume it is to automatically generate a proxy class using Visual Studio, or use the ServiceModel Metadata Utility Tool (svcutil.exe
), which is part of the Windows SDK. In some circumstances, you can also generate proxy classes dynamically (see recipe 10-14 for details).
The following example demonstrates the creation of a simple SOAP-based web service that allows you to create, update, find, and delete employee records. The example is self-hosted, but could be moved to IIS or WAS without changes to the service code. The IEmployeeService
interface defines the service contract.
using System; using System.ServiceModel; namespace Apress.VisualCSharpRecipes.Chapter10 {
[ServiceContract(Namespace = "Apress.VisualCSharpRecipes.Chapter10")] public interface IEmployeeService { [OperationContract] Employee CreateEmployee(Employee newEmployee); [OperationContract] bool DeleteEmployee(int employeeId); [OperationContract(Name="GetEmployeeById")] Employee GetEmployee(int employeeId); [OperationContract(Name = "GetEmployeeByName")] Employee GetEmployee(string employeeName); [OperationContract] Employee UpdateEmployee(Employee updatedEmployee); } }
Here is the class that declares the Employee
data contract representing the data that is passed between the client and the service:
using System; using System.Runtime.Serialization; namespace Apress.VisualCSharpRecipes.Chapter10 { [DataContract] public class Employee { [DataMember] public DateTime DateOfBirth { get; set; } [DataMember] public int Id { get; set; } [DataMember] public string Name { get; set; } } }
The Employee
Service
class implements the IEmployeeService
interface and provides the actual logic of the web service.
using System; using System.Collections.Generic; using System.Linq; namespace Apress.VisualCSharpRecipes.Chapter10 {
public class EmployeeService : IEmployeeService { private Dictionary<int, Employee> employees; public EmployeeService() { employees = new Dictionary<int, Employee>(); } // Create an Employee based on the contents of a provided // Employee object. Return the new Employee object. public Employee CreateEmployee(Employee newEmployee) { // NOTE: Should validate new employee data. newEmployee.Id = employees.Count + 1; lock (employees) { employees[newEmployee.Id] = newEmployee; } return newEmployee; } // Delete an employee by the specified Id and return true // or false depending on if an Employee was deleted. public bool DeleteEmployee(int employeeId) { lock(employees) { return employees.Remove(employeeId); } } // Get an employee by the specified Id and return null if // the employee does not exist. public Employee GetEmployee(int employeeId) { Employee employee = null; lock (employees) { employees.TryGetValue(employeeId, out employee); } return employee; }
// Get an employee by the specified Name and return null if // the employee does not exist. public Employee GetEmployee(string employeeName) { Employee employee = null; lock (employees) { employee = employees.Values.FirstOrDefault (e => e.Name.ToLower() == employeeName.ToLower()); } return employee; } // Update an employee based on the contents of a provided // Employee object. Return the updated Employee object. public Employee UpdateEmployee(Employee updatedEmployee) { Employee employee = GetEmployee(updatedEmployee.Id); // NOTE: Should validate new employee data. if (employee != null) { lock (employees) { employees[employee.Id] = updatedEmployee; } } return updatedEmployee; } } }
The following code shows the simple service host created to run the service:
using System; using System.ServiceModel; namespace Apress.VisualCSharpRecipes.Chapter10 { public static class Recipe10_13Service { static void Main(string[] args) { ServiceHost host = new ServiceHost(typeof(EmployeeService)); host.Open();
// Wait to continue. Console.WriteLine("Service host running. Press Enter to terminate."); Console.ReadLine(); } } }
The following shows the configuration information used by the service host:
<?xml version="1.0" encoding="utf-8" ?> <configuration> <system.serviceModel> <behaviors> <serviceBehaviors> <behavior name="EmployeeServiceBehavior" > <serviceMetadata httpGetEnabled="true" /> </behavior> </serviceBehaviors> </behaviors> <services> <service name="Apress.VisualCSharpRecipes.Chapter10.EmployeeService" behaviorConfiguration="EmployeeServiceBehavior"> <endpoint address="http://localhost:8000/EmployeeService" binding="wsHttpBinding" contract="Apress.VisualCSharpRecipes.Chapter10.IEmployeeService" /> <endpoint address="http://localhost:8000/EmployeeService/mex" binding="mexHttpBinding" contract="IMetadataExchange" /> <host> <baseAddresses> <add baseAddress="http://localhost:8000/" /> </baseAddresses> </host> </service> </services> </system.serviceModel> </configuration>
Finally, the following simple client code demonstrates how to interact with the service via a proxy:
using System; using Apress.VisualCSharpRecipes.Chapter10.Services; namespace Apress.VisualCSharpRecipes.Chapter10 { class Recipe10_13Client { private static string FormatEmployee(Employee emp) {
return String.Format("ID:{0}, NAME:{1}, DOB:{2}", emp.Id, emp.Name, emp.DateOfBirth); } static void Main(string[] args) { // Create a service proxy. using (EmployeeServiceClient employeeService = new EmployeeServiceClient()) { // Create a new Employee. Employee emp = new Employee() { DateOfBirth = DateTime.Now, Name = "Allen Jones" }; // Call the EmployeeService to create a new Employee record. emp = employeeService.CreateEmployee(emp); Console.WriteLine("Created employee record - {0}", FormatEmployee(emp)); // Update the existing Employee. emp.DateOfBirth = new DateTime(1950, 10, 13); emp = employeeService.UpdateEmployee(emp); Console.WriteLine("Updated employee record - {0}", FormatEmployee(emp)); // Wait to continue. Console.WriteLine(Environment.NewLine); Console.WriteLine("Main method complete. Press Enter"); Console.ReadLine(); } } } }
You need to call the methods of a WCF service but can't or don't want to generate a static proxy class as described in recipe 10-13.
If you have access to classes that represent the service and data contracts exposed by the service, you can create a dynamic service proxy using the System.ServiceModel.ChannelFactory
class.
The ChannelFactory
class is a generic class that allows you to create proxies dynamically based on WCF service contracts. When instantiating a ChannelFactory
, in addition to identifying the type of the service proxy you want to create, you must provide details of the WCF endpoint to which you want to connect. This includes the binding you want to use and the address of the service you want to communicate with.
Once you have instantiated a properly configured ChannelFactory
, you call its CreateChannel
method, which will return a service proxy in the form of an instance of the service contract type. You can then use this proxy to make calls against the service endpoint identified in the ChannelFactory
constructor.
The following code demonstrates the use of a dynamic service proxy to communicate with a WCF service. The service used in this example is basically the same as that used in recipe 10-13. Instead of using Visual Studio to generate a proxy class, the client project contains a reference to the service project. The reference gives the client code access to the service and data contract classes of the service, enabling the use of the ChannelFactory
to create dynamic service proxies at runtime.
using System; using System.ServiceModel; namespace Apress.VisualCSharpRecipes.Chapter10 { class Recipe10_14Client { static void Main(string[] args) { string serviceUri = "http://localhost:8000/EmployeeService"; // Create the ChannelFactory that is used to generate service // proxies. using (ChannelFactory<IEmployeeService> channelFactory = new ChannelFactory<IEmployeeService>(new WSHttpBinding(), serviceUri)) { // Create a dynamic proxy for IEmployeeService. IEmployeeService proxy = channelFactory.CreateChannel(); // Create a new Employee. Employee emp = new Employee() { DateOfBirth = DateTime.Now, Name = "Allen Jones" };
// Call the EmployeeService to create a new Employee record. emp = proxy.CreateEmployee(emp); Console.WriteLine("Created employee record - {0}", emp); // Update an existing Employee record. emp.DateOfBirth = new DateTime(1950, 10, 13); emp = proxy.UpdateEmployee(emp); Console.WriteLine("Updated employee record - {0}", emp); // Wait to continue. Console.WriteLine(Environment.NewLine); Console.WriteLine("Main method complete. Press Enter"); Console.ReadLine(); } } } }
You need to process the content of an Atom 1.0 or RSS 2.0 feed to extract details of the feed, or the items it contains.
Parse the feed data using one of the format-specific classes derived from System.ServiceModel.Syndication.SyndicationFeedFormatter
. Once parsed, the SyndicationFeedFormatter
.Feed
property returns a System.ServiceModel.Syndication
.SyndicationFeed
object whose properties provide access to the attributes of the feed and the items it contains. Use the SyndicationFeedFormatter.Items
property to access a collection of System.ServiceModel.Syndication.SyndicationItem
objects that represent the items in the feed. The properties of each SyndicationItem
object expose the attributes of the individual feed items.
The SyndicationFeed
and SyndicationFeedItem
classes provide a generic abstraction of Atom 1.0 and RSS 2.0 feeds and feed items, and present a common interface to simplify the processing of both feed types. The Rss20FeedFormatter
class allows you to create a SyndicationFeed
object from an RSS 2.0 feed, and the Atom10FeedFormatter
class provides equivalent support for Atom 1.0 feeds. Both classes are members of the System.ServiceModel.Syndication
namespace.
To load feed data for processing, use the SyndicationFeedFormatter
.
ReadFrom
method and pass it a System.Xml.XmlReader
that provides access to the feed data. Once loaded with feed data, the SyndicationFeedFormatter
.Feed
property provides access to a SyndicationFeed
object allowing you to use the properties listed in Table 10-5 to access the feed attributes.
Table 10.5. Properties of the SyndicatedFeed Class
Property | Description |
---|---|
| Gets a collection of authors from the feed |
| Gets the base URI from the feed |
| Gets a collection of categories from the feed |
| Gets the description from the feed |
| Gets the ID from the feed |
| Gets the image URL from the feed |
| Gets the collection of items contained in the feed |
| Gets the language from the feed |
| Gets the last updated time from the feed |
| Gets a collection of links associated with the feed |
| Gets the title from the feed |
The SyndicationFeed
.Items
property provides access to the set of items contained in the feed. Each item is represented by a SyndicationItem
. Table 10-6 lists the properties that provide access to the attributes of a feed item.
Table 10.6. Properties of the SyndicationItem Class
Property | Description |
---|---|
| Gets a collection of authors from the feed item |
| Gets the base URI from the feed item |
| Gets a collection of categories from the feed item |
| Gets the content from the feed item |
| Gets the ID from the feed item |
| Gets the language from the feed item |
| Gets the last updated time from the feed item |
| Gets a collection of links associated with the feed |
| Gets the summary from the feed item |
| Gets the title from the feed item |
The following example takes the URL of a feed as a command-line argument, downloads the feed, determines whether it is an RSS or Atom feed, and parses it using the appropriate SyndicationFeedFormatter
. The output from running the example contains the title and description of the overall feed, and then the title, summary, and publication date of each item in the feed.
using System; using System.Net; using System.ServiceModel.Syndication; using System.Xml.Linq; namespace Apress.VisualCSharpRecipes.Chapter10 { class Recipe10_15 { static void Main(string[] args) { Uri feedUrl = null;
if (args.Length == 0 || String.IsNullOrEmpty(args[0]) || !Uri.TryCreate(args[0], UriKind.RelativeOrAbsolute, out feedUrl)) { // Error and wait to continue. Console.WriteLine("Invalid feed URL. Press Enter."); Console.ReadLine(); return; } // Create the web request based on the URL provided for the feed. WebRequest req = WebRequest.Create(feedUrl); // Get the data from the feed. WebResponse res = req.GetResponse(); // Simple test for the type of feed: Atom 1.0 or RSS 2.0. SyndicationFeedFormatter formatter = null; XElement feed = XElement.Load(res.GetResponseStream()); if (feed.Name.LocalName == "rss") { formatter = new Rss20FeedFormatter(); } else if (feed.Name.LocalName == "feed") { formatter = new Atom10FeedFormatter(); } else { // Error and wait to continue. Console.WriteLine("Unsupported feed type: " + feed.Name.LocalName); Console.ReadLine(); return; } // Read the feed data into the formatter. formatter.ReadFrom(feed.CreateReader()); // Display feed level data: Console.WriteLine("Title: " + formatter.Feed.Title.Text); Console.WriteLine("Description: " + formatter.Feed.Description.Text); Console.Write(Environment.NewLine); Console.WriteLine("Items: ");
// Display the item data. foreach (var item in formatter.Feed.Items) { Console.WriteLine(" Title: " + item.Title.Text); Console.WriteLine(" Summary: " + item.Summary.Text); Console.WriteLine(" Publish Date: " + item.PublishDate); Console.Write(Environment.NewLine); } // Wait to continue. Console.WriteLine(Environment.NewLine); Console.WriteLine("Main method complete. Press Enter."); Console.ReadLine(); } } }
Use the System.Uri
class to extract the component parts of an existing URI and the System.Uri
Builder
class to construct a new well-formed URI.
When doing web or network programming, you will regularly need to manipulate URIs and their closely related derivatives: URNs and URLs. For example, you may need to construct URLs that represent servers and network resources you want to access, or extract information like the host, port, or protocol specified in an externally provided URI.
The System.Uri
class provides an object representation of a URI and implements properties that allow you to extract the various elements that constitute the URI. To create a Uri
, pass the string representing the URI you want to parse to the Uri
constructor. A number of constructor overloads allow you to handle both absolute and relative URIs. However, if the URI string is invalid, the constructor will throw a System.UriFormatException
. To avoid this, you can use the static method Uri
.
Try
Create
, which returns true
if the parse was successful and false
otherwise.
Once you have a Uri
object, you can use its properties to extract specific components of the URI. Table 10-7 contains some commonly used properties of the Uri
class.
Table 10.7. Commonly Used Members of the Uri Class
Property | Description |
---|---|
| Gets a |
| Gets a |
| Gets a |
| Gets a |
| Gets the original |
| Gets an |
| Gets a |
| Gets a |
The Uri
class is read-only, so if you want to create a new well-formed URI, you should use the Uri
Builder
class. You can specify the key elements of the URI in various overloads of the Uri
Builder
constructor, or you can configure a new Uri
Builder
object via its properties. Table 10-8 describes the properties of the Uri
Builder
class.
Table 10.8. Properties of the UriBuilder Class
Property | Description |
---|---|
| Gets or sets a |
| Gets or sets a |
| Gets or sets a |
| Gets or sets a |
| Gets or sets an |
| Gets or sets a |
| Gets or sets a |
| Gets or sets a |
Once you have configured the UriBuilder
, you obtain an appropriately configured Uri
object representing the URI via the UriBuilder.Uri
property. Many methods that require URIs take Uri
instances, but if you need a string
representation of the URI you can use the Uri.AbsolutePath
property.
The following code demonstrates the use of the Uri
and UriBuilder
classes.
using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace Apress.VisualCSharpRecipes.Chapter10 { class Recipe10_16 { private static string defualtUrl = "http://www.apress.com:80/book/view/9781430225256"; static void Main(string[] args) { Uri uri = null; // Extract information from a string URL passed as a // command line argument or use the default URL. string strUri = defualtUrl; if (args.Length > 0 && !String.IsNullOrEmpty(args[0])) { strUri = args[0]; } // Safely parse the url if (Uri.TryCreate(strUri, UriKind.RelativeOrAbsolute, out uri)) { Console.WriteLine("Parsed URI: " + uri.OriginalString); Console.WriteLine(" Scheme: " + uri.Scheme); Console.WriteLine(" Host: " + uri.Host); Console.WriteLine(" Port: " + uri.Port); Console.WriteLine(" Path and Query: " + uri.PathAndQuery); }
else { Console.WriteLine("Unable to parse URI: " + strUri); } // Create a new URI. UriBuilder newUri = new UriBuilder(); newUri.Scheme = "http"; newUri.Host = "www.apress.com"; newUri.Port = 80; newUri.Path = "book/view/9781430225256"; Console.WriteLine(Environment.NewLine); Console.WriteLine("Created URI: " + newUri.Uri.AbsoluteUri); // Wait to continue. Console.WriteLine(Environment.NewLine); Console.WriteLine("Main method complete. Press Enter."); Console.ReadLine(); } } }