Like the title suggests, we are going to build a simple HTTP Server.
https://github.com/Corey255A1/BareBonesHttpServer
Suggested Reading
https://developer.mozilla.org/en-US/docs/Web/HTTP/Overview
At its core, an HTTP Server is just a TCP Server with a specific packet protocol. Typically webpages are hosted on port 80 for HTTP and 443 for HTTPS. But it can be any port really. Your browser just defaults and makes the assumption of it being on one of those. In the HttpServer class, StartListening loops and waits for a client to connect to the TCP Server.
var client = await this._server.AcceptTcpClientAsync(); if ((HttpNewClientConnected!=null && HttpNewClientConnected.Invoke(client)) || HttpNewClientConnected==null) { var httpc = new HttpClientHandler(client); httpc.HttpRequestReceived += HttpRequestReceived; httpc.WebSocketDataReceived += HttpWebSocketDataReceived; _clients[client.Client.RemoteEndPoint]=httpc; }
The StartListening method is an Asynchronous method. This means that if you call it with out using the await, it spins off on to a different task/thread automatically. Within that we have a loop that is Asynchronous listening for clients. The HttpNewClientConnected has a bool return so that in the future I can filter out client connections.
I get the TcpClient and create a wrapper HttpClientHandler to handle the HTTP processing for that client.
The constructor of the HttpClientHandler saves off the NetworkStream and the client information and then begins reading. Which is another Async method that loops and reads from the stream.
int bytesread = await _stream.ReadAsync(_buffer, 0, BUFFERSIZE); while (bytesread > 0) {<span style="color: #008800; font-weight: bold">if</span> (!<span style="color: #008800; font-weight: bold">this</span>.WebSocketUpgrade) { <span style="color: #333399; font-weight: bold">string</span> msg = System.Text.Encoding.UTF8.GetString(_buffer); HttpRequest h = <span style="color: #008800; font-weight: bold">new</span> HttpRequest(msg); HttpRequestReceived?.Invoke(<span style="color: #008800; font-weight: bold">this</span>, h); } <span style="color: #008800; font-weight: bold">else</span> { <span style="color: #888888">//Not handling Multiple Frames worth of data...</span> WebSocketFrame frame = <span style="color: #008800; font-weight: bold">new</span> WebSocketFrame(_buffer); WebSocketDataReceived?.Invoke(<span style="color: #008800; font-weight: bold">this</span>, frame); <span style="color: #888888">//Console.WriteLine(Encoding.UTF8.GetString(frame.Payload));</span> } bytesread = <span style="color: #008800; font-weight: bold">await</span> _stream.ReadAsync(_buffer, <span style="color: #6600EE; font-weight: bold">0</span>, BUFFERSIZE);
}
You can see how there is some code in there for handling WebSocket connections. We will get there.
First though we will focus on the HttpRequest. I’m assuming HTTP1.1 plain text UTF8 packets.
[Example Request, There is a lot more data that comes through typically]
GET / HTTP/1.1
Host: WunderVision
Accept-Language: en-US
[Here is one that I captured from a Firefox request]
GET / HTTP/1.1
Host: localhost
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:65.0) Gecko/20100101 Firefox/65.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,/;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Connection: keep-alive
Upgrade-Insecure-Requests: 1
I made the most basic HttpContainer which is basically a map that handles the Key, Value pairs from the Http Header.
A co-worker had brought to my attention the other day that you can override the [] operator of a C# Class, so this makes it a convenient way to access the elements of the HTTP header.
public string this[string obj] { get { return _properties.ContainsKey(obj) ? _properties[obj] : ""; } set { if (_properties.ContainsKey(obj)) { _properties[obj] = value; } else { _properties.Add(obj, value); } } }
I made two derived classes from the HttpContainer, HttpRequest and HttpResponse, The HttpRequest decodes the first line of the HttpHeader (example Get / HTTP1.1) and assigns them to their appropriate Property names. This way all pieces of the HTTP Request can be accessed the same way.
The HttpResponse is a little bit different. It has the capability to add on Http Content. (i.e html, .png, .jpg, etc). They have the GetBytes method which recombines the key/values in to a string and then gets the packet as an array of UTF8 Bytes.
The HttpClientHandler decodes the packet and passes it back up to the to the handling application for processing. I want to add one more layer to the code, but for now I have a test application (SimpleWebHost) that is handling the various HTTP Requests.
private static void ClientRequest(HttpClientHandler client, HttpRequest req) { HttpResponse resp = null; //Console.WriteLine(req.ToString()); if (req["Request"] == "GET") { string uri = req["URI"]; if (uri == "/") { resp = new HttpResponse("HTTP/1.1", "200", "OK"); resp.AddProperty("Date", DateTime.Now.ToShortDateString()); resp.AddProperty("Server", "WunderVision"); resp.AddProperty("Content-Type", "text/html;charset=UTF-8"); resp.SetData(Site); }
Here is the most basic response back to the client site. And you can see how the [] override makes it easy to get the data. First we see if the Request packet type is a GET. Then we get the URI that is requested. If it is the root / we create our default response. I don’t think I’m sending the Date format back right, but it doesn’t seem to affect Firefox, Chrome or Edge haha. You can see how I’m filling out just the basic response fields. There is a lot more information that can be sent back, but I’m only sticking with the minimum. The SetData(Site) is just setting the content of the header to the string version of an HTML file I read above in the program.
[Example of the response]
HTTP/1.1 200 OK
Date: Sat, 09 March 2019 13:09:02 GMT
Server: WunderVision
Content-Type: text/html;charset=UTF-8
Content-Length: 1327
case ".ico": return "image/x-icon"; case ".jpg": return "image/jpeg"; case ".png": return "image/png"; case ".gif": return "image/gif"; case ".css": return "text/css"; case ".js": return "text/javascript"; case ".json": return "application/json"; case ".html": case ".htm": default: return "text/html";
This obviously doesn’t cover everything, and isn’t very extensible. But the name of the game was to be simple!
There is the piece of code that looks for all other files that are requested with in our root serving directory and builds the appropriate response back depending on the file type.
Uri requestedfile = new Uri(Root + uri); //Console.WriteLine(requestedfile.LocalPath); if (File.Exists(requestedfile.LocalPath)) { string mime = HttpTools.GetFileMimeType(uri); //Console.WriteLine(mime); byte[] data; if (HttpTools.IsFileBinary(uri)) { data = File.ReadAllBytes(requestedfile.LocalPath); } else { data = System.Text.Encoding.UTF8.GetBytes(File.ReadAllText(requestedfile.LocalPath)); } resp = new HttpResponse("HTTP/1.1", "200", "OK"); resp.AddProperty("Date", DateTime.Now.ToShortDateString()); resp.AddProperty("Server", "WunderVision"); resp.AddProperty("Content-Type", mime); resp.SetData(data); }
And with that, we can serve up some basic webpages all with images, CSS and Javascript!
Here I’m we can see it processing the requests for all of the pieces to the DragWindow.html page (can be found here: https://corey255a1.github.io/DragWindow/DragWindow.html)
However, I wasn’t done, I also need basic WebSocket support …