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 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 VB.Net. This Web server just returns html formatted files and also supports images. It does not supports any kind of scripting. I have developed a console-based application for simplicity.
The Web Server
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 data directory i.e.
Mimes.Dat
Vdirs.Dat
Default.Dat
Mime.Dat will have the mime type supported by our web server, Syntax: <EXTENSION>; <MIME Type>
e.g.
.html; text/html
.htm; text/html
.gif; image/gif
.bmp; image/bmp
VDirs.Dat will have the virtual directory Information. Syntax: <VirtualDir>; <PhysicalDir>
e.g.
/; C:\myWebServerRoot/
/test/; C:\myWebServerRoot\Imtiaz\
Note: We have to include all the directories used by our web server, for example, if the html page contains a reference to images and we want to display image, we need to include it also. e.g.
/images/; c:myWebServerRoot\Images\
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 our code let us first look the header information which browser will pass while requesting for our web site
Let say we request test.html. We type http://localhost:5050/test.html (Remember to include port in the url), Here is what the web server gets.
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, *
Let us dive into the code.
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 ;
public MyWebServer()
{
try
{
myListener = new TcpListener(port) ;
myListener.Start();
Console.WriteLine("Web Server Running... Press ^C to Stop...");
Thread th = new Thread(new ThreadStart(StartListen));
th.Start() ;
}
catch(Exception e)
{
Console.WriteLine("An Exception Occurred while Listening :"
+ e.ToString());
}
}
We defined a 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 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 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, 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
{
sr = new StreamReader("data\\Default.Dat");
while ((sLine = sr.ReadLine()) != null)
{
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;
sDirName.Trim();
sMyWebServerRoot = sMyWebServerRoot.ToLower();
sDirName = sDirName.ToLower();
try
{
sr = new StreamReader("data\\VDirs.Dat");
while ((sLine = sr.ReadLine()) != null)
{
sLine.Trim();
if (sLine.Length > 0)
{
iStartPos = sLine.IndexOf(";");
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 = "";
sRequestedFile = sRequestedFile.ToLower();
int iStartPos = sRequestedFile.IndexOf(".");
sFileExt = sRequestedFile.Substring(iStartPos);
try
{
sr = new StreamReader("data\\Mime.Dat");
while ((sLine = sr.ReadLine()) != null)
{
sLine.Trim();
if (sLine.Length > 0)
{
iStartPos = sLine.IndexOf(";");
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 sends header information to the browser (client)
public void SendHeader(string sHttpVersion, string sMIMEHeader,
int iTotBytes, string sStatusCode, ref Socket mySocket)
{
String sBuffer = "";
if (sMIMEHeader.Length == 0 )
{
sMIMEHeader = "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 looks for the request type, extracts the HTTP Version, file and directory information.
if ( sDirName == "/")
sLocalDir = sMyWebServerRoot;
else
{
sLocalDir = GetLocalPath(sMyWebServerRoot, sDirName);
}
Console.WriteLine("Directory Requested : " + sLocalDir);
if (sLocalDir.Length == 0 )
{
sErrorMessage = "<H2>Error!! Requested Directory does not exists</H2><Br>";
SendHeader(sHttpVersion, "", sErrorMessage.Length,
" 404 Not Found", ref mySocket);
SendToBrowser(sErrorMessage, ref mySocket);
mySocket.Close();
continue;
}
Note: Microsoft Internet Explorer usually displays a 'friendly' 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.
if (sRequestedFile.Length == 0 )
{
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 identify the MIME type
String sMimeType = GetMimeType(sRequestedFile);
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);
BinaryReader reader = new BinaryReader(fs);
byte[] bytes = new byte[fs.Length];
int read;
while((read = reader.Read(bytes, 0, bytes.Length)) != 0)
{
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.Close();
Compilation and Execution
To compile the program from the command line:
In my version of.NET I don't need to specify any library name, may be for old versions we need to add the reference to the dll, using /r parameter.
To run the application simply type the application name and press Enter.
Now, let say the user sends the request. Our web server will identify the default file name and send it to the browser.
The user can also request the Image file..
Possible Improvements
There are many improvements can be made to the WebServer application. Currently it does not supports embedded images and has no supports 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. The code to write basic ISAPI filters 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 if I can get any comments on improving the same. I am also looking forward for adding the capabilities of calling Microsoft ISAPI Filter from this application.