Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / web

Build Your Own Web Server

4.88/5 (92 votes)
9 Oct 2012CPOL4 min read 405.6K   20.1K  
Let's build a simple web server and make it accessible from the internet

Contents

Introduction

We will learn to write a simple web server which can send responses to the most well-known HTTP methods (GET and POST), in C#. Then we will make this server accessible from the internet. This time, we will really say "Hello world!"

Simple Web Server

Background

HTTP Protocol

HTTP is a communication protocol between servers and clients. It uses TCP/IP protocol to send/receive requests/responses.

There are a few HTTP methods and we will implement two of them; GET and POST.

GET

What happens when we write an address into the address bar of our web browser and hit enter? (We mostly don't specify a port number although it is required for TCP/IP, because it has a default value for http and it is 80. We don't have to specify it if it is 80.)

GET / HTTP/1.1\r\n
Host: atasoyweb.net\r\n
User-Agent: Mozilla/5.0 (Windows NT 6.1; rv:14.0) Gecko/20100101 Firefox/14.0.1\r\n
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\n
Accept-Language: tr-tr,tr;q=0.8,en-us;q=0.5,en;q=0.3\r\n
Accept-Encoding: gzip, deflate\r\n
Connection: keep-alive\r\n\r\n

This is the GET request which is sent by our browser to the server using TCP/IP. This means the browser requests the server to send the contents of "/" from the root folder of "atasoyweb.net".

We (or browsers) can add more headers. But the most simplified version of this request is below:

GET / HTTP/1.1\r\n
Host: atasoyweb.net\r\n\r\n 

POST

POST requests are similar to GET requests. In a GET request, variables are appended to the URLs using ? character. But in a POST request, variables are appended to the end of the request after 2 line break characters and total length (content-length) is specified.

POST /index.html HTTP/1.1\r\n
Host: atasoyweb.net\r\n
User-Agent: Mozilla/5.0 (Windows NT 6.1; rv:15.0) Gecko/20100101 Firefox/15.0.1\r\n
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\n
Accept-Language: tr-tr,tr;q=0.8,en-us;q=0.5,en;q=0.3\r\n
Accept-Encoding: gzip, deflate\r\n
Connection: keep-alive\r\n
Referer: http://atasoyweb.net/\r\n
Content-Type: application/x-www-form-urlencoded\r\n
Content-Length: 35\r\n\r\n
variable1=value1&variable2=value2

Simplified version of this request:

POST /index.html HTTP/1.1\r\n
Host: atasoyweb.net\r\n
Content-Length: 35\r\n\r\n
variable1=value1&variable2=value2 

Responses

When a request is received by the server, it is parsed and a response with a status code is returned:

HTTP/1.1 200 OK\r\n
Server: Apache/1.3.3.7 (Unix) (Red-Hat/Linux)\r\n
Content-Length: {content_length}\r\n
Connection: close\r\n
Content-Type: text/html; charset=UTF-8\r\n\r\n
the content of which length is equal to {content_length}

This is the response header. "200 OK" means everything is OK, requested content will be returned. There are many status codes. We will use just 200, 501 and 404:

  • "501 Not Implemented": Method is not implemented. We will implement only GET and POST. So, we will send response with this code for all other methods.
  • "404 Not Found": Requested content is not found.

Content Types

Servers must specify the type of the content in their response. There are many content types and these are also called "MIME (Multipurpose Internet Mail Extensions) types" (because they are also used to identify non-ASCII parts of emails). Here are the content types that we will use in our implementation: (you can modify the code and add more):

  • text/html
  • text/xml
  • text/plain
  • text/css
  • image/png
  • image/gif
  • image/jpg
  • image/jpeg
  • application/zip

If servers specify the wrong content types, contents will be misinterpreted. For example, if a server sends plain text using the "image/png" type, the client tries to show the text as an image.

Multithreading

