← Back to articles

Async/await HTTP server in C#

There are plenty of C# web servers already, but I wanted to play with TPL and async/await — so here is a minimal HTTP server built around them. Downloadable code included.

Tedd

Since there has been a bit of interest for the code below I mocked together another version:Â Kernel.HttpServer.zip

Background

Ok, so there are already alternatives for web servers in C#. Some of them are very feature rich.

But I wanted to play around with TPL and async/await a bit so I put together a quick hack of a minimal HTTP-server.

The main focus here has been on the use of async await keyword in .Net 4.5 that allows for thread conservation while keeping the code clean. .Net will automatically manage thread pool. In theory this solution could hold 10 000 connections and still use only 10 threads.

Traditional a server like this would be implemented in one of two ways:

  • Synchronous: one dedicated thread per connected client.

  • Asynchronous: Required calls to Begin* and End* to form a sort of loop. Methods are re-executed for every receive or send operation and you have to split your code into several methods.

Task Parallel Library + await

With the introduction TPL the traditional thread model is encapsulated in a managed Task with a wide variety of functionality. This in itself is handy, works great with anonymous methods and is handled by .Net managed thread pool. In .Net 4.5 a new keyword “await” was introduced which magically allows us to pause methods while waiting for a synchronous operation. During this pause the thread returns to the thread pool to do other work, and upon resume a thread is assigned the task of continuing execution of the method. The magic is of course just an illusion and behind the scenes the compiler adds some code that resembles the yield implementation. It converts the method to a state machine allowing a thread to enter and exit a method at certain positions using goto-statements and labels.

An example of how await allows us to release the thread while waiting for write operation to complete, then flush the stream. This would traditionally either block a thread (synchronous) or require you to split code into two methods (asynchronous).

private async void SendText(string text)
{
 var data = Encoding.UTF8.GetBytes(text);
 await _networkStream.WriteAsync(data, 0, data.Length);
 await _networkStream.FlushAsync();
} 

The code

As always you should consider security if you use this code. I have implemented a regex-check of the file, which is the only data we pass to the OS in any way. New version don’t contain any regex, its a gaping security hole! But a regex-check like that could probably easily be vulnerable to a prematurely null-terminated string for instance. (Don’t trust code.)

Listener.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;

namespace Tedd.Demo.HttpServer.Server
{
 class Listener
 {
 public readonly int Port;
 private readonly TcpListener _tcpListener;
 private readonly Task _listenTask;

 public Listener(int port)
 {
 Port = port;

 // Start listening
 _tcpListener = new TcpListener(IPAddress.Any, Port);
 _tcpListener.Start();

 // Start a background thread to listen for incoming
 _listenTask = Task.Factory.StartNew(() => ListenLoop());

 }

 private async void ListenLoop()
 {
 for (; ; )
 {
 // Wait for connection
 var socket = await _tcpListener.AcceptSocketAsync();
 if (socket == null)
 break;

 // Got new connection, create a client handler for it
 var client = new Client(socket);
 // Create a task to handle new connection
 Task.Factory.StartNew(client.Do);
 }
 }
 }
} 

Client.cs

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Sockets;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Microsoft.Win32;

namespace Tedd.Demo.HttpServer.Server
{
 class Client
 {
 private readonly Socket _socket;
 private readonly NetworkStream _networkStream;
 private readonly MemoryStream _memoryStream = new MemoryStream();
 private readonly StreamReader _streamReader;
 private readonly string _serverName = "Tedd.Demo.HttpServer";

 public Client(Socket socket)
 {
 _socket = socket;
 _networkStream = new NetworkStream(socket, true);
 _streamReader = new StreamReader(_memoryStream);
 }

