Creating your own Web Server using C#

Environment: C#, .Net


SUMMARY

This article explains how to write a simple web server application using C#. Though it can be developed in any .net-supported language, I chose C# for this example. The code is compiled using beta2. Microsoft (R) Visual C# Compiler Version 7.00.9254 [CLR version v1.0.2914]. It can be used with Beta1 with some minor modifications. This application can co-exist with IIS or any web server, the key is to choose any free port. I assume that the user has some basic understanding of .net and C# or VB.Net. This Web server just returns html formatted files and also supports images. It does not load the embedded image or support any kind of scripting. I have developed a console-based application for simplicity.

First, we will define the root folder for our web server. Eg: C:\MyPersonalwebServer, and will create a Data directory underneath, our root directory Eg: C:\MyPersonalwebServer\Data. We will Create three files under the data directory i.e.

  • Mimes.Dat
  • Vdirs.Dat
  • Default.Dat

Mime.Dat will have the mime type supported by our web server. The format will be

E.g.

.html; text/html
.htm; text/html
.bmp; image/bmp

VDirs.Dat will have the virtual directory Information. The format will be

E.g.

/; C:\myWebServerRoot/
test/; C:\myWebServerRoot\Imtiaz\

Default.Dat will have the virtual directory Information.

E.g.

default.html
default.htm
Index.html
Index.htm;

We will store all the information in plain text files for simplicity, we can use XML, registry, or even hard code it. Before proceeding to our code let us first look at the header information that the browser will pass while requesting our website

Let's say we request for test.html we type http://localhost:5050/test.html
(Remember to include the port in the URL), here is what the web server gets.

