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 Beta 2. Microsoft (R) Visual C# Compiler Version 7.00.9254 [CLR version v1.0.2914]. It can be used with Beta 1 with some minor modification. This application can co-exists 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 Visual Basic .Net.

This Web server returns HTML formatted files and also supports images. It does not loads the embedded image or supports any kind of scripting. I have developed a console-based application for simplicity.

First...

First we will define the root folder for the Web server. Eg: C:\MyPersonalwebServer. Then create a data directory underneath the root directory Eg: C:\MyPersonalwebServer\Data. Create three files under 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 <EXTENSION>; <MIME Type>e.g.
.html; text/html
.htm; text/html
.bmp; image/bmp

VDirs.Dat will have the virtual directory Information. The format will be </VIRTUALDIR />; <PHYSICAL>e.g.
<DRIVE:\PHYSICALDIR \>/; 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 file for simplicity, we can use XML, registry or even hard code it. Before proceeding to the code first look at the header information which the browser will pass while requesting the web site

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

</DRIVE:\PHYSICALDIR>

GET /test.html HTTP/1.1
Accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, 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

Diving 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 make the TcpListener start listening on th
    //given port. It also calls a Thread on the method StartListen(). 
   public MyWebServer()
   {
      try
      {
          //start listing 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());
      }
   }

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

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

We have already stored the default file name in the default.dat and stored in the data directory. The GetTheDefaultFileName function takes the directory path as input, open the default.dat file and looks for the file in the directory provided and returns the file name or blank depends 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( sLocalDirectory + sLine) == true)
            break;
      }
   }
   catch(Exception e)
   {
      Console.WriteLine("An Exception Occurred : " + e.ToString());
   }
   if (File.Exists( sLocalDirectory + sLine) == true)
      return sLine;
   else
      return "";
}

We also need to resolve the virtual directory to the actual physical directory like we do in IIS. We have already stored the mapping between the Actual and Virtual directory 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.Trim();



   // Convert to lowercase
   sMyWebServerRoot = sMyWebServerRoot.ToLower();

   // Convert to lowercase
   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.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 Vdirs.dat to find out the list virtual directories
      sr = new StreamReader("data\\Mime.Dat");

      while ((sLine = sr.ReadLine()) != null)
      {

         sLine.Trim();

         if (sLine.Length > 0)
         {
            //find the separator
            iStartPos = sLine.IndexOf(";");

            // Convert to lower case
            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 write the function, to build and sends 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 = sBuffer + sHttpVersion + sStatusCode + "\r\n";
   sBuffer = sBuffer + "Server: cx1193719-b\r\n";
   sBuffer = sBuffer + "Content-Type: " + sMIMEHeader + "\r\n";
   sBuffer = sBuffer + "Accept-Ranges: bytes\r\n";
   sBuffer = 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) { if (( numBytes = mySocket.Send(bSendData, bSendData.Length,0)) == -1) Console.WriteLine("Socket Error cannot Send Packet"); else { Console.WriteLine("No. of bytes send {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==================\n
          CLient 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.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 string from bytes then look for the request type, 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 exists then
// dispaly the error message
if (sLocalDir.Length == 0 )
{
   sErrorMessage = "<H2>Error!! Requested Directory does not exists</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 GetLocalPath function to get the physical directory information, if the directory not found (or does not mapped with 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 error occurred it is thrown to 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 == "")
   {
      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 indentify the Mime type:


/////////////////////////////////////////////////////////////////////
// Get TheMime 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 Exists...</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:


Compile.jpg
Click here for larger image

In my version of .NET I don't need to specify any library name, may be for old version we require to add the reference to dll, using /r parameter.

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


RunApp.jpg
Click here for larger image

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

HtmlOutput.jpg

User can also request the Image file..

ImgOutput.jpg

Possible Improvements

There are many improvements can be made to the WebServer application. Currently it does not supports embedded images and no supports for scripting. You can write your own ISAPI filter for the same or you can use the IIS ISAPI filter for learning purpose. The code to write basic ISAPI filter is very well explained at ISAPI Filters: Designing SiteSentry, an Anti-Scraping Filter for IIS

Conclusion

This article gives very basic idea of writing Web server application, lots of improvement can be done. I'll appreciate any comments on improving this. I am also looking forward to adding the capabilities of calling a Microsoft ISAPI filter from this application.

Imtiaz Alam is a Senior Developer, currently residing in Phoenix, Arizona.He has more than five years of development experience in developing Mirosoft based Solution. He can be reached at alamimtiaz@hotmail.com.

Downloads

Download source - xx Kb


Comments

  • server side scripting

    Posted by zzarzzur on 04/25/2010 01:42pm

    how do you add server side scripting like php to this web server

    Reply
  • messy code

    Posted by kodart on 10/14/2008 01:47am

    take a look at xf.server api, it allows to write much better network applications.

    http://code.msdn.microsoft.com/IOCPServer

    Reply
  • adding imbeded image support

    Posted by Legacy on 05/29/2003 12:00am

    Originally posted by: Allen

    How hard would it be to add imbeded image support to this code

    • how?

      Posted by real_SErhio on 08/13/2004 10:02am

      How to add imbeded image support to this code? pls.

      Reply
    Reply
  • thanks a lot

    Posted by Legacy on 01/20/2002 12:00am

    Originally posted by: ahmad budiman jalil

    i would like learn it.
    
    

    Reply
  • What would the pool consist of?

    Posted by Legacy on 12/03/2001 12:00am

    Originally posted by: jAnGbAkSa

    How would you implement this using thread pools? and how many threads would you have to spawn?
    would the pool just consist of Sockets?
    Can anyone explain?

    Reply
  • Thread pool?

    Posted by Legacy on 11/13/2001 12:00am

    Originally posted by: Nemanja Trifunovic

    This is very nice example. The first thing to improve, IMHO, would be to add a thread pool, instead of creating a new thread for each request.

    Reply
Leave a Comment
  • Your email address will not be published. All fields are required.

Top White Papers and Webcasts

  • Protecting business operations means shifting the priorities around availability from disaster recovery to business continuity. Enterprises are shifting their focus from recovery from a disaster to preventing the disaster in the first place. With this change in mindset, disaster recovery is no longer the first line of defense; the organizations with a smarter business continuity practice are less impacted when disasters strike. This SmartSelect will provide insight to help guide your enterprise toward better …

  • Savvy enterprises are discovering that the cloud holds the power to transform IT processes and support business objectives. IT departments can use the cloud to redefine the continuum of development and operations—a process that is becoming known as DevOps. Download the Executive Brief DevOps: Why IT Operations Managers Should Care About the Cloud—prepared by Frost & Sullivan and sponsored by IBM—to learn how IBM SmartCloud Application services provide a robust platform that streamlines …

Most Popular Programming Stories

More for Developers

Latest Developer Headlines

RSS Feeds