 public async void Do()
 {
 // We are executed on a separate thread from listener, but will release this back to the threadpool as often as we can.
 byte[] buffer = new byte[4096];
 for (; ; )
 {
 // Read a chunk of data
 int bytesRead = await _networkStream.ReadAsync(buffer, 0, buffer.Length);

 // If Read returns with no data then the connection is closed.
 if (bytesRead == 0)
 return;

 // Write to buffer and process
 _memoryStream.Seek(0, SeekOrigin.End);
 _memoryStream.Write(buffer, 0, bytesRead);
 bool done = ProcessHeader();
 if (done)
 break;
 }

 }

 private bool ProcessHeader()
 {
 // Our task is to find when full HTTP header has been received, then send reply.
 for (; ; )
 {
 _memoryStream.Seek(0, SeekOrigin.Begin);
 var line = _streamReader.ReadLine();
 if (line == null)
 break;

 if (line.ToUpperInvariant().StartsWith("GET "))
 {
 // We got a request: GET /file HTTP/1.1
 var file = line.Split(' ')[1].TrimStart('/');
 // Default document is index.html
 if (string.IsNullOrWhiteSpace(file))
 file = "index.html";
 // Send header+file
 SendFile(file);
 return true;
 }

 }
 return false;
 }

 private async void SendFile(string file)
 {
 // Get info and assemble header
 byte[] data;
 string responseCode = "";
 string contentType = "";
 try
 {
 if (File.Exists(file))
 {
 // Read file
 data = File.ReadAllBytes(file);
 contentType = GetContentType(Path.GetExtension(file).TrimStart(".".ToCharArray()));
 responseCode = "200 OK";
 }
 else
 {
 data = System.Text.Encoding.ASCII.GetBytes("<html><body><h1>404 File Not Found</h1></body></html>");
 contentType = GetContentType("html");
 responseCode = "404 Not found";
 }
 }
 catch (Exception exception)
 {
 // In case of error dump exception to client.
 data = System.Text.Encoding.ASCII.GetBytes("<html><body><h1>500 Internal server error</h1><pre>" + exception.ToString() + "</pre></body></html>");
 responseCode = "500 Internal server error";
 }

 string header = string.Format("HTTP/1.1 {0}\\\r\\
"
 + "Server: {1}\\\r\\
"
 + "Content-Length: {2}\\\r\\
"
 + "Content-Type: {3}\\\r\\
"
 + "Keep-Alive: Close\\\r\\
"
 + "\\\r\\
",
 responseCode, _serverName, data.Length, contentType);
 // Send header & data
 var headerBytes = System.Text.Encoding.ASCII.GetBytes(header)
 await _networkStream.WriteAsync(headerBytes, 0, headerBytes.Length);
 await _networkStream.WriteAsync(data, 0, data.Length);
 await _networkStream.FlushAsync();
 // Close connection (we don't support keep-alive)
 _networkStream.Dispose();
    }

 /// <summary>
 /// Get mime type from a file extension
 /// </summary>
 /// <param name="extension">File extension without starting dot (html, not .html)</param>
 /// <returns>Mime type or default mime type "application/octet-stream" if not found.</returns>
 private string GetContentType(string extension)
 {
 // We are accessing the registry with data received from third party, so we need to have a strict security test. We only allow letters and numbers.
 if (Regex.IsMatch(extension, "^[a-z0-9]+$", RegexOptions.IgnoreCase | RegexOptions.Compiled))
 return (Registry.GetValue(@"HKEY_CLASSES_ROOT\\\\." + extension, "Content Type", null) as string) ?? "application/octet-stream";
 return "application/octet-stream";

 }
 }
} 

Sample Program.cs

using System;

namespace Tedd.Demo.HttpServer.Server
{
 class Program
 {
 private static Listener listener;
 static void Main(string[] args)
 {
 listener = new Listener(8080);
 Console.WriteLine("Listening on port 8080 (http://localhost:8080/).");
 Console.WriteLine("Remember to put the files you want to serve in the output folder of the project.");
 Console.WriteLine("Press any key to exit.");
 Console.ReadKey();
 }
 }
} 

Written by Tedd · Oslo, Norway

Updated 5 Jan 2017

Follow the journal via RSS