If we want our server to be available even if a response is being sent to another client at that time, we must create new threads for every request. Thus, every thread handles a single request and exits after it completes its mission. (Multithreading also speeds up page loadings, because if we request a page that uses CSS and includes images, different GET requests are sent for every image and CSS file.)

Implementation of a Simple Web Server

Now we are ready to implement a simple web server. First of all, let's define variables that we will use:

C#
public bool running = false; // Is it running?

private int timeout = 8; // Time limit for data transfers.
private Encoding charEncoder = Encoding.UTF8; // To encode string
private Socket serverSocket; // Our server socket
private string contentPath; // Root path of our contents

// Content types that are supported by our server
// You can add more...
// To see other types: http://www.webmaster-toolkit.com/mime-types.shtml
private Dictionary<string, string> extensions = new Dictionary<string, string>()
{ 
    //{ "extension", "content type" }
    { "htm", "text/html" },
    { "html", "text/html" },
    { "xml", "text/xml" },
    { "txt", "text/plain" },
    { "css", "text/css" },
    { "png", "image/png" },
    { "gif", "image/gif" },
    { "jpg", "image/jpg" },
    { "jpeg", "image/jpeg" },
    { "zip", "application/zip"}
};

Method to start our server:

C#
public bool start(IPAddress ipAddress, int port, int maxNOfCon, string contentPath)
{
    if (running) return false; // If it is already running, exit.

    try
    {
        // A tcp/ip socket (ipv4)
        serverSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream,
                       ProtocolType.Tcp);
        serverSocket.Bind(new IPEndPoint(ipAddress, port));
        serverSocket.Listen(maxNOfCon);
        serverSocket.ReceiveTimeout = timeout;
        serverSocket.SendTimeout = timeout;
        running = true;
        this.contentPath = contentPath;
    }
    catch { return false; }

    // Our thread that will listen connection requests
    // and create new threads to handle them.
    Thread requestListenerT = new Thread(() =>
    {
        while (running)
        {
            Socket clientSocket;
            try
            {
                clientSocket = serverSocket.Accept();
                // Create new thread to handle the request and continue to listen the socket.
                Thread requestHandler = new Thread(() =>
                {
                    clientSocket.ReceiveTimeout = timeout;
                    clientSocket.SendTimeout = timeout;
                    try { handleTheRequest(clientSocket); }
                    catch
                    {
                        try { clientSocket.Close(); } catch { }
                    }
                });
                requestHandler.Start();
            }
            catch{}
        }
    });
    requestListenerT.Start();

    return true;
}

Method to stop the server:

C#
public void stop()
{
    if (running)
    {
        running = false;
        try { serverSocket.Close(); }
        catch { }
        serverSocket = null;
    }
}

The most important part of the code:

