Chapter 16 - Networking

IP Addresses and URIs

Simple Address Parsing

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

Console.WriteLine();
Console.WriteLine("Address with port:");
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

Uri Usage

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
HttpClient

Simple request

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

Parallel downloads

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

(await task1).Length.Dump ("First page length");
(await task2).Length.Dump ("Second page length");

Response messages

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

EscapeDataString

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

Util.CreateSynchronizationContext();
var html = await new HttpClient().GetStringAsync (requestURI);
File.WriteAllText ("temp.html", html);
Process.Start (new ProcessStartInfo ("temp.html") { UseShellExecute = true });

Uploading data

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());

Using HttpMessageHandler for mocking

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);

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);
  }
}

static class Assert
{
  public static void AreEqual (object o1, object o2)
  {
    if (!Equals (o1, o2)) throw new Exception ("Objects are not equal");    
  }    
}

Chaining handlers with DelegatingHandler

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

var logger = new LoggingHandler (mocker);

var client = new HttpClient (logger);
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);

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);
  }
}

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;
  }
}

static class Assert
{
  public static void AreEqual (object o1, object o2)
  {
    if (!Equals (o1, o2)) throw new Exception ("Objects are not equal");    
  }    
}

Uploading form data - with HttpClient

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

var cc = new CookieContainer();
var handler = new HttpClientHandler { CookieContainer = cc };
var client = new HttpClient (handler);
await client.GetStringAsync ("http://www.google.com");
cc.GetAllCookies().Dump();

EXTRA - HttpClient With Progress

// Based on: https://stackoverflow.com/q/21169573/141172
//           https://stackoverflow.com/q/230128/141172

HttpClient client = new HttpClient();

var linqPadProgressBar = new Util.ProgressBar ("Download progress").Dump();

var progress = new Progress<double>();

progress.ProgressChanged += (sender, value) =>
  linqPadProgressBar.Percent = (int) value;

var cancellationToken = new CancellationTokenSource();

using var destination = File.OpenWrite ("LINQPad6Setup.exe");
await DownloadFileAsync ("https://www.linqpad.net/GetFile.aspx?LINQPad6Setup.exe", destination, progress, default);

async Task CopyStreamWithProgressAsync (Stream input, Stream output, long total, IProgress<double> progress, CancellationToken token)
{
  const int IO_BUFFER_SIZE = 8 * 1024; // Optimal size depends on your scenario

  // Expected size of input stream may be known from an HTTP header when reading from HTTP. Other streams may have their
  // own protocol for pre-reporting expected size.

  var canReportProgress = total != -1 && progress != null;
  var totalRead = 0L;
  byte[] buffer = new byte [IO_BUFFER_SIZE];
  int read;
  
  while ((read = await input.ReadAsync (buffer, 0, buffer.Length)) > 0)
  {
    token.ThrowIfCancellationRequested();
    await output.WriteAsync (buffer, 0, read);
    totalRead += read;
    if (canReportProgress)
      progress.Report ((totalRead * 1d) / (total * 1d) * 100);
  }
}
 
 async Task DownloadFileAsync (string url, Stream destination, IProgress<double> progress, CancellationToken token)
{
  var response = await client.GetAsync (url, HttpCompletionOption.ResponseHeadersRead, token);

  if (!response.IsSuccessStatusCode)
    throw new Exception (string.Format ("The request returned with HTTP status code {0}", response.StatusCode));

  var total = response.Content.Headers.ContentLength.HasValue ? response.Content.Headers.ContentLength.Value : -1L;

  using var source = await response.Content.ReadAsStreamAsync();
  
  await CopyStreamWithProgressAsync(source, destination, total, progress, token);
}

One Response HTTP Server

using var server = new SimpleHttpServer();

Console.WriteLine (await new HttpClient().GetStringAsync
  ("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();
}

Simple HTTP Server

// Listen on port 51111, serving files in d:\webroot:
var server = new WebServer ("http://localhost:51111/", Path.Combine (GetTempDirectory(), "webroot"));
try
{
  server.Start();
  // If running in LINQPad, stop the query manually:
  Console.WriteLine ("Server running... press Enter to stop");
  Console.ReadLine();
}
finally { server.Stop(); }

string GetTempDirectory() =>
  RuntimeInformation.IsOSPlatform (OSPlatform.Windows) ? @"C:\Temp" : "/tmp";

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); }
  }
}

DNS

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

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

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

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

Sending mail with SMTP

var client = new SmtpClient ("smtp.myisp.com", 587)
{
  Credentials = new NetworkCredential ("me@myisp.com", "MySecurePass"),
  EnableSsl = true
};

MailMessage mm = new MailMessage();

mm.Sender = new MailAddress ("kay@domain.com", "Kay");
mm.From = new MailAddress ("kay@domain.com", "Kay");
mm.To.Add (new MailAddress ("bob@domain.com", "Bob"));
mm.CC.Add (new MailAddress ("dan@domain.com", "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);

client.Send ("me@myisp.com", "someone@somewhere.com", "Subject", "Body");
Console.WriteLine ("Sent");

TCP - simple demo

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();
}

TCP - Concurrency

RunServerAsync();

using (TcpClient client = new TcpClient ("localhost", 51111))
using (NetworkStream n = client.GetStream())
{
  BinaryWriter w = new BinaryWriter (n);
  w.Write (Enumerable.Range (0, 5000).Select (x => (byte) x).ToArray());
  w.Flush();
  Console.WriteLine (new BinaryReader (n).ReadBytes (5000));
}

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); }
}

Receiving POP3 mail

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");
}

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());
}

void SendCommand (Stream stream, string line)
{
  byte[] data = Encoding.UTF8.GetBytes (line + "\r\n");
  stream.Write (data, 0, data.Length);
  string response = ReadLine (stream);
  if (!response.StartsWith ("+OK"))
    throw new Exception ("POP Error: " + response);
}
C# 12 in a Nutshell
Buy from amazon.com Buy print or Kindle edition
Buy from ebooks.com Buy PDF edition
Buy from O'Reilly Read via O'Reilly subscription