Implementing a Web server

In document in .NET (Page 135-145)

HTTP: Communicating with Web Servers

4.3 Web servers

4.3.1 Implementing a Web server

Start a new Visual Studio .NET project as usual. Draw two textboxes,

tbPath and tbPort, onto the form, followed by a button, btnStart, and a list box named lbConnections, which has its view set to list.

At the heart of an HTTP server is a TCP server, and you may notice an overlap of code between this example and the TCP server in the previous chapter. The server has to be multithreaded, so the first step is to declare an Array List of sockets:

C#

public class Form1 : System.Windows.Forms.Form {

private ArrayList alSockets;

...

VB.NET

Public Class Form1 Inherits System.Windows.Forms.Form Private alSockets As ArrayList

...

Every HTTP server has an HTTP root, which is a path to a folder on your hard disk from which the server will retrieve Web pages. IIS has a default HTTP root of C:\inetpub\wwwroot; in this case, we shall use the path in which the application is saved.

To obtain the application path, we can use Application.Executable-Path, which returns not only the path but also the filename, and thus we can trim off all characters after the last backslash.

C#

private void Form1_Load(object sender, System.EventArgs e) {

tbPath.Text = Application.ExecutablePath;

// trim off filename, to get the path tbPath.Text =

tbPath.Text.Substring(0,tbPath.Text.LastIndexOf("\\"));

}

VB.NET

Private Sub Form1_Load(ByVal sender As Object, _ ByVal e As System.EventArgs)

tbPath.Text = Application.ExecutablePath ' trim off filename, to get the path tbPath.Text = _

tbPath.Text.Substring(0,tbPath.Text.LastIndexOf("\")) End Sub

Clicking the Start button will initialize the Array List of sockets and start the main server thread. Click btnStart:

C#

private void btnStart_Click(object sender, System.EventArgs e) {

alSockets = new ArrayList();

Thread thdListener =

new Thread(new ThreadStart(listenerThread));

thdListener.Start();

}

VB.NET

Private Sub btnStart_Click(ByVal sender As Object, _ ByVal e As System.EventArgs)

alSockets = New ArrayList()

Dim thdListener As Thread = New Thread(New _ ThreadStart( AddressOf listenerThread)) thdListener.Start()

End Sub

The listenerThread function manages new incoming connections, allocating each new connection to a new thread, where the client’s requests will be handled.

HTTP operates over port 80, but if any other application is using port 80 at the same time (such as IIS), the code will crash. Therefore, the port for this server is configurable. The first step is to start the TcpListener on the port specified in tbPort.Text.

This thread runs in an infinite loop, constantly blocking on the

AcceptSocket method. Once the socket is connected, some text is written to the screen, and a new thread calls the handlerSocket function.

The reason for the lock(this) command is that handlerSocket

retrieves the socket by reading the last entry in ArrayList. In the case where two connections arrive simultaneously, two entries will be written to

ArrayList, and one of the calls to handlerSocket will use the wrong socket. Lock ensures that the spawning of the new thread cannot happen at the same time as the acceptance of a new socket.

C#

public void listenerThread() {

int port =0;

port = Convert.ToInt16(tbPort.Text);

TcpListener tcpListener = new TcpListener(port);

tcpListener.Start();

while(true) {

Socket handlerSocket = tcpListener.AcceptSocket();

if (handlerSocket.Connected) {

lbConnections.Items.Add(

handlerSocket.RemoteEndPoint.ToString() + " connected."

);

lock(this) {

alSockets.Add(handlerSocket);

ThreadStart thdstHandler = new ThreadStart(handlerThread);

Thread thdHandler = new Thread(thdstHandler);

thdHandler.Start();

} } } }

VB.NET

Public Sub listenerThread() Dim port As Integer = 0

port = Convert.ToInt16(tbPort.Text)

Dim tcpListener As TcpListener = New TcpListener(port) tcpListener.Start()

do

Dim handlerSocket As Socket = tcpListener.AcceptSocket() If handlerSocket.Connected = true then

lbConnections.Items.Add( _

handlerSocket.RemoteEndPoint.ToString() + " _ connected.")

syncLock(me)

alSockets.Add(handlerSocket)

Dim thdstHandler As ThreadStart = New _ ThreadStart(AddressOf handlerThread) Dim thdHandler As Thread = New _ Thread(thdstHandler)

thdHandler.Start() end syncLock

end if loop End sub

The handlerThread function is where HTTP is implemented, albeit minimally. Taking a closer look at the code should better explain what is happening here.

The first task this thread must perform, before it can communicate with the client to which it has been allocated, is to retrieve a socket from the top of the public ArrayList. Once this socket has been obtained, it can then create a stream to this client by passing the socket to the constructor of a

NetworkStream.

To make processing of the stream easier, a StreamReader is used to read one line from the incoming NetworkStream. This line is assumed to be:

GET <some URL path> HTTP/1.1

HTTP posts will be handled identically to HTTP gets. Because this server has no support for server-side scripting, there is no use for anything else in the HTTP POST data, or anything else in the HTTP Request header for that matter.

Assuming that the HTTP request is properly formatted, we can extract the requested page URL from this line by splitting it into an array of strings (verbs[]), delimited by the space character.

The next task is to convert a URL path into a physical path on the local hard drive. This involves four steps:

1. Converting forward slashes to backslashes

2. Trimming off any query string (i.e., everything after the question mark)

3. Appending a default page, if none is specified; in this case,

“index.htm”

4. Prefixing the URL path with the HTTP root

Once the physical path is resolved, it can be read from disk and sent out on the network stream. It is reported on screen, and then the socket is closed. This server does not return any HTTP headers, which means the client will have to determine how to display the data being sent to it.

C#

public void handlerThread() {

Socket handlerSocket = (

Socket)alSockets[alSockets.Count-1];

String streamData = "";

String filename = "";

String[] verbs;

StreamReader quickRead;

NetworkStream networkStream = new NetworkStream(handlerSocket);

quickRead = new StreamReader(networkStream);

streamData = quickRead.ReadLine();

verbs = streamData.Split(" ".ToCharArray());

// Assume verbs[0]=GET

filename = verbs[1].Replace("/","\\");

if (filename.IndexOf("?")!=-1) {

// Trim of anything after a question mark (Querystring) filename = filename.Substring(0,filename.IndexOf("?"));

}

if (filename.EndsWith("\\")) {

// Add a default page if not specified filename+="index.htm";

}

filename = tbPath.Text + filename;

FileStream fs = new FileStream(filename, FileMode.OpenOrCreate);

fs.Seek(0, SeekOrigin.Begin);

byte[] fileContents= new byte[fs.Length];

fs.Read(fileContents, 0, (int)fs.Length);

fs.Close();

// optional: modify fileContents to include HTTP header.

handlerSocket.Send(fileContents);

lbConnections.Items.Add(filename);

handlerSocket.Close();

}

VB.NET

Public Sub handlerThread() Dim handlerSocket As Socket = _

CType(alSockets(alSockets.Count-1), Socket) Dim streamData As String = ""

Dim filename As String = ""

Dim verbs() As String

Dim quickRead As StreamReader

Dim networkStream As NetworkStream = New _ NetworkStream(handlerSocket)

quickRead = New StreamReader(networkStream) streamData = quickRead.ReadLine()

verbs = streamData.Split(" ".ToCharArray()) ' Assume verbs[0]=GET

filename = verbs(1).Replace("/","\\") If filename.IndexOf("?")<>-1 Then

' Trim of anything after a question mark (Querystring) filename = filename.Substring(0,filename.IndexOf("?")) End If

If filename.EndsWith("\\") Then

' Add a default page if not specified filename+="index.htm"

End If

filename = tbPath.Text + filename Dim fs As FileStream = New _

FileStream(filename,FileMode.OpenOrCreate) fs.Seek(0, SeekOrigin.Begin)

Dim fileContents() As Byte = New Byte(fs.Length) {}

fs.Read(fileContents, 0, CType(fs.Length, Integer)) fs.Close()

' optional: modify fileContents to include HTTP header.

handlerSocket.Send(fileContents) lbConnections.Items.Add(filename) handlerSocket.Close()

End Sub

Most modern browsers can determine how best to display the data being sent to them, without the need for Content-Type headers. For instance, Internet Explorer can tell the difference between JPEG image data and HTML by looking for the standard JPEG header in the received data; how-ever, this system is not perfect.

A simple example is the difference between how XML is rendered on a browser window and how HTML is displayed. Without the Content-Type

header, Internet Explorer will mistake all XML (excluding the <?xml?> tag) as HTML. You can see this by viewing a simple XML file containing the text <a><b/></a> through this server.

And, the usual namespaces are thrown in:

C#

using System.Threading;

using System.Net;

using System.Net.Sockets;

using System.Text;

using System.IO;

VB.NET

Imports System.Threading Imports System.Net

Imports System.Net.Sockets Imports System.Text

Imports System.IO

To test the server, you will need a simple HTML page. Save the follow-ing text as index.htm in the same folder where the executable is built (the HTTP root).

HTML

<html>

Hello world!

</html>

Run the server from Visual Studio .NET, change the port to 90 if you are running IIS, and press Start. Open a browser and type in http://

localhost:90. Localhost should be replaced by the IP address of the server, if you are running the server on a second computer (Figure 4.7).

As mentioned previously, the server does not return HTTP headers. It is worthwhile to extend the example to include one of the more important headers, Content-Type, to save data from being misinterpreted at the client.

Figure 4.7 HTTP server application.

First, implement a new function called getMime(). This will retrieve a file’s MIME type from the computer’s registry from its file extension:

C#

public string getMime(string filename) {

FileInfo thisFile = new FileInfo(filename);

RegistryKey key = Registry.ClassesRoot;

key = key.OpenSubKey(thisFile.Extension);

return key.GetValue("Content Type").ToString();

}

VB.NET

Public Function getMime(ByVal filename As String) As String Dim thisFile As FileInfo = New FileInfo(filename) Dim key As RegistryKey = Registry.ClassesRoot key = key.OpenSubKey(thisFile.Extension)

Return key.GetValue("Content Type").ToString() End Function

If you have never used Windows registry before, this code may need a little explaining. The Windows registry is a repository for information that holds the vast amount of settings and preferences that keep Windows tick-ing over. You can view and edit the registry ustick-ing Registry Editor (Figure 4.8); start this by clicking Start→→→→Run and typing regedit or regedt32.

To view MIME types that correspond with file type extensions, click on HKEY_CLASSES_ROOT, scroll down to the file extension in question, and look at the Content Type key on the right-hand side of the screen.

Figure 4.8 Registry Editor utility.

This data is accessed programmatically by first extracting the file type extension using the Extension property of a FileInfo object. The first step in drilling down through the registry data is to open the root key. In this case, it is Registry.ClassesRoot.

The .html subkey is then opened using the openSubKey method.

Finally, the Content Type value is retrieved using the getValue statement and returned as a string to the calling function.

Now the final call to the Send method must be replaced by a slightly more elaborate sending procedure, which issues correct HTTP headers:

C#

handlerSocket.Send(fileContents);

VB.NET

handlerSocket.Send(fileContents)

These become:

C#

string responseString = "HTTP/1.1 200 OK\r\nContent-Type: " + getMime(filename) + "\r\n\r\n";

System.Collections.ArrayList al = new ArrayList();

al.AddRange(Encoding.ASCII.GetBytes(responseString));

al.AddRange(fileContents);

handlerSocket.Send((byte[])al.ToArray((new byte()).GetType()));

VB.NET

Dim responseString As String

responseString = "HTTP/1.1 200 OK" + vbCrLf + _

"Content-Type: " + getMime(filename) + vbCrLf + vbCrLf Dim al As System.Collections.ArrayList = New ArrayList al.AddRange(Encoding.ASCII.GetBytes(responseString)) al.AddRange(fileContents)

handlerSocket.Send(CType( _

al.ToArray((New Byte).GetType()), Byte()))

Finally, to support the registry access functionality, we need to include an extra namespace:

C#

using Microsoft.Win32;

VB.NET

Imports Microsoft.Win32

To demonstrate the difference this makes to running the server, create two files, test.txt and test.xml, both containing the text <a><b/></a>. Save them both in the HTTP root of your server and type in http:localhost/test.xml and http:localhost/test.txt. You will notice that test.xml will be rendered as a collapsible tree, and the text file will be shown as a series of characters.

In document in .NET (Page 135-145)