GET /test.html HTTP/1.1

  • Accept: image/gif, image/x-bitmap, image/jpeg, image/jpeg, application/vnd.ms-powerpoint, application/vnd.ms-excel, application/msword, */*
  • Accept-Language: en-usAccept-Encoding: gzip, deflate
  • User-Agent: Mozilla/4.0 (compatible; MSIE 5.5; Windows NT 4.0; .NET CLR 1.0.2914)
  • Host: localhost:5050Connection: Keep-Alive

Let us dive into the code

// MyWebServer Written by Imtiaz Alam
namespace Imtiaz
{
    using System;
    using System.IO;
    using System.Net;
    using System.Net.Sockets;
    using System.Text;
    using System.Threading;
    class MyWebServer
    {
        private TcpListener myListener;
        private int port = 5050; // Select any free port you wish
        // The constructor which makes the TcpListener start listening on the given port.
        // It also calls a Thread on the method StartListen().
        public MyWebServer()
        {
            try
            {
                // Start listening on the given port
                myListener = new TcpListener(port);
                myListener.Start();
                Console.WriteLine("Web Server Running... Press ^C to Stop...");
                // Start the thread which calls the method 'StartListen'
                Thread th = new Thread(new ThreadStart(StartListen));
                th.Start();
            }
            catch (Exception e)
            {
                Console.WriteLine("An Exception Occurred while Listening: " + e.ToString());
            }
        }
        // Additional methods and properties can be added here
    }
}

We defined the namespace, included the references required in our application, and initialized the port in the constructor, started the listener created a new thread, and called the startlisten function.

Now let us assume that the user does not supply the file name, in that case, we have to identify the default filename and send it to the browser, as in IIS we define the default document under the documents tab.

We have already stored the default file name in the default.dat and stored it in the data directory. The GetTheDefaultFileName function takes the directory path as input, opens the default.dat file looks for the file in the directory provided, and returns the file name or blank depending on the situation.

public string GetTheDefaultFileName(string sLocalDirectory)
{
    StreamReader sr;
    string sLine = "";
    try
    {
        // Open the default.dat to find out the list
        // of default file
        sr = new StreamReader("data\\Default.Dat");
        while ((sLine = sr.ReadLine()) != null)
        {
            // Look for the default file in the web server root folder
            if (File.Exists(Path.Combine(sLocalDirectory, sLine)))
                break;
        }
    }
    catch (Exception e)
    {
        Console.WriteLine("An Exception Occurred: " + e.ToString());
    }

    if (File.Exists(Path.Combine(sLocalDirectory, sLine)))
        return sLine;
    else
        return "";
}

We also need to resolve the virtual directory to the actual physical directory as we do in IIS. We have already stored the mapping between the Actual and Virtual directories in Vdir.Dat. Remember in all the cases the file format is very important.

public string GetLocalPath(string sMyWebServerRoot, string sDirName)
{
    StreamReader sr;
    string sLine = "";
    string sVirtualDir = "";
    string sRealDir = "";
    int iStartPos = 0;
    // Remove extra spaces
    sDirName = sDirName.Trim();
    // Convert to lowercase
    sMyWebServerRoot = sMyWebServerRoot.ToLower();
    sDirName = sDirName.ToLower();
    try
    {
        // Open the Vdirs.dat to find out the list virtual directories
        sr = new StreamReader("data\\VDirs.Dat");
        while ((sLine = sr.ReadLine()) != null)
        {
            // Remove extra spaces
            sLine = sLine.Trim();
            if (sLine.Length > 0)
            {
                // Find the separator
                iStartPos = sLine.IndexOf(";");
                // Convert to lowercase
                sLine = sLine.ToLower();
                sVirtualDir = sLine.Substring(0, iStartPos);
                sRealDir = sLine.Substring(iStartPos + 1);
                if (sVirtualDir == sDirName)
                {
                    break;
                }
            }
        }
    }
    catch (Exception e)
    {
        Console.WriteLine("An Exception Occurred: " + e.ToString());
    }

    if (sVirtualDir == sDirName)
        return sRealDir;
    else
        return "";
}

We also need to identify the Mime type, using the file extension supplied by the user.

public string GetMimeType(string sRequestedFile)
{
    StreamReader sr;
    string sLine = "";
    string sMimeType = "";
    string sFileExt = "";
    string sMimeExt = "";
    // Convert to lowercase
    sRequestedFile = sRequestedFile.ToLower();
    int iStartPos = sRequestedFile.IndexOf(".");
    sFileExt = sRequestedFile.Substring(iStartPos);
    try
    {
        // Open the Mime.Dat to find out the list of MIME types
        sr = new StreamReader("data\\Mime.Dat");
        while ((sLine = sr.ReadLine()) != null)
        {
            sLine = sLine.Trim();
            if (sLine.Length > 0)
            {
                // Find the separator
                iStartPos = sLine.IndexOf(";");
                // Convert to lowercase
                sLine = sLine.ToLower();
                sMimeExt = sLine.Substring(0, iStartPos);
                sMimeType = sLine.Substring(iStartPos + 1);
                if (sMimeExt == sFileExt)
                {
                    break;
                }
            }
        }
    }
    catch (Exception e)
    {
        Console.WriteLine("An Exception Occurred: " + e.ToString());
    }

    if (sMimeExt == sFileExt)
        return sMimeType;
    else
        return "";
}

Now we will write the function, to build and send header information to the browser (client)

public void SendHeader(string sHttpVersion, string sMIMEHeader, int iTotBytes, string sStatusCode, ref Socket mySocket)
{
    string sBuffer = "";
    // if Mime type is not provided set default to text/html
    if (sMIMEHeader.Length == 0)
    {
        sMIMEHeader = "text/html"; // Default Mime Type is text/html
    }
    sBuffer += sHttpVersion + sStatusCode + "\r\n";
    sBuffer += "Server: cx1193719-b\r\n";
    sBuffer += "Content-Type: " + sMIMEHeader + "\r\n";
    sBuffer += "Accept-Ranges: bytes\r\n";
    sBuffer += "Content-Length: " + iTotBytes + "\r\n\r\n";
    byte[] bSendData = Encoding.ASCII.GetBytes(sBuffer);
    SendToBrowser(bSendData, ref mySocket);
    Console.WriteLine("Total Bytes: " + iTotBytes.ToString());
}

The SendToBrowser function sends information to the browser. This is an overloaded function.

public void SendToBrowser(string sData, ref Socket mySocket)
{
    SendToBrowser(Encoding.ASCII.GetBytes(sData), ref mySocket);
}
public void SendToBrowser(byte[] bSendData, ref Socket mySocket)
{
    int numBytes = 0;
    try
    {
        if (mySocket.Connected)
        {
            numBytes = mySocket.Send(bSendData, bSendData.Length, 0);
            if (numBytes == -1)
                Console.WriteLine("Socket Error: Cannot Send Packet");
            else
                Console.WriteLine("No. of bytes sent: {0}", numBytes);
        }
        else
        {
            Console.WriteLine("Connection Dropped....");
        }
    }
    catch (Exception e)
    {
        Console.WriteLine("Error Occurred: {0}", e);
    }
}

We now have all the building blocks ready, now we will delve into the key function of our application.

public void StartListen()
{
    int iStartPos = 0;
    String sRequest;
    String sDirName;
    String sRequestedFile;
    String sErrorMessage;
    String sLocalDir;
    String sMyWebServerRoot = "C:\\MyWebServerRoot\\";
    String sPhysicalFilePath = "";
    String sFormattedMessage = "";
    String sResponse = "";
    while (true)
    {
        // Accept a new connection
        Socket mySocket = myListener.AcceptSocket();
        Console.WriteLine("Socket Type " + mySocket.SocketType);
        if (mySocket.Connected)
        {
            Console.WriteLine("\nClient Connected!!\n==================\nClient IP {0}\n", mySocket.RemoteEndPoint);
            // make a byte array and receive data from the client
            Byte[] bReceive = new Byte[1024];
            int i = mySocket.Receive(bReceive, bReceive.Length, 0);
            // Convert Byte to String
            string sBuffer = Encoding.ASCII.GetString(bReceive);
            // At present we will only deal with GET type
            if (sBuffer.Substring(0, 3) != "GET")
            {
                Console.WriteLine("Only Get Method is supported..");
                mySocket.Close();
                return;
            }
            // Look for HTTP request
            iStartPos = sBuffer.IndexOf("HTTP", 1);
            // Get the HTTP text and version e.g. it will return "HTTP/1.1"
            string sHttpVersion = sBuffer.Substring(iStartPos, 8);
            // Extract the Requested Type and Requested file/directory
            sRequest = sBuffer.Substring(0, iStartPos - 1);
            // Replace backslash with Forward Slash, if Any
            sRequest = sRequest.Replace("\\", "/");
            // If file name is not supplied add forward slash to indicate
            // that it is a directory and then we will look for the
            // default file name..
            if ((sRequest.IndexOf(".") < 1) && (!sRequest.EndsWith("/")))
            {
                sRequest = sRequest + "/";
            }
            // Extract the requested file name
            iStartPos = sRequest.LastIndexOf("/") + 1;
            sRequestedFile = sRequest.Substring(iStartPos);
            // Extract The directory Name
            sDirName = sRequest.Substring(sRequest.IndexOf("/"), sRequest.LastIndexOf("/") - 3);
        }
    }
}

The code is self-explanatory. It receives the request, converts it into a string from bytes then looks for the request type, and extracts the HTTP Version, file, and directory information.

/////////////////////////////////////////////////////////////////////
// Identify the Physical Directory
/////////////////////////////////////////////////////////////////////
if (sDirName == "/")
    sLocalDir = sMyWebServerRoot;
else
{
    // Get the Virtual Directory
    sLocalDir = GetLocalPath(sMyWebServerRoot, sDirName);
}
Console.WriteLine("Directory Requested : " + sLocalDir);
// If the physical directory does not exist then
// display the error message
if (sLocalDir.Length == 0)
{
    string sErrorMessage = "<H2>Error!! Requested Directory does not exist</H2><Br>";
    //sErrorMessage = sErrorMessage + "Please check data\\Vdirs.Dat";
    // Format The Message
    SendHeader(sHttpVersion, "", sErrorMessage.Length, " 404 Not Found", ref mySocket);
    // Send to the browser
    SendToBrowser(sErrorMessage, ref mySocket);
    mySocket.Close();
    continue;
}

Note. Microsoft Internet Explorer usually displays a 'friendy' HTTP Error Page if you want to display our error message then you need to disable the 'Show friendly HTTP error messages' option under the 'Advanced' tab in Tools->Internet Options. Next, we look if the directory name is supplied, we call the GetLocalPath function to get the physical directory information, if the directory is not found (or is not mapped with an entry in Vdir.Dat) error message is sent to the browser. Next, we will identify the file name, if the filename is not supplied by the user we will call the GetTheDefaultFileName function to retrieve the filename, if an error occurs it is thrown to the browser.

/////////////////////////////////////////////////////////////////////
// Identify the File Name
/////////////////////////////////////////////////////////////////////
// If The file name is not supplied then look in the default file list
if (sRequestedFile.Length == 0)
{
    // Get the default filename
    sRequestedFile = GetTheDefaultFileName(sLocalDir);
    if (sRequestedFile == "")
    {
        string sErrorMessage = "<H2>Error!! No Default File Name Specified</H2>";
        SendHeader(sHttpVersion, "", sErrorMessage.Length, " 404 Not Found", ref mySocket);
        SendToBrowser(sErrorMessage, ref mySocket);
        mySocket.Close();
        return;
    }
}

Then we need to identify the Mime type

/////////////////////////////////////////////////////////////////////
// Get The Mime Type
/////////////////////////////////////////////////////////////////////
String sMimeType = GetMimeType(sRequestedFile);
// Build the physical path
sPhysicalFilePath = sLocalDir + sRequestedFile;
Console.WriteLine("File Requested : " + sPhysicalFilePath);

Now the final steps of opening the requested file and sending it to the browser.

if (File.Exists(sPhysicalFilePath) == false)
{
    sErrorMessage = "<H2>404 Error! File Does Not Exist...</H2>";
    SendHeader(sHttpVersion, "", sErrorMessage.Length, " 404 Not Found", ref mySocket);
    SendToBrowser(sErrorMessage, ref mySocket);
    Console.WriteLine(sFormattedMessage);
}
else
{
    int iTotBytes = 0;
    sResponse = "";
    FileStream fs = new FileStream(sPhysicalFilePath, FileMode.Open, FileAccess.Read, FileShare.Read);
    // Create a reader that can read bytes from the FileStream.
    BinaryReader reader = new BinaryReader(fs);
    byte[] bytes = new byte[fs.Length];
    int read;
    while ((read = reader.Read(bytes, 0, bytes.Length)) != 0)
    {
        // Read from the file and write the data to the network.
        sResponse = sResponse + Encoding.ASCII.GetString(bytes, 0, read);
        iTotBytes = iTotBytes + read;
    }
    reader.Close();
    fs.Close();
    SendHeader(sHttpVersion, sMimeType, iTotBytes, " 200 OK", ref mySocket);
    SendToBrowser(bytes, ref mySocket);
    //mySocket.Send(bytes, bytes.Length, 0);
}
mySocket.Close();

Compilation and Execution

To compile the program from the command line.

Command line

In my version.net I don't need to specify any library name maybe for the old version we were required to add the reference to dll, using the/r parameter.

To run the application simply type the application name and press Enter.

Run the application

Now, let's say the user sends the request, our web server will identify the default file name and send it to the browser.

This a test

Users can also request the Image file.

Microsoft internet

Possible Improvements

Many improvements can be made to the WebServer application. Currently, it does not support embedded images and no support for scripting. We can write our own Isapi filter for the same or we can also use the IIS isapi filter for our learning purpose.

Conclusion

This article gives a very basic idea of writing Web server applications, lots of improvement can be done. I'd appreciate it if I could get any comments on improving the same. I am also looking forward to adding the capabilities of calling Microsoft Isapi Filter from this application.


Similar Articles