Chapter 16. Networking

.NET offers a variety of classes in the System.Net.* namespaces for communicating via standard network protocols, such as HTTP, TCP/IP, and FTP. Here’s a summary of the key components:

  • A WebClient façade class for simple download/upload operations via HTTP or FTP

  • WebRequest and WebResponse classes for low-level control over client-side HTTP or FTP operations

  • HttpClient for consuming HTTP web APIs and RESTful services

  • HttpListener for writing an HTTP server

  • SmtpClient for constructing and sending mail messages via SMTP

  • Dns for converting between domain names and addresses

  • TcpClient, UdpClient, TcpListener, and Socket classes for direct access to the transport and network layers

These types are all part of .NET Standard 2.0, which means Universal Windows Platform (UWP) applications can use them. UWP apps can also use the Windows Runtime (WinRT) types for TCP and UDP communication in Windows.Networking.Sockets, which we demonstrate in the final section in this chapter. These have the advantage of encouraging asynchronous programming.

The .NET types in this chapter are in the System.Net.* and System.IO namespaces.

Network Architecture

Figure 16-1 illustrates the .NET networking types and the communication layers in which they reside. Most types reside in the transport layer or application layer. The transport layer defines basic protocols for sending and receiving bytes (TCP and UDP); the application layer defines higher-level protocols designed for specific applications, such as retrieving web pages (HTTP), transferring files (FTP), sending mail (SMTP), and converting between domain names and IP addresses (DNS).

Network architecture
Figure 16-1. Network architecture

It’s usually most convenient to program at the application layer; however, there are a couple of reasons why you might want to work directly at the transport layer. One is if you need an application protocol not provided in .NET, such as POP3 for retrieving mail. Another is if you want to invent a custom protocol for a special application such as a peer-to-peer client.

Of the application protocols, HTTP is special in its applicability to general-purpose communication. Its basic mode of operation—“give me the web page with this URL”—adapts nicely to “get me the result of calling this endpoint with these arguments.” (In addition to the “get” verb, there is “put,” “post,” and “delete,” allowing for REST-based services.)

HTTP also has a rich set of features that are useful in multitier business applications and service-oriented architectures, such as protocols for authentication and encryption, message chunking, extensible headers and cookies, and the ability to have many server applications share a single port and IP address. For these reasons, HTTP is well supported in .NET—both directly, as described in this chapter, and at a higher level, through such technologies as Web API and ASP.NET Core.

.NET Core provides client-side support for FTP, the popular internet protocol for sending and receiving files. Server-side support comes in the form of IIS or Unix-based server software.

As the preceding discussion makes clear, networking is a field that is awash in acronyms. We list the most common in Table 16-1.