C#
private void handleTheRequest(Socket clientSocket)
{
    byte[] buffer = new byte[10240]; // 10 kb, just in case
    int receivedBCount = clientSocket.Receive(buffer); // Receive the request
    string strReceived = charEncoder.GetString(buffer, 0, receivedBCount);

    // Parse method of the request
    string httpMethod = strReceived.Substring(0, strReceived.IndexOf(" "));

    int start = strReceived.IndexOf(httpMethod) + httpMethod.Length + 1;
    int length = strReceived.LastIndexOf("HTTP") - start - 1;
    string requestedUrl = strReceived.Substring(start, length);

    string requestedFile;
    if (httpMethod.Equals("GET") || httpMethod.Equals("POST"))
        requestedFile = requestedUrl.Split('?')[0];
    else // You can implement other methods...
    {
        notImplemented(clientSocket);
        return;
    }

    requestedFile = requestedFile.Replace("/", @"\").Replace("\\..", "");
    start = requestedFile.LastIndexOf('.') + 1;
    if (start > 0)
    {
        length = requestedFile.Length - start;
        string extension = requestedFile.Substring(start, length);
        if (extensions.ContainsKey(extension)) // Do we support this extension?
            if (File.Exists(contentPath + requestedFile)) //If yes check existence of the file
                // Everything is OK, send requested file with correct content type:
                sendOkResponse(clientSocket,
                  File.ReadAllBytes(contentPath + requestedFile), extensions[extension]);
            else
                notFound(clientSocket); // We don't support this extension.
                                        // We are assuming that it doesn't exist.
    }
    else
    {
        // If file is not specified try to send index.htm or index.html
        // You can add more (default.htm, default.html)
        if (requestedFile.Substring(length - 1, 1) != @"\")
            requestedFile += @"\";
        if (File.Exists(contentPath + requestedFile + "index.htm"))
            sendOkResponse(clientSocket,
              File.ReadAllBytes(contentPath + requestedFile + "\\index.htm"), "text/html");
        else if (File.Exists(contentPath + requestedFile + "index.html"))
            sendOkResponse(clientSocket,
              File.ReadAllBytes(contentPath + requestedFile + "\\index.html"), "text/html");
        else
            notFound(clientSocket);
    }
}

Responses for different status codes:

C#
private void notImplemented(Socket clientSocket)
{
   
    sendResponse(clientSocket, "<html><head><meta 
        http-equiv=\"Content-Type\" content=\"text/html; 
        charset=utf-8\">
        </head><body><h2>Atasoy Simple Web 
        Server</h2><div>501 - Method Not 
        Implemented</div></body></html>", 
        "501 Not Implemented", "text/html");
}

private void notFound(Socket clientSocket)
{
  
    sendResponse(clientSocket, "<html><head><meta 
        http-equiv=\"Content-Type\" content=\"text/html; 
        charset=utf-8\"></head><body><h2>Atasoy Simple Web 
        Server</h2><div>404 - Not 
        Found</div></body></html>", 
        "404 Not Found", "text/html");
}

private void sendOkResponse(Socket clientSocket, byte[] bContent, string contentType)
{
    sendResponse(clientSocket, bContent, "200 OK", contentType);
}

The method that will send responses to clients:

C#
// For strings
private void sendResponse(Socket clientSocket, string strContent, string responseCode,
                          string contentType)
{
    byte[] bContent = charEncoder.GetBytes(strContent);
    sendResponse(clientSocket, bContent, responseCode, contentType);
}

// For byte arrays
private void sendResponse(Socket clientSocket, byte[] bContent, string responseCode,
                          string contentType)
{
    try
    {
        byte[] bHeader = charEncoder.GetBytes(
                            "HTTP/1.1 " + responseCode + "\r\n"
                          + "Server: Atasoy Simple Web Server\r\n"
                          + "Content-Length: " + bContent.Length.ToString() + "\r\n"
                          + "Connection: close\r\n"
                          + "Content-Type: " + contentType + "\r\n\r\n");
        clientSocket.Send(bHeader);
        clientSocket.Send(bContent);
        clientSocket.Close();
    }
    catch { }
}

Usage

C#
// to create new one:
Server server = new Server();
// to start it
server.start(ipAddress, port, maxconnections, contentpath);
// to stop it
server.stop();

Let's Say "Hello" to All The World!

Our simple web server is ready. Now we will make it accessible from the internet. To achieve this, we must redirect requests that come to our modem to our computer. It is simple if our modem supports UPnP.

  1. Download this UPnP Port Forwarder and run it.
  2. Click "Search For Devices" button. If your modem supports UPnP, it will be added to the combobox.
  3. Click "Update List" button to list forwarded ports.
  4. Then click "Add New" button and fill the form.
  5. If you check "IP" checkbox and type an IP, only requests from this IP will be redirected. So, do not fill it.
  6. Internal port must be equal to our server's port.
  7. "Port" and "Internal port" don't have to be equal.

Adding new port forwarding entry

From now on, all requests come to "externalip:port" will be redirected from the modem to our computer. To test the server if it is accessible from the internet, you can use www.web-sniffer.net. Just write your external IP and port as "http://externalip:port" and hit "Submit" button...

Test result

History

  • 13th September, 2012: The POST method was explained and supported
  • 3rd September, 2012: First version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)