Background
Ok, so there are already alternatives for web servers in C#. Some of them are very feature rich.
- http://cassinidev.codeplex.com/
- http://webserver.codeplex.com/
- http://www.codeproject.com/Articles/137979/Simple-HTTP-Server-in-C
- http://www.codeproject.com/Articles/1505/Create-your-own-Web-Server-using-C
- http://kayakhttp.com/
- http://mikehadlow.blogspot.no/2006/07/playing-with-httpsys.html
- http://ultidev.com/products/cassini/
- http://lmgtfy.com/?q=c%23+webserver
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).
1 2 3 4 5 6 |
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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 |
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\n" + "Server: {1}\r\n" + "Content-Length: {2}\r\n" + "Content-Type: {3}\r\n" + "Keep-Alive: Close\r\n" + "\r\n", 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
using System; namespace Tedd.Demo.HttpServer.Server { class Program { private static Listener listener; static void Main(string[] args) { listener = new Listener(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(); } } } |
It doesn’t working.
_socket.SendAsync require SocketAsyncEventArgs and return bool
await require void methode
and so on..
🙁
Best Regards
Hi
Thanks for your feedback.
Not really sure how I managed to copy-paste that wrong. It was supposed to be _networkStream, not _socket – so probably a last minute edit. Also fixed a race condition with async socket disposing related to that last error.
Great code, so far.
headerBytes is missing. The GetContentType() method is too, but I can figure out how it works. I am stuck on headerBytes, though.
The error was “await headerBytes” should be “var headerBytes”.
I put the source into a new project and fixed it up, so latest version is downloadable from top of page. 🙂
Hi There,
This is a great simple example of TPL being used in a real world scenario.
At the minute i am trying to put together an asynchronous tcp server but to be honest there isn’t much info out there on building a reliable server using the new socketasynceventargs (At least none that are particularly easily read).
This though by the looks of it implements it, though in a much more readable format much like the way a thread based implementation would except without a thread being created every time a client connects and allowing TPL to worry about the getting the tasks done. Would that be a fair assesment?
What i was wondering is would this design allow for a large number of concurrently connected clients? I know you mentioned at the top of the page it could hold 10000 connections with 10 threads but have you tried this with concurrently sending clients? or would this literally handle requests in a similar fashion as a traditional asynchronous server.
Also would this design make use of I/O completion ports
Hi. I have not tested this with high concurrency, but I would like to “when I have time”®. 🙂
Hi,
sorry for replying on a thead that’s almost two years old, but I really like this sample. Very clean and easy-to-follow way of dealing with something that could get pretty complicated if done “the old way”.
One thing I’ve been wondering with these parallel setups though: How would you limit/control the number of concurrent connections the server allows?
The only thing I can think of is keeping a list (preferably a ConcurrentCollection) of the processing task objects and using Thread.Sleep to wait until its count is below the limit before spawning a new task. Isn’t there a more elegant way of doing it?
Thanks!