Table 16-1. Network acronyms
Acronym Expansion Notes
DNS Domain Name Service Converts between domain names (e.g., ebay.com) and IP addresses (e.g., 199.54.213.2)
FTP File Transfer Protocol Internet-based protocol for sending and receiving files
HTTP Hypertext Transfer Protocol Retrieves web pages and runs web services
IIS Internet Information Services Microsoft’s web server software
IP Internet Protocol Network-layer protocol below TCP and UDP
LAN Local Area Network Most LANs use internet-based protocols such as TCP/IP
POP Post Office Protocol Retrieves internet mail
REST REpresentational State Transfer A popular web service architecture that uses machine-followable links in responses and that can operate over basic HTTP
SMTP Simple Mail Transfer Protocol Sends internet mail
TCP Transmission and Control Protocol Transport-layer internet protocol on top of which most higher-layer services are built
UDP Universal Datagram Protocol Transport-layer internet protocol used for low-overhead services such as VoIP
UNC Universal Naming Convention \computersharenamefilename
URI Uniform Resource Identifier Ubiquitous resource naming system (e.g., http://www.amazon.com or mailto:[email protected])
URL Uniform Resource Locator Technical meaning (fading from use): subset of URI; popular meaning: synonym of URI

Addresses and Ports

For communication to work, a computer or device requires an address. The internet uses two addressing systems:

IPv4
Currently the dominant addressing system; IPv4 addresses are 32 bits wide. When string-formatted, IPv4 addresses are written as four dot-separated decimals (e.g., 101.102.103.104). An address can be unique in the world—or unique within a particular subnet (such as on a corporate network).
IPv6
The newer 128-bit addressing system. Addresses are string-formatted in hexadecimal with a colon separator (e.g., [3EA0:FFFF:198A:E4A3:4FF2:54fA:41BC:8D31]). .NET requires that you add square brackets around the address.

The IPAddress class in the System.Net namespace represents an address in either protocol. It has a constructor accepting a byte array, and a static Parse method accepting a correctly formatted string:

IPAddress a1 = new IPAddress (new byte[] { 101, 102, 103, 104 });
IPAddress a2 = IPAddress.Parse ("101.102.103.104");
Console.WriteLine (a1.Equals (a2));                     // True
Console.WriteLine (a1.AddressFamily);                   // InterNetwork

IPAddress a3 = IPAddress.Parse
  ("[3EA0:FFFF:198A:E4A3:4FF2:54fA:41BC:8D31]");
Console.WriteLine (a3.AddressFamily);   // InterNetworkV6

The TCP and UDP protocols break out each IP address into 65,535 ports, allowing a computer on a single address to run multiple applications, each on its own port. Many applications have standard default port assignments; for instance, HTTP uses port 80; SMTP uses port 25.

Note

The TCP and UDP ports from 49152 to 65535 are officially unassigned, so they are good for testing and small-scale deployments.

An IP address and port combination is represented in .NET by the IPEndPoint class:

IPAddress a = IPAddress.Parse ("101.102.103.104");
IPEndPoint ep = new IPEndPoint (a, 222);           // Port 222
Console.WriteLine (ep.ToString());                 // 101.102.103.104:222
Note

Firewalls block ports. In many corporate environments, only a few ports are open—typically, port 80 (for unencrypted HTTP) and port 443 (for secure HTTP).

URIs

A URI is a specially formatted string that describes a resource on the internet or a LAN, such as a web page, file, or email address. Examples include http://www.ietf.org, ftp://myisp/doc.txt, and [email protected]. The exact formatting is defined by the Internet Engineering Task Force (IETF).

A URI can be broken up into a series of elements—typically, scheme, authority, and path. The Uri class in the System namespace performs just this division, exposing a property for each element, as illustrated in Figure 16-2.

URI properties
Figure 16-2. URI properties
Note

The Uri class is useful when you need to validate the format of a URI string or to split a URI into its component parts. Otherwise, you can treat a URI simply as a string—most networking methods are overloaded to accept either a Uri object or a string.

You can construct a Uri object by passing any of the following strings into its constructor:

  • A URI string, such as http://www.ebay.com or file://janespc/sharedpics/dolphin.jpg

  • An absolute path to a file on your hard disk, such as c:myfilesdata.xlsx or, on Unix, /tmp/myfiles/data.xlsx

  • A UNC path to a file on the LAN, such as \janespcsharedpicsdolphin.jpg

File and UNC paths are automatically converted to URIs: the “file:” protocol is added, and backslashes are converted to forward slashes. The Uri constructors also perform some basic cleanup on your string before creating the Uri, including converting the scheme and hostname to lowercase and removing default and blank port numbers. If you supply a URI string without the scheme, such as “www.test.com,” a UriFormatException is thrown.

Uri has an IsLoopback property, which indicates whether the Uri references the local host (IP address 127.0.0.1), and an IsFile property, which indicates whether the Uri references a local or UNC (IsUnc) path (IsUnc reports false for a Samba share mounted in a Linux filesystem). If IsFile returns true, the LocalPath property returns a version of AbsolutePath that is friendly to the local OS (with slashes or backslashes as appropriate to the OS), on which you can call File.Open.

Instances of Uri have read-only properties. To modify an existing Uri, instantiate a UriBuilder object—this has writable properties and can be converted back via its Uri property.

Uri also provides methods for comparing and subtracting paths:

Uri info = new Uri ("http://www.domain.com:80/info/");
Uri page = new Uri ("http://www.domain.com/info/page.html");

Console.WriteLine (info.Host);     // www.domain.com
Console.WriteLine (info.Port);     // 80
Console.WriteLine (page.Port);     // 80  (Uri knows the default HTTP port)

Console.WriteLine (info.IsBaseOf (page));         // True
Uri relative = info.MakeRelativeUri (page);
Console.WriteLine (relative.IsAbsoluteUri);       // False
Console.WriteLine (relative.ToString());          // page.html

A relative Uri, such as page.html in this example, will throw an exception if you call almost any property or method other than IsAbsoluteUri and ToString(). You can directly instantiate a relative Uri, as follows:

Uri u = new Uri ("page.html", UriKind.Relative);
Note

A trailing slash is significant in a URI and makes a difference as to how a server processes a request if a path component is present.

In a traditional web server, for instance, given the URI http://www.albahari.com/nutshell/, you can expect an HTTP web server to look in the nutshell subdirectory in the site’s web folder and return the default document (usually index.html).

Without the trailing slash, the web server will instead look for a file called nutshell (without an extension) directly in the site’s root folder—which is usually not what you want. If no such file exists, most web servers will assume the user mistyped and will return a 301 Permanent Redirect error, suggesting the client retries with the trailing slash. A .NET HTTP client, by default, will respond transparently to a 301 in the same way as a web browser—by retrying with the suggested URI. This means that if you omit a trailing slash when it should have been included, your request will still work—but will suffer an unnecessary extra round trip.

The Uri class also provides static helper methods such as EscapeUriString(), which converts a string to a valid URL by converting all characters with an ASCII value greater than 127 to hexadecimal representation. The CheckHostName() and CheckSchemeName() methods accept a string and check whether it is syntactically valid for the given property (although they do not attempt to determine whether a host or URI exists).

Client-Side Classes

WebRequest and WebResponse are common base classes for managing both HTTP and FTP client-side activity as well as the “file:” protocol. They encapsulate the “request/response” model that these protocols all share: the client makes a request and then awaits a response from a server.

WebClient is a convenient façade class that does the work of calling WebRequest and WebResponse, saving you some coding. WebClient gives you a choice of dealing in strings, byte arrays, files, or streams; WebRequest and WebResponse support just streams. Unfortunately, you cannot rely entirely on WebClient, because it doesn’t support some features (such as cookies).

HttpClient is a newer API for working with HTTP and is designed to work well with web APIs, REST-based services, and custom authentication schemes. In .NET Framework, HttpClient relied on WebRequest and WebResponse, but from .NET Core 3, it handles HTTP itself.

For simply downloading/uploading a file, string, or byte array, both WebClient and HttpClient are suitable. Both have asynchronous methods, although only WebClient offers progress reporting.

WebClient

Here are the steps in using WebClient:

  1. Instantiate a WebClient object.

  2. Assign the Proxy property.

  3. Assign the Credentials property if authentication is required.

  4. Call a DownloadXXX or UploadXXX method with the desired URI.

Its download methods are as follows:

public void   DownloadFile   (string address, string fileName);
public string DownloadString (string address);
public byte[] DownloadData   (string address);
public Stream OpenRead       (string address);

Each is overloaded to accept a Uri object instead of a string address. The upload methods are similar; their return values contain the response (if any) from the server:

public byte[] UploadFile  (string address, string fileName);
public byte[] UploadFile  (string address, string method, string fileName);
public string UploadString(string address, string data);
public string UploadString(string address, string method, string data);
public byte[] UploadData  (string address, byte[] data);
public byte[] UploadData  (string address, string method, byte[] data);
public byte[] UploadValues(string address, NameValueCollection data);
public byte[] UploadValues(string address, string method,
                                            NameValueCollection data);
public Stream OpenWrite    (string address);
public Stream OpenWrite    (string address, string method);

You can use the UploadValues methods to post values to an HTTP form, with a method argument of "POST". WebClient also has a BaseAddress property; this allows you to specify a string to be prefixed to all addresses, such as http://www.mysite.com/data/.

Here’s how to download the code samples page for this book to a file in the current folder:

WebClient wc = new WebClient { Proxy = null };
wc.DownloadFile ("http://www.albahari.com/nutshell/code.aspx", "code.htm");
Note

WebClient implements IDisposable under duress—by virtue of deriving from Component (this allows it to be sited in the Visual Studio’s Designer’s component tray). Its Dispose method does nothing useful at runtime, however, so you don’t need to dispose WebClient instances.

WebClient provides asynchronous versions of its long-running methods (Chapter 14) that return tasks that you can await:

await wc.DownloadFileTaskAsync ("http://oreilly.com", "webpage.htm");

(The TaskAsync suffix disambiguates these methods from the old EAP-based asynchronous methods that use the Async suffix.) Unfortunately, the new methods don’t support the standard Task-Based Asynchronous Pattern (TAP) for cancellation and progress reporting. Instead, for cancellation you must call the CancelAsync method on the WebClient object, and for progress reporting, handle the DownloadProgress​Changed/UploadProgressChanged event. The following downloads a web page with progress reporting, canceling the download if it takes longer than five seconds:

var wc = new WebClient();

wc.DownloadProgressChanged += (sender, args) => 
  Console.WriteLine (args.ProgressPercentage + "% complete");

Task.Delay (5000).ContinueWith (ant => wc.CancelAsync());
  
await wc.DownloadFileTaskAsync ("http://oreilly.com", "webpage.htm");
Note

When a request is canceled, a WebException is thrown whose Status property is WebExceptionStatus.RequestCanceled. (For historical reasons, an OperationCanceledException is not thrown.)

The progress-related events capture and post to the active synchronization context, so their handlers can update UI controls without needing Dispatcher.Begin​Invoke.

Warning

Using the same WebClient object to perform more than one operation in sequence should be avoided if you’re relying on cancellation or progress reporting because it can result in race conditions.

WebRequest and WebResponse

WebRequest and WebResponse are more complex to use than WebClient but are also more flexible. Here’s how to get started:

  1. Call WebRequest.Create with a URI to instantiate a web request.

  2. Assign the Proxy property.

  3. Assign the Credentials property if authentication is required.

To upload data:

  1. Call GetRequestStream on the request object and then write to the stream. Go to step 5 if a response is expected.

To download data:

  1. Call GetResponse on the request object to instantiate a web response.

  2. Call GetResponseStream on the response object and then read the stream (a StreamReader can help!).

The following downloads and displays the code samples web page (a rewrite of the preceding example):

WebRequest req = WebRequest.Create
                ("http://www.albahari.com/nutshell/code.html");
req.Proxy = null;
using (WebResponse res = req.GetResponse())
using (Stream rs = res.GetResponseStream())
using (FileStream fs = File.Create ("code.html"))
  rs.CopyTo (fs);

Here’s the asynchronous equivalent:

WebRequest req = WebRequest.Create
                ("http://www.albahari.com/nutshell/code.html");
req.Proxy = null;
using (WebResponse res = await req.GetResponseAsync())
using (Stream rs = res.GetResponseStream())
using (FileStream fs = File.Create ("code.html"))
  await rs.CopyToAsync (fs);
Note

The web response object has a ContentLength property, indicating the length of the response stream in bytes, as reported by the server. This value comes from the response headers and might be missing or incorrect. In particular, if an HTTP server chooses the “chunked” mode to break up a large response, the ContentLength value is usually –1. The same can apply with dynamically generated pages.

The static Create method instantiates a subclass of the WebRequest type, such as HttpWebRequest or FtpWebRequest. Its choice of subclass depends on the URI’s prefix and is shown in Table 16-2.

Table 16-2. URI prefixes and web request types
Prefix Web request type
http: or https: HttpWebRequest
ftp: FtpWebRequest
file: FileWebRequest
Note

Casting a web request object to its concrete type (HttpWebRequest or FtpWebRequest) allows you to access its protocol-specific features.

You can also register your own prefixes by calling WebRequest.RegisterPrefix. This requires a prefix along with a factory object with a Create method that instantiates an appropriate web request object.

The “https:” protocol is for secure (encrypted) HTTP, via Secure Sockets Layer (SSL). Both WebClient and WebRequest activate SSL transparently upon seeing this prefix. The “file:” protocol simply forwards requests to a FileStream object. Its purpose is in meeting a consistent protocol for reading a URI, whether it be a web page, FTP site, or file path.

WebRequest has a Timeout property, in milliseconds. If a timeout occurs, a WebException is thrown with a Status property of WebExceptionStatus.Timeout. The default timeout is 100 seconds for HTTP and infinite for FTP.

You cannot recycle a WebRequest object for multiple requests—each instance is good for one job only.

HttpClient

HttpClient provides another layer on top of HttpWebRequest and HttpWebResponse. It was written in response to the growth of HTTP-based web APIs and REST services to provide a better experience than WebClient when dealing with protocols more elaborate than simply fetching a web page; specifically:

  • A single HttpClient instance supports concurrent requests. To get concurrency with WebClient, you need to create a fresh instance per concurrent request, which can get awkward when you introduce custom headers, cookies, and authentication schemes.

  • HttpClient lets you write and plug in custom message handlers. This enables mocking in unit tests, and the creation of custom pipelines (for logging, compression, encryption, and so on). Unit-testing code that calls WebClient is a pain.

  • HttpClient has a richer and extensible type system for headers and content.

Note

HttpClient is not a complete replacement for WebClient, because it doesn’t directly support progress reporting. WebClient also has the advantage of supporting FTP, file://, and custom URI schemes. It’s also available in older runtime versions.

For a solution to progress reporting with HttpClient, see HttpClient with Progress.linq at http://www.albahari.com/nutshell/code.aspx or via LINQPad’s interactive samples gallery.

The simplest way to use HttpClient is to instantiate it and then call one of its Get* methods, passing in a URI:

string html = await new HttpClient().GetStringAsync ("http://linqpad.net");

(There’s also GetByteArrayAsync and GetStreamAsync.) All I/O-bound methods in HttpClient are asynchronous (there are no synchronous equivalents).

Unlike with WebClient, to get the best performance with HttpClient, you must reuse the same instance (otherwise things such as DNS resolution can be unnecessarily repeated and sockets are held open longer than necessary). HttpClient permits concurrent operations, so the following is legal and downloads two web pages at once:

var client = new HttpClient();
var task1 = client.GetStringAsync ("http://www.linqpad.net");
var task2 = client.GetStringAsync ("http://www.albahari.com");
Console.WriteLine (await task1);
Console.WriteLine (await task2);

HttpClient has a Timeout property and a BaseAddress property that prefixes a URI to every request. HttpClient is somewhat of a thin shell: most of the other properties that you might expect to find here are defined in another class called HttpClient​Handler. To access this class, you instantiate it and then pass the instance into HttpClient’s constructor:

var handler = new HttpClientHandler { UseProxy = false };
var client = new HttpClient (handler);
...

In this example, we told the handler to disable proxy support. There are also properties to control cookies, automatic redirection, authentication, and so on (we describe these in the following sections as well as in “Working with HTTP”).

GetAsync and response messages

The GetStringAsync, GetByteArrayAsync, and GetStreamAsync methods are convenient shortcuts for calling the more general GetAsync method, which returns a response message:

var client = new HttpClient();
// The GetAsync method also accepts a CancellationToken.
HttpResponseMessage response = await client.GetAsync ("http://...");
response.EnsureSuccessStatusCode();
string html = await response.Content.ReadAsStringAsync();

HttpResponseMessage exposes properties for accessing the headers (see “Working with HTTP”) and the HTTP StatusCode. Unlike with WebClient, an unsuccessful status code such as 404 (not found) doesn’t cause an exception to be thrown unless you explicitly call Ensure​SuccessStatusCode. Communication or DNS errors, however, do throw exceptions (see “Exception Handling”).

HttpContent has a CopyToAsync method for writing to another stream, which is useful in writing the output to a file:

using (var fileStream = File.Create ("linqpad.html"))
  await response.Content.CopyToAsync (fileStream);

GetAsync is one of four methods corresponding to HTTP’s four verbs (the others are PostAsync, PutAsync and DeleteAsync). We demonstrate PostAsync later in “Uploading Form Data”.

SendAsync and request messages

The four methods just described are all shortcuts for calling SendAsync, the single low-level method into which everything else feeds. To use this, you first construct an HttpRequestMessage:

var client = new HttpClient();
var request = new HttpRequestMessage (HttpMethod.Get, "http://...");
HttpResponseMessage response = await client.SendAsync (request);
response.EnsureSuccessStatusCode();
...

Instantiating a HttpRequestMessage object means that you can customize properties of the request, such as the headers (see “Headers”) and the content itself, allowing you to upload data.

Uploading data and HttpContent

After instantiating an HttpRequestMessage object, you can upload content by assigning its Content property. The type for this property is an abstract class called HttpContent. .NET includes the following concrete subclasses for different kinds of content (you can also write your own):

For example:

var client = new HttpClient (new HttpClientHandler { UseProxy = false });
var request = new HttpRequestMessage (
  HttpMethod.Post, "http://www.albahari.com/EchoPost.aspx");
request.Content = new StringContent ("This is a test");
HttpResponseMessage response = await client.SendAsync (request);
response.EnsureSuccessStatusCode();
Console.WriteLine (await response.Content.ReadAsStringAsync());

HttpMessageHandler

We said previously that most of the properties for customizing requests are defined not in HttpClient, but in HttpClientHandler. The latter is actually a subclass of the abstract HttpMessageHandler class, defined as follows:

public abstract class HttpMessageHandler : IDisposable
{
  protected internal abstract Task<HttpResponseMessage> SendAsync
    (HttpRequestMessage request, CancellationToken cancellationToken);

  public void Dispose();
  protected virtual void Dispose (bool disposing);
}

The SendAsync method is called from HttpClient’s SendAsync method.

HttpMessageHandler is simple enough to subclass easily and offers an extensibility point into HttpClient.

Unit testing and mocking

We can subclass HttpMessageHandler to create a mocking handler to assist with unit testing:

class MockHandler : HttpMessageHandler
{
  Func <HttpRequestMessage, HttpResponseMessage> _responseGenerator;
    
  public MockHandler
    (Func <HttpRequestMessage, HttpResponseMessage> responseGenerator)
  {
    _responseGenerator = responseGenerator;
  }
    
  protected override Task <HttpResponseMessage> SendAsync
    (HttpRequestMessage request, CancellationToken cancellationToken)
  {
    cancellationToken.ThrowIfCancellationRequested();
    var response = _responseGenerator (request);
    response.RequestMessage = request;
    return Task.FromResult (response);
  }
}

Its constructor accepts a function that tells the mocker how to generate a response from a request. This is the most versatile approach because the same handler can test multiple requests.

SendAsync is synchronous by virtue of Task.FromResult. We could have maintained asynchrony by having our response generator return a Task<HttpResponseMessage>, but this is pointless given that we can expect a mocking function to be short-running. Here’s how to use our mocking handler:

var mocker = new MockHandler (request => 
  new HttpResponseMessage (HttpStatusCode.OK)
  {
    Content = new StringContent ("You asked for " + request.RequestUri)
  });

var client = new HttpClient (mocker); 
var response = await client.GetAsync ("http://www.linqpad.net");
string result = await response.Content.ReadAsStringAsync();
Assert.AreEqual ("You asked for http://www.linqpad.net/", result);

(Assert.AreEqual is a method you’d expect to find in a unit-testing framework such as NUnit.)

Chaining handlers with DelegatingHandler

You can create a message handler that calls another (resulting in a chain of handlers) by subclassing DelegatingHandler. You can use this to implement custom authentication, compression, and encryption protocols. The following demonstrates a simple logging handler:

class LoggingHandler : DelegatingHandler 
{
  public LoggingHandler (HttpMessageHandler nextHandler)
  {
     InnerHandler = nextHandler;
  }
    
  protected async override Task <HttpResponseMessage> SendAsync
    (HttpRequestMessage request, CancellationToken cancellationToken)
  {
    Console.WriteLine ("Requesting: " + request.RequestUri);
    var response = await base.SendAsync (request, cancellationToken);
    Console.WriteLine ("Got response: " + response.StatusCode);
    return response;
  }
}

Notice that we’ve maintained asynchrony in overriding SendAsync. Introducing the async modifier when overriding a task-returning method is perfectly legal—and desirable in this case.

A better solution than writing to the Console would be to have the constructor accept some kind of logging object. Better still would be to accept a couple of Action<T> delegates that tell it how to log the request and response objects.

Proxies

A proxy server is an intermediary through which HTTP and FTP requests can be routed. Organizations sometimes set up a proxy server as the only means by which employees can access the internet—primarily because it simplifies security. A proxy has an address of its own and can demand authentication so that only selected users on the LAN can access the internet.

You can instruct a WebClient or WebRequest object to route requests through a proxy server with a WebProxy object:

// Create a WebProxy with the proxy's IP address and port. You can
// optionally set Credentials if the proxy needs a username/password.

WebProxy p = new WebProxy ("192.178.10.49", 808);
p.Credentials = new NetworkCredential ("username", "password");
// or:
p.Credentials = new NetworkCredential ("username", "password", "domain");

WebClient wc = new WebClient();
wc.Proxy = p;
  ...

// Same procedure with a WebRequest object:
WebRequest req = WebRequest.Create ("...");
req.Proxy = p;

To use a proxy with HttpClient, first create an HttpClientHandler, assign its Proxy property, and then feed that into HttpClient’s constructor:

WebProxy p = new WebProxy ("192.178.10.49", 808);
p.Credentials = new NetworkCredential ("username", "password", "domain");

var handler = new HttpClientHandler { Proxy = p };
var client = new HttpClient (handler);
...
Note

If you know there’s no proxy, it’s worth setting the Proxy property to null on WebClient and WebRequest objects. Otherwise, the runtime might attempt to “autodetect” your proxy settings, adding up to 30 seconds to your request. If you’re wondering why your web requests execute slowly, this is probably it!

HttpClientHandler also has a UseProxy property that you can assign to false instead of nulling out the Proxy property to defeat autodetection.

If you supply a domain when constructing the NetworkCredential, Windows-based authentication protocols are used. To use the currently authenticated Windows user, assign the static CredentialCache.DefaultNetworkCredentials value to the proxy’s Credentials property.

As an alternative to repeatedly setting the Proxy, you can set the global default as follows:

WebRequest.DefaultWebProxy = myWebProxy;

Or, like this:

WebRequest.DefaultWebProxy = null;

Whatever you set applies for the life of the application domain (unless some other code changes it!).

Authentication

You can supply a username and password to an HTTP or FTP site by creating a NetworkCredential object and assigning it to the Credentials property of WebClient or WebRequest:

WebClient wc = new WebClient { Proxy = null };
wc.BaseAddress = "ftp://ftp.myserver.com";

// Authenticate, then upload and download a file to the FTP server.
// The same approach also works for HTTP and HTTPS.

string username = "myuser";
string password = "mypassword";
wc.Credentials = new NetworkCredential (username, password);

wc.DownloadFile ("guestbook.txt", "guestbook.txt");

string data = "Hello from " + Environment.UserName + "!
";
File.AppendAllText ("guestbook.txt", data);

wc.UploadFile ("guestbook.txt", "guestbook.txt");

HttpClient exposes the same Credentials property through HttpClientHandler:

var handler = new HttpClientHandler();
handler.Credentials = new NetworkCredential (username, password);
var client = new HttpClient (handler);
...

This works with dialog-based authentication protocols, such as Basic and Digest, and is extensible through the AuthenticationManager class. It also supports Windows NTLM and Kerberos (if you include a domain name when constructing the NetworkCredential object). If you want to use the currently authenticated Windows user, you can leave the Credentials property null and instead set UseDefaultCredentials true.

The authentication is ultimately handled by a WebRequest subtype (in this case, FtpWebRequest), which automatically negotiates a compatible protocol. In the case of HTTP, there can be a choice: if you examine the initial response from a Microsoft Exchange server web mail page, for instance, it might contain the following headers:

HTTP/1.1 401 Unauthorized
Content-Length: 83
Content-Type: text/html
Server: Microsoft-IIS/6.0
WWW-Authenticate: Negotiate
WWW-Authenticate: NTLM
WWW-Authenticate: Basic realm="exchange.somedomain.com"
X-Powered-By: ASP.NET
Date: Sat, 05 Aug 2006 12:37:23 GMT

The 401 code signals that authorization is required; the “WWW-Authenticate” headers indicate what authentication protocols are understood. If you configure a WebClient or WebRequest object with the correct username and password, however, this message will be hidden from you because the runtime responds automatically by choosing a compatible authentication protocol and then resubmitting the original request with an extra header. Here’s an example:

Authorization: Negotiate TlRMTVNTUAAABAAAt5II2gjACDArAAACAwACACgAAAAQ
ATmKAAAAD0lVDRdPUksHUq9VUA==

This mechanism provides transparency, but generates an extra round trip with each request. You can avoid the extra round trips on subsequent requests to the same URI by setting the PreAuthenticate property to true. This property is defined on the WebRequest class (and works only in the case of HttpWebRequest). WebClient doesn’t support this feature at all.

CredentialCache

You can force a particular authentication protocol with a CredentialCache object. A credential cache contains one or more NetworkCredential objects, each keyed to a particular protocol and URI prefix. For example, you might want to avoid the Basic protocol when logging into an Exchange Server because it transmits passwords in plain text:

CredentialCache cache = new CredentialCache();
Uri prefix = new Uri ("http://exchange.somedomain.com");
cache.Add (prefix, "Digest",  new NetworkCredential ("joe", "passwd"));
cache.Add (prefix, "Negotiate", new NetworkCredential ("joe", "passwd"));

WebClient wc = new WebClient();
wc.Credentials = cache;
...

An authentication protocol is specified as a string. The valid values are as follows:

Basic, Digest, NTLM, Kerberos, Negotiate

In this particular example, WebClient will choose Negotiate, because the server didn’t indicate that it supported Digest in its authentication headers. Negotiate is a Windows protocol that currently boils down to either Kerberos or NTLM, depending on the capabilities of the server, but ensures forward compatibility of your application when future security standards are deployed.

The static CredentialCache.DefaultNetworkCredentials property allows you to add the currently authenticated Windows user to the credential cache without having to specify a password:

cache.Add (prefix, "Negotiate", CredentialCache.DefaultNetworkCredentials);

Authenticating via headers with HttpClient

If you’re using HttpClient, another way to authenticate is to set the authentication header directly:

var client = new HttpClient();
client.DefaultRequestHeaders.Authorization = 
  new AuthenticationHeaderValue ("Basic",
    Convert.ToBase64String (Encoding.UTF8.GetBytes ("username:password")));
...

This strategy also works with custom authentication systems such as OAuth. We discuss headers in more detail soon.

Exception Handling

WebRequest, WebResponse, WebClient, and their streams all throw a WebException in the case of a network or protocol error. HttpClient does the same but then wraps the WebException in an HttpRequestException. You can determine the specific error via the WebException’s Status property; this returns a WebExceptionStatus enum that has the following members:

CacheEntryNotFound
ConnectFailure
ConnectionClosed
KeepAliveFailure
MessageLengthLimitExceeded
NameResolutionFailure
Pending
PipelineFailure
ProtocolError
ProxyNameResolutionFailure
ReceiveFailure
RequestCanceled
RequestProhibitedByCachePolicy
RequestProhibitedByProxy
SecureChannelFailure
SendFailure
ServerProtocolViolation
Success
Timeout
TrustFailure
UnknownError

An invalid domain name causes a NameResolutionFailure; a dead network causes a ConnectFailure; a request exceeding WebRequest.Timeout milliseconds causes a Timeout.

Errors such as “Page not found,” “Moved Permanently,” and “Not Logged In” are specific to the HTTP or FTP protocols, and so are all lumped together under the ProtocolError status. With HttpClient, these errors are not thrown unless you call EnsureSuccessStatusCode on the response object. Prior to doing so, you can get the specific status code by querying the StatusCode property:

var client = new HttpClient();
var response = await client.GetAsync ("http://linqpad.net/foo");
HttpStatusCode responseStatus = response.StatusCode;

With WebClient and WebRequest/WebResponse, you must actually catch the Web​Exception and then:

  1. Cast the WebException’s Response property to HttpWebResponse or FtpWeb​Res⁠ponse.

  2. Examine the response object’s Status property (an HttpStatusCode or Ftp​Sta⁠tusCode enum) and/or its StatusDescription property (string).

For example:

WebClient wc = new WebClient { Proxy = null };
try
{
  string s = wc.DownloadString ("http://www.albahari.com/notthere");
}
catch (WebException ex)
{
  if (ex.Status == WebExceptionStatus.NameResolutionFailure)
    Console.WriteLine ("Bad domain name");
  else if (ex.Status == WebExceptionStatus.ProtocolError)
  {
    HttpWebResponse response = (HttpWebResponse) ex.Response;
    Console.WriteLine (response.StatusDescription);      // "Not Found"
    if (response.StatusCode == HttpStatusCode.NotFound)
      Console.WriteLine ("Not there!");                  // "Not there!"
  }
  else throw;
}
Note

If you want the three-digit status code, such as 401 or 404, simply cast the HttpStatusCode or FtpStatusCode enum to an integer.

By default, you’ll never get a redirection error because WebClient and WebRequest automatically follow redirection responses. You can switch off this behavior in a WebRequest object by setting AllowAutoRedirect to false.

The redirection errors are 301 (Moved Permanently), 302 (Found/Redirect), and 307 (Temporary Redirect).

If an exception is thrown because you’ve incorrectly used the WebClient or WebRequest classes, it will more likely be an InvalidOperationException or ProtocolViolationException than a WebException.

Working with HTTP

This section describes HTTP-specific request and response features of WebClient, HttpWebRequest/HttpWebResponse, and the HttpClient class.

Headers

WebClient, WebRequest, and HttpClient all let you add custom HTTP headers as well as enumerate the headers in a response. A header is simply a key/value pair containing metadata, such as the message content type or server software. Here’s how to add a custom header to a request and then list all headers in a response message in a WebClient:

WebClient wc = new WebClient { Proxy = null };
wc.Headers.Add ("CustomHeader", "JustPlaying/1.0");
wc.DownloadString ("http://www.oreilly.com");

foreach (string name in wc.ResponseHeaders.Keys)
  Console.WriteLine (name + "=" + wc.ResponseHeaders [name]);

Age=51
X-Cache=HIT from oregano.bp
X-Cache-Lookup=HIT from oregano.bp:3128
Connection=keep-alive
Accept-Ranges=bytes
Content-Length=95433
Content-Type=text/html
...

HttpClient instead exposes strongly typed collections with properties for standard HTTP headers. The DefaultRequestHeaders property is for headers that apply to every request:

var client = new HttpClient (handler);

client.DefaultRequestHeaders.UserAgent.Add (
  new ProductInfoHeaderValue ("VisualStudio", "2015"));

client.DefaultRequestHeaders.Add ("CustomHeader", "VisualStudio/2015");

The Headers property on the HttpRequestMessage class, however, is for headers specific to a request.

Query Strings

A query string is simply a string appended to a URI with a question mark, used to send simple data to the server. You can specify multiple key/value pairs in a query string with the following syntax:

?key1=value1&key2=value2&key3=value3...

WebClient provides an easy way to add query strings through a dictionary-style property. The following searches Google for the word “WebClient”, displaying the result page in French:

WebClient wc = new WebClient { Proxy = null };
wc.QueryString.Add ("q", "WebClient");     // Search for "WebClient"
wc.QueryString.Add ("hl", "fr");           // Display page in French
wc.DownloadFile ("http://www.google.com/search", "results.html");

To achieve the same result with WebRequest or with HttpClient, you must manually append a correctly formatted string to the request URI:

string requestURI = "http://www.google.com/search?q=WebClient&hl=fr";

If there’s a possibility of your query including symbols or spaces, you can use Uri’s EscapeDataString method to create a legal URI:

string search = Uri.EscapeDataString ("(WebClient OR HttpClient)");
string language = Uri.EscapeDataString ("fr");
string requestURI = "http://www.google.com/search?q=" + search +
                    "&hl=" + language;

This resultant URI is:

http://www.google.com/search?q=(WebClient%20OR%20HttpClient)&hl=fr

(EscapeDataString is similar to EscapeUriString except that it also encodes characters such as & and =, which would otherwise mess up the query string.)

Uploading Form Data

WebClient provides UploadValues methods for posting data to an HTML form:

WebClient wc = new WebClient { Proxy = null };

var data = new System.Collections.Specialized.NameValueCollection();
data.Add ("Name", "Joe Albahari");
data.Add ("Company", "O'Reilly");

byte[] result = wc.UploadValues ("http://www.albahari.com/EchoPost.aspx",
                                 "POST", data);

Console.WriteLine (Encoding.UTF8.GetString (result));

The keys in the NameValueCollection, such as searchtextbox and searchMode, correspond to the names of input boxes on the HTML form.

Uploading form data is more work via WebRequest. (You’ll need to take this route if you need to use features such as cookies.) Here’s the procedure:

  1. Set the request’s ContentType to "application/x-www-form-urlencoded" and its Method to "POST".

  2. Build a string containing the data to upload, encoded as follows:

    name1=value1&name2=value2&name3=value3...
  3. Convert the string to a byte array, with Encoding.UTF8.GetBytes.

  4. Set the web request’s ContentLength property to the byte array length.

  5. Call GetRequestStream on the web request and write the data array.

  6. Call GetResponse to read the server’s response.

Here’s the previous example written with WebRequest:

var req = WebRequest.Create ("http://www.albahari.com/EchoPost.aspx");
req.Proxy = null;
req.Method = "POST";
req.ContentType = "application/x-www-form-urlencoded";

string reqString = "Name=Joe+Albahari&Company=O'Reilly";
byte[] reqData = Encoding.UTF8.GetBytes (reqString);
req.ContentLength = reqData.Length;

using (Stream reqStream = req.GetRequestStream())
  reqStream.Write (reqData, 0, reqData.Length);

using (WebResponse res = req.GetResponse())
using (Stream resSteam = res.GetResponseStream())
using (StreamReader sr = new StreamReader (resSteam))
  Console.WriteLine (sr.ReadToEnd());

With HttpClient, you instead create and populate FormUrlEncodedContent object, which you can then either pass into the PostAsync method or assign to a request’s Content property:

string uri = "http://www.albahari.com/EchoPost.aspx";
var client = new HttpClient();
var dict = new Dictionary<string,string> 
{
    { "Name", "Joe Albahari" },
    { "Company", "O'Reilly" }
};
var values = new FormUrlEncodedContent (dict);
var response = await client.PostAsync (uri, values);
response.EnsureSuccessStatusCode();
Console.WriteLine (await response.Content.ReadAsStringAsync());

Cookies

A cookie is a name/value string pair that an HTTP server sends to a client in a response header. A web browser client typically remembers cookies and replays them to the server in each subsequent request (to the same address) until their expiry. A cookie allows a server to know whether it’s talking to the same client it was a minute ago—or yesterday—without needing a messy query string in the URI.

By default, HttpWebRequest ignores any cookies received from the server. To accept cookies, create a CookieContainer object and assign it to the WebRequest. The cookies received in a response can then be enumerated:

var cc = new CookieContainer();

var request = (HttpWebRequest) WebRequest.Create ("http://www.google.com");
request.Proxy = null;
request.CookieContainer = cc;
using (var response = (HttpWebResponse) request.GetResponse())
{
  foreach (Cookie c in response.Cookies)
  {
    Console.WriteLine (" Name:   " + c.Name);
    Console.WriteLine (" Value:  " + c.Value);
    Console.WriteLine (" Path:   " + c.Path);
    Console.WriteLine (" Domain: " + c.Domain);
  }
  // Read response stream...
}

 Name:   PREF
 Value:  ID=6b10df1da493a9c4:TM=1179025486:LM=1179025486:S=EJCZri0aWEHlk4tt
 Path:   /
 Domain: .google.com

To do the same with HttpClient, first instantiate a HttpClientHandler:

var cc = new CookieContainer();
var handler = new HttpClientHandler();
handler.CookieContainer = cc;
var client = new HttpClient (handler);
...

The WebClient façade class does not support cookies.

To replay the received cookies in future requests, simply assign the same CookieContainer object to each new WebRequest object, or with HttpClient, keep using the same object to make requests. Alternatively, you can start with a fresh CookieContainer and then add cookies manually, as follows:

Cookie c = new Cookie ("PREF",
                       "ID=6b10df1da493a9c4:TM=1179...",
                       "/",
                       ".google.com");
freshCookieContainer.Add (c);

The third and fourth arguments indicate the path and domain of the originator. A CookieContainer on the client can house cookies from many different places; Web​Request sends only those cookies whose path and domain match those of the server.

Writing an HTTP Server

You can write your own .NET HTTP server with the HttpListener class. The following is a simple server that listens on port 51111, waits for a single client request, and then returns a one-line reply:

using var server = new SimpleHttpServer();

// Make a client request:
Console.WriteLine (new WebClient().DownloadString
  ("http://localhost:51111/MyApp/Request.txt"));

class SimpleHttpServer : IDisposable
{
  readonly HttpListener listener = new HttpListener();
  
  public SimpleHttpServer() => ListenAsync();  
  async void ListenAsync()
  {
    listener.Prefixes.Add ("http://localhost:51111/MyApp/");  // Listen on
    listener.Start();                                         // port 51111

    // Await a client request:
    HttpListenerContext context = await listener.GetContextAsync();

    // Respond to the request:
    string msg = "You asked for: " + context.Request.RawUrl;
    context.Response.ContentLength64 = Encoding.UTF8.GetByteCount (msg);
    context.Response.StatusCode = (int)HttpStatusCode.OK;

    using (Stream s = context.Response.OutputStream)
    using (StreamWriter writer = new StreamWriter (s))
      await writer.WriteAsync (msg);
  }

  public void Dispose() => listener.Close();
}

OUTPUT: You asked for: /MyApp/Request.txt

On Windows, HttpListener does not internally use .NET Socket objects; it instead calls the Windows HTTP Server API. This allows many applications on a computer to listen on the same IP address and port—as long as each registers different address prefixes. In our example, we registered the prefix http://localhost/myapp, so another application would be free to listen on the same IP and port on another prefix, such as http://localhost/anotherapp. This is of value because opening new ports on corporate firewalls can be politically arduous.

HttpListener waits for the next client request when you call GetContext, returning an object with Request and Response properties. Each is analogous to a WebRequest and WebResponse object, but from the server’s perspective. You can read and write headers and cookies, for instance, to the request and response objects, much as you would at the client end.

You can choose how fully to support features of the HTTP protocol, based on your anticipated client audience. At a bare minimum, you should set the content length and status code on each request.

Here’s a very simple web page server, written asynchronously:

using System;
using System.IO;
using System.Net;
using System.Text;
using System.Threading.Tasks;

class WebServer
{
  HttpListener _listener;
  string _baseFolder;      // Your web page folder.

  public WebServer (string uriPrefix, string baseFolder)
  {
    _listener = new HttpListener();
    _listener.Prefixes.Add (uriPrefix);
    _baseFolder = baseFolder;
  }

  public async void Start()
  {
    _listener.Start();
    while (true)
      try 
      {
        var context = await _listener.GetContextAsync();
        Task.Run (() => ProcessRequestAsync (context));
      }
      catch (HttpListenerException)     { break; }   // Listener stopped.
      catch (InvalidOperationException) { break; }   // Listener stopped.
  }

  public void Stop() => _listener.Stop();

  async void ProcessRequestAsync (HttpListenerContext context)
  {
    try
    {
      string filename = Path.GetFileName (context.Request.RawUrl);
      string path = Path.Combine (_baseFolder, filename);
      byte[] msg;
      if (!File.Exists (path))
      {
        Console.WriteLine ("Resource not found: " + path);
        context.Response.StatusCode = (int) HttpStatusCode.NotFound;
        msg = Encoding.UTF8.GetBytes ("Sorry, that page does not exist");
      }
      else
      {
        context.Response.StatusCode = (int) HttpStatusCode.OK;
        msg = File.ReadAllBytes (path);
      }
      context.Response.ContentLength64 = msg.Length;
      using (Stream s = context.Response.OutputStream)
        await s.WriteAsync (msg, 0, msg.Length);
    }
    catch (Exception ex) { Console.WriteLine ("Request error: " + ex); }
  }
}

The following code sets things in motion:

// Listen on port 51111, serving files in d:webroot:
var server = new WebServer ("http://localhost:51111/", @"d:webroot");
try
{
  server.Start();
  Console.WriteLine ("Server running... press Enter to stop");
  Console.ReadLine();
}
finally { server.Stop(); }

You can test this at the client end with any web browser; the URI in this case will be http://localhost:51111/ plus the name of the web page.

Note

HttpListener will not start if other software is competing for the same port (unless that software also uses the Windows HTTP Server API). Examples of applications that might listen on the default port 80 include a web server or a peer-to-peer program such as Skype.

Our use of asynchronous functions makes this server scalable and efficient. Starting this from a user interface (UI) thread, however, would hinder scalability because for each request, execution would bounce back to the UI thread after each await. Incurring such overhead is particularly pointless given that we don’t have shared state, so in a UI scenario we’d get off the UI thread, either like this

Task.Run (Start);

or by calling ConfigureAwait(false) after calling GetContextAsync.

Note that we used Task.Run to call ProcessRequestAsync even though the method was already asynchronous. This allows the caller to process another request immediately rather than having to first wait out the synchronous phase of the method (up until the first await).

Using FTP

For simple FTP upload and download operations, you can use WebClient, as we did previously:

WebClient wc = new WebClient { Proxy = null };
wc.Credentials = new NetworkCredential ("myuser", "mypassword");
wc.BaseAddress = "ftp://ftp.myserver.com";
wc.UploadString ("tempfile.txt", "hello!");
Console.WriteLine (wc.DownloadString ("tempfile.txt"));   // hello!

There’s more to FTP, however, than just uploading and downloading files. The protocol also defines a set of commands or “methods,” which are exposed as string constants in WebRequestMethods.Ftp:

AppendFile
DeleteFile
DownloadFile
GetDateTimestamp
GetFileSize
ListDirectory
ListDirectoryDetails
MakeDirectory
PrintWorkingDirectory
RemoveDirectory
Rename
UploadFile
UploadFileWithUniqueName

To run one of these commands, you assign its string constant to the web request’s Method property and then call GetResponse(). Here’s how to get a directory listing:

var req = (FtpWebRequest) WebRequest.Create ("ftp://ftp.myserver.com");
req.Proxy = null;
req.Credentials = new NetworkCredential ("myuser", "mypassword");
req.Method = WebRequestMethods.Ftp.ListDirectory;

using (WebResponse resp = req.GetResponse())
using (StreamReader reader = new StreamReader (resp.GetResponseStream()))
  Console.WriteLine (reader.ReadToEnd());

RESULT:
.
..
guestbook.txt
tempfile.txt
test.doc

In the case of getting a directory listing, we needed to read the response stream to get the result. Most other commands, however, don’t require this step. For instance, to get the result of the GetFileSize command, just query the response’s Content​Length property:

var req = (FtpWebRequest) WebRequest.Create (
                          "ftp://ftp.myserver.com/tempfile.txt");
req.Proxy = null;
req.Credentials = new NetworkCredential ("myuser", "mypassword");

req.Method = WebRequestMethods.Ftp.GetFileSize;

using (WebResponse resp = req.GetResponse())
  Console.WriteLine (resp.ContentLength);            // 6

The GetDateTimestamp command works in a similar way except that you query the response’s LastModified property. This requires that you cast to FtpWebResponse:

...
req.Method = WebRequestMethods.Ftp.GetDateTimestamp;

using (var resp = (FtpWebResponse) req.GetResponse() )
  Console.WriteLine (resp.LastModified);

To use the Rename command, you must populate the request’s RenameTo property with the new filename (without a directory prefix). For example, to rename a file in the incoming directory from tempfile.txt to deleteme.txt:

var req = (FtpWebRequest) WebRequest.Create (
                          "ftp://ftp.myserver.com/tempfile.txt");
req.Proxy = null;
req.Credentials = new NetworkCredential ("myuser", "mypassword");

req.Method = WebRequestMethods.Ftp.Rename;
req.RenameTo = "deleteme.txt";

req.GetResponse().Close();        // Perform the rename

Here’s how to delete a file:

var req = (FtpWebRequest) WebRequest.Create (
                          "ftp://ftp.myserver.com/deleteme.txt");
req.Proxy = null;
req.Credentials = new NetworkCredential ("myuser", "mypassword");

req.Method = WebRequestMethods.Ftp.DeleteFile;

req.GetResponse().Close();        // Perform the deletion
Note

In all these examples, you would typically use an exception-handling block to catch network and protocol errors. A typical catch block looks like this:

catch (WebException ex)
{
   if (ex.Status == WebExceptionStatus.ProtocolError)
   {
     // Obtain more detail on error:
     var response = (FtpWebResponse) ex.Response;
     FtpStatusCode errorCode = response.StatusCode;
     string errorMessage = response.StatusDescription;
     ...
   }
   ...
 }

Using DNS

The static Dns class encapsulates the DNS, which converts between a raw IP address, such as 66.135.192.87, and a human-friendly domain name, such as ebay.com.

The GetHostAddresses method converts from domain name to IP address (or addresses):

foreach (IPAddress a in Dns.GetHostAddresses ("albahari.com"))
  Console.WriteLine (a.ToString());     // 205.210.42.167

The GetHostEntry method goes the other way around, converting from address to domain name:

IPHostEntry entry = Dns.GetHostEntry ("205.210.42.167");
Console.WriteLine (entry.HostName);                    // albahari.com

GetHostEntry also accepts an IPAddress object, so you can specify an IP address as a byte array:

IPAddress address = new IPAddress (new byte[] { 205, 210, 42, 167 });
IPHostEntry entry = Dns.GetHostEntry (address);
Console.WriteLine (entry.HostName);                    // albahari.com

Domain names are automatically resolved to IP addresses when you use a class such as WebRequest or TcpClient. However, if you plan to make many network requests to the same address over the life of an application, you can sometimes improve performance by first using Dns to explicitly convert the domain name into an IP address, and then communicating directly with the IP address from that point on. This prevents repeated round-tripping to resolve the same domain name, and it can be of benefit when dealing at the transport layer (via TcpClient, UdpClient, or Socket).

The DNS class also provides awaitable task-based asynchronous methods:

foreach (IPAddress a in await Dns.GetHostAddressesAsync ("albahari.com"))
  Console.WriteLine (a.ToString());

Sending Mail with SmtpClient

The SmtpClient class in the System.Net.Mail namespace allows you to send mail messages through the ubiquitous Simple Mail Transfer Protocol, or SMTP. To send a simple text message, instantiate SmtpClient, set its Host property to your SMTP server address, and then call Send:

SmtpClient client = new SmtpClient();
client.Host = "mail.myserver.com";
client.Send ("[email protected]", "[email protected]", "subject", "body");

Constructing a MailMessage object exposes further options, including the ability to add attachments:

SmtpClient client = new SmtpClient();
client.Host = "mail.myisp.net";
MailMessage mm = new MailMessage();

mm.Sender = new MailAddress ("[email protected]", "Kay");
mm.From   = new MailAddress ("[email protected]", "Kay");
mm.To.Add  (new MailAddress ("[email protected]", "Bob"));
mm.CC.Add  (new MailAddress ("[email protected]", "Dan"));
mm.Subject = "Hello!";
mm.Body = "Hi there. Here's the photo!";
mm.IsBodyHtml = false;
mm.Priority = MailPriority.High;

Attachment a = new Attachment ("photo.jpg",
                               System.Net.Mime.MediaTypeNames.Image.Jpeg);
mm.Attachments.Add (a);
client.Send (mm);

To frustrate spammers, most SMTP servers on the internet will accept connections only from authenticated connections and require communication over SSL:

var client = new SmtpClient ("smtp.myisp.com", 587)
{
  Credentials = new NetworkCredential ("[email protected]", "MySecurePass"),
  EnableSsl = true
};
client.Send ("[email protected]", "[email protected]", "Subject", "Body");
Console.WriteLine ("Sent");

By changing the DeliveryMethod property, you can instruct the SmtpClient to instead use IIS to send mail messages or simply to write each message to an .eml file in a specified directory. This can be useful during development:

SmtpClient client = new SmtpClient();
client.DeliveryMethod = SmtpDeliveryMethod.SpecifiedPickupDirectory;
client.PickupDirectoryLocation = @"c:mail";

Using TCP

TCP and UDP constitute the transport layer protocols on top of which most internet—and LAN—services are built. HTTP (version 2 and below), FTP, and SMTP use TCP; DNS and HTTP version 3 use UDP. TCP is connection-oriented and includes reliability mechanisms; UDP is connectionless, has a lower overhead, and supports broadcasting. BitTorrent uses UDP, as does Voice over IP (VoIP).

The transport layer offers greater flexibility—and potentially improved performance—over the higher layers, but it requires that you handle such tasks as authentication and encryption yourself.

With TCP in .NET, you have a choice of either the easier-to-use TcpClient and TcpListener façade classes, or the feature-rich Socket class. (In fact, you can mix and match, because TcpClient exposes the underlying Socket object through the Client property.) The Socket class exposes more configuration options and allows direct access to the network layer (IP) and non-internet-based protocols such as Novell’s SPX/IPX.

(TCP and UDP communication is also possible via WinRT types: see “TCP in UWP”.)

As with other protocols, TCP differentiates a client and server: the client initiates a request, while the server waits for a request. Here’s the basic structure for a synchronous TCP client request:

using (TcpClient client = new TcpClient())
{
  client.Connect ("address", port);
  using (NetworkStream n = client.GetStream())
  {
    // Read and write to the network stream...
  }
}

TcpClient’s Connect method blocks until a connection is established (ConnectAsync is the asynchronous equivalent). The NetworkStream then provides a means of two-way communication, for both transmitting and receiving bytes of data from a server.

A simple TCP server looks like this:

TcpListener listener = new TcpListener (<ip address>, port);
listener.Start();

while (keepProcessingRequests)
  using (TcpClient c = listener.AcceptTcpClient())
  using (NetworkStream n = c.GetStream())
  {
    // Read and write to the network stream...
  }

listener.Stop();

TcpListener requires the local IP address on which to listen (a computer with two network cards, for instance, can have two addresses). You can use IPAddress.Any to instruct it to listen on all (or the only) local IP addresses. AcceptTcpClient blocks until a client request is received (again, there’s also an asynchronous version), at which point we call GetStream, just as on the client side.

When working at the transport layer, you need to decide on a protocol for who talks when and for how long—rather like with a walkie-talkie. If both parties talk or listen at the same time, communication breaks down!

Let’s invent a protocol in which the client speaks first, saying “Hello,” and then the server responds by saying “Hello right back!” Here’s the code:

using System;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Threading;

new Thread (Server).Start();       // Run server method concurrently.
Thread.Sleep (500);                // Give server time to start.
Client();

void Client()
{
  using (TcpClient client = new TcpClient ("localhost", 51111))
  using (NetworkStream n = client.GetStream())
  {
    BinaryWriter w = new BinaryWriter (n);
    w.Write ("Hello");
    w.Flush();
    Console.WriteLine (new BinaryReader (n).ReadString());
  }
}

void Server()     // Handles a single client request, then exits.
{
  TcpListener listener = new TcpListener (IPAddress.Any, 51111);
  listener.Start();
  using (TcpClient c = listener.AcceptTcpClient())
  using (NetworkStream n = c.GetStream())
  {
    string msg = new BinaryReader (n).ReadString();
    BinaryWriter w = new BinaryWriter (n);
    w.Write (msg + " right back!");
    w.Flush();                      // Must call Flush because we're not
  }                                 // disposing the writer.
  listener.Stop();
}

// OUTPUT: Hello right back!

In this example, we’re using the localhost loopback to run the client and server on the same machine. We’ve arbitrarily chosen a port in the unallocated range (above 49152) and used a BinaryWriter and BinaryReader to encode the text messages. We’ve avoided closing or disposing the readers and writers in order to keep the underlying NetworkStream open until our conversation completes.

BinaryReader and BinaryWriter might seem like odd choices for reading and writing strings. However, they have a major advantage over StreamReader and StreamWriter: they prefix strings with an integer indicating the length, so a BinaryReader always knows exactly how many bytes to read. If you call StreamReader.ReadToEnd you might block indefinitely—because a NetworkStream doesn’t have an end! As long as the connection is open, the network stream can never be sure that the client isn’t going to send more data.

Note

StreamReader is in fact completely out of bounds with NetworkStream, even if you plan only to call ReadLine. This is because StreamReader has a read-ahead buffer, which can result in it reading more bytes than are currently available, blocking indefinitely (or until the socket times out). Other streams such as FileStream don’t suffer this incompatibility with StreamReader because they have a definite end—at which point Read returns immediately with a value of 0.

Concurrency with TCP

TcpClient and TcpListener offer task-based asynchronous methods for scalable concurrency. Using these is simply a question of replacing blocking method calls with their *Async versions and awaiting the task that’s returned.

In the following example, we write an asynchronous TCP server that accepts requests of 5,000 bytes in length, reverses the bytes, and then sends them back to the client:

async void RunServerAsync ()
{
  var listener = new TcpListener (IPAddress.Any, 51111);
  listener.Start ();
  try
  {
    while (true)
      Accept (await listener.AcceptTcpClientAsync ());
  }
  finally { listener.Stop(); }
}

async Task Accept (TcpClient client)
{
  await Task.Yield ();
  try
  {
    using (client)
    using (NetworkStream n = client.GetStream ())
    {
      byte[] data = new byte [5000];
      
      int bytesRead = 0; int chunkSize = 1;
      while (bytesRead < data.Length && chunkSize > 0)
        bytesRead += chunkSize =
          await n.ReadAsync (data, bytesRead, data.Length - bytesRead);
      
      Array.Reverse (data);   // Reverse the byte sequence
      await n.WriteAsync (data, 0, data.Length);
    }
  }
  catch (Exception ex) { Console.WriteLine (ex.Message); }
}

Such a program is scalable in that it does not block a thread for the duration of a request. So, if 1,000 clients were to connect at once over a slow network connection (so that each request took several seconds from start to finish, for example), this program would not require 1,000 threads for that time (unlike with a synchronous solution). Instead, it leases threads only for the small periods of time required to execute code before and after the await expressions.

Receiving POP3 Mail with TCP

.NET provides no application-layer support for POP3, so you need to write at the TCP layer in order to receive mail from a POP3 server. Fortunately, this is a simple protocol; a POP3 conversation goes like this:

Client Mail server Notes
Client connects... +OK Hello there. Welcome message
USER joe +OK Password required.
PASS password +OK Logged in.
LIST +OK
1 1876
2 5412
3 845
.
Lists the ID and file size of each message on the server
RETR 1 +OK 1876 octets
Content of message #1...
.
Retrieves the message with the specified ID
DELE 1 +OK Deleted. Deletes a message from the server
QUIT +OK Bye-bye.

Each command and response is terminated by a new line (CR + LF) except for the multiline LIST and RETR commands, which are terminated by a single dot on a separate line. Because we can’t use StreamReader with NetworkStream, we can start by writing a helper method to read a line of text in a nonbuffered fashion:

string ReadLine (Stream s)
{
  List<byte> lineBuffer = new List<byte>();
  while (true)
  {
    int b = s.ReadByte();
    if (b == 10 || b < 0) break;
    if (b != 13) lineBuffer.Add ((byte)b);
  }
  return Encoding.UTF8.GetString (lineBuffer.ToArray());
}

We also need a helper method to send a command. Because we always expect to receive a response starting with +OK, we can read and validate the response at the same time:

void SendCommand (Stream stream, string line)
{
  byte[] data = Encoding.UTF8.GetBytes (line + "
");
  stream.Write (data, 0, data.Length);
  string response = ReadLine (stream);
  if (!response.StartsWith ("+OK"))
    throw new Exception ("POP Error: " + response);
}

With these methods written, the job of retrieving mail is easy. We establish a TCP connection on port 110 (the default POP3 port) and then start talking to the server. In this example, we write each mail message to a randomly named file with an .eml extension before deleting the message off the server:

using (TcpClient client = new TcpClient ("mail.isp.com", 110))
using (NetworkStream n = client.GetStream())
{
  ReadLine (n);                             // Read the welcome message.
  SendCommand (n, "USER username");
  SendCommand (n, "PASS password");
  SendCommand (n, "LIST");                  // Retrieve message IDs
  List<int> messageIDs = new List<int>();
  while (true)
  {
    string line = ReadLine (n);             // e.g.,  "1 1876"
    if (line == ".") break;
    messageIDs.Add (int.Parse (line.Split (' ')[0] ));   // Message ID
  }

  foreach (int id in messageIDs)         // Retrieve each message.
  {
    SendCommand (n, "RETR " + id);
    string randomFile = Guid.NewGuid().ToString() + ".eml";
    using (StreamWriter writer = File.CreateText (randomFile))
      while (true)
      {
        string line = ReadLine (n);      // Read next line of message.
        if (line == ".") break;          // Single dot = end of message.
        if (line == "..") line = ".";    // "Escape out" double dot.
        writer.WriteLine (line);         // Write to output file.
      }
    SendCommand (n, "DELE " + id);       // Delete message off server.
  }
  SendCommand (n, "QUIT");
}
Note

You can find open source POP3 libraries on NuGet that provide support for protocol aspects such as authentication TLS/SSL connections, MIME parsing, and more.

TCP in UWP

In UWP applications, TCP functionality is exposed through WinRT types in the Windows.Networking.Sockets namespace. As with the .NET implementation, there are two primary classes to handle server and client roles, StreamSocketListener and StreamSocket.

Note

Your application manifest must declare the capability Internet (Client) if the host is on the internet or Private Networks (Client & Server) if the host is private (including localhost).

The following method starts a server on port 51111, and waits for a client to connect. It then reads a single message comprising a length-prefixed string:

async void Server()
{
  var listener = new StreamSocketListener();
  listener.ConnectionReceived += async (sender, args) =>
  {
    using (StreamSocket socket = args.Socket)
    {
      var reader = new DataReader (socket.InputStream);
      await reader.LoadAsync (4);
      uint length = reader.ReadUInt32();
      await reader.LoadAsync (length);
      Debug.WriteLine (reader.ReadString (length));
    }
    listener.Dispose();   // Close listener after one message.
  };
  await listener.BindServiceNameAsync ("51111");
}

In this example, we used a WinRT type called DataReader (in Windows.Networking) to read from the input stream, rather than converting to a .NET Stream object and using a BinaryReader. DataReader is rather like BinaryReader except that it supports asynchrony. The LoadAsync method asynchronously reads a specified number of bytes into an internal buffer, which then allows you to call methods such as ReadUInt32 or ReadString. The idea is that if you wanted to, say, read 1,000 integers in a row, you’d first call LoadAsync with a value of 4000 and then ReadInt32 1,000 times in a loop. This prevents the overhead of calling asynchronous operations in a loop (because each asynchronous operation incurs a small overhead).

Note

DataReader/DataWriter have a ByteOrder property to control whether numbers are encoding in big- or little-endian format. Big-endian is the default.

The StreamSocket object that we obtained from awaiting AcceptAsync has separate input and output streams. So, to write a message back, we’d use the socket’s OutputStream. We can illustrate the use of OutputStream and DataWriter with the corresponding client code:

async void Client()
{
  using (var socket = new StreamSocket())
  {
    await socket.ConnectAsync (new HostName ("localhost"), "51111",
                              SocketProtectionLevel.PlainSocket);
    var writer = new DataWriter (socket.OutputStream);
    string message = "Hello!";
    uint length = (uint) Encoding.UTF8.GetByteCount (message);
    writer.WriteUInt32 (length);
    writer.WriteString (message);
    await writer.StoreAsync();
  }
}

We start by directly instantiating a StreamSocket and then call ConnectAsync with the host name and port. (You can pass either a DNS name or an IP address string into HostName’s constructor.) By specifying SocketProtectionLevel.Ssl, you can request SSL encryption (if configured on the server).

Again, we used a WinRT DataWriter rather than a .NET BinaryWriter and wrote the length of the string (measured in bytes rather than characters), followed by the string itself, which is UTF-8 encoded. Finally, we called StoreAsync, which writes the buffer to the backing stream, and closed the socket.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.219.22.169