Introduction
This article shows how make a simple web server which supports GZIP compression, applications, and sessions. This project is implemented using C# with .NET 4 and Visual Studio 2010. Two demo applications are included in the project. 1-Launch the server tester, check if port 8080 is available, 2- click 'Start', and type in your browser one of these URIs (if you use browsers like IE, it is recommended to use the IE9 version):
- http://localhost:8080/chat/ to test the ChatServer application,that use HTTP long polling client side, you need to open two or more browsers.
- http://localhost:8080/desk/ to test the DesktopViewer application, this simple application shows you the server screen in a browser, use HTTP long polling with singleton session.
So what is a web server? In a few words, it is a program running in a computer that listens to requests in a specific endpoint and responds using the HTTP protocol. The request generally wants a resource back internally at the server; the resources in this proj could be 'static' (js, css, png... files) or 'dynamic' (html, htm, dhtml.. ). By default static resources are managed automatically by the application but you can change this behavior and respond with whatever you want (see the Desktop Viewer implementation).
Summary
The first view of the project: The solutions is composed of seven projects: SocketEngine, ComunicationLayer, Service, and BizApplication; a common library, a project with two demos, and a WinForms project used like a tester.
Using the code
First, let's see how to configure the server. Settings are located in the app.config file in the TesterProject and includes the root server directory, default error page root, and the path of the application-xml file. The application-xml file is read at service start-up and contains the information for loading our web application classes. For creating a new web application instance, we just need three things: the path of the DLL, the full name (namespace + classname) of the web application class, and the full name of the application settings class. For example:
<Application>
<Name>Chat</Name>
<Assembly>Demos.dll</Assembly>
<ApplicationSettingsClass>Chat.HttpApplication.ChatServerConfiguration</ApplicationSettingsClass>
<ApplicationClass>Chat.HttpApplication.ChatServer</ApplicationClass>
</Application>
For each assembly, we create a new SessionManager
structure which keeps the types and exposes methods for creating instances through the .NET Activator
class. Now, before I explain how the request from a socket is driven in a web application instance, it is helpful to see how the pieces are linked together. The structure of the server strictly follows the definition of the Service
class:
public class Service<PROVIDER,OUTPUTCHANNEL> : IServiceBase
where OUTPUTCHANNEL : IServerOutput
where PROVIDER : IServerService
{
OUTPUTCHANNEL sender;
PROVIDER provider;
public Service(SENDER sender, PROVIDER provider)
{
this.sender = sender;
this.provider = provider;
}
public void ParseNewRequest(RawRequest e)
{
sender.SendResponse(provider.GetResponse(e));
}
public void Dispose()
{
}
}
The service class provides a request/response model and binds a PROVIDER with a SENDER in a simple pattern: sender.SendResponse(provider.GetResponse(request));
. We need now an input component that uses the Service's IServiceBase
interface and calls the ParseNewRequest
method. In our vision the requests are coming from the browsers via sockets, so using this template with types: HttpService
as PROVIDER and SocketComunicator
as CHANNEL for input and output (in this case, these are the same but there exists a lot of contexts where the output channel and the input channel in a service may be different). We are now ready to create the HTTP service:
SocketComunicator In_Out_channel=new SocketComunicator();
Service<HttpService, SocketComunicator> service =
new Service <HttpService,SocketComunicator>(In_Out_channel, service);
In_Out_channel.SetServiceHandler(servicehost);
In_Out_channel.StartListen(port);
Now that we've seen the guidelines of the server architecture, let's see the details of the implementation. The SocketComunicator
class is used to receive and send data through a TCP/IP browser connection, expose the IChannelOutput
interface, and use IServiceBase
each time the engine receives something. The RawRequest
structure holds the socket and the data.
...
public void SetServiceHandler(IServiceBase wb)
{
this.serviceInterface = wb;
}
void Socket_OnNewDataReceived(object sender, SocketConnection e, byte[] dataRef)
{
if (this.serviceInterface != null)
this.serviceInterface.ParseNewRequest(new RawRequest() { Connection = e, RawData = dataRef });
}
...
Make sure every single Socket_OnNewDataReceived
call is completely asynchronous each other even though a thread is created, because the SocketAsycnEventArgs
class uses the I/O Completion Ports model. This is a great advantage to working asynchronously with sockets.
At this point, let's take a look at how the request is parsed by the provider. The request reaches HttpService
's GetResponse
function, and the first thing this method does is check if the request matches with the HTTP protocol. If these tests don't pass the connection will be closed, otherwise it creates a new HttpRequest
which contains all HTTP data as headers, paths, and QueryStrings.
public RawResponse GetResponse(RawRequest req)
{
RawResponse service_output = null;
HttpRequest httpreq = null;
if (HttpApplicationManager.TryValidate(req, out httpreq))
{
this.tracer.trace("New request: " + httpreq.CompleteRequest);
service_output = ManageHttpRequest(httpreq);
}
else
{
this.tracer.trace("Invalid request.");
service_output = new RawResponse(req) { Action = ResponseAction.Disconnect };
}
return service_output;
}
I assume that every request from a browser refers to a specific application in the server. The first part of the URI, what I call mainpath, indicates the name of the application to invoke. After having found it, the following parts are directly resolved by the application. For example, the following request: http//localhost:8080/chat/roompage2.html wants back a page called 'roompage2.html' from an an application called chat. To deliver a request to an application, we have to create it before, so that means every request does generate a new instance in the server? Depends, each application declares in its ApplicationSettings
class how the server has to deal with the session. In this project there are three ways to handle sessions: ApplicationSessionMode{ SingletonSession, IpSession, BrowserSession}
. For example, the ChatServer application uses the BrowserSession
mode. The HttpApplicationManager
component takes care of this issue by checking if the HTTP request matches with an existing session. If it doesn't exist, it creates a new one, and also checks how many sessions are expired. For example, after 30 seconds of inactivity (see the ApplicationSettings
class).
public bool TryGetApplicationInstance(HttpRequest e,
out ApplicationInstanceBase application)
{
application = null;
string[] paths = e.Path.Split(new string[] { "/" },
StringSplitOptions.RemoveEmptyEntries);
if (paths.Length == 0) return false;
string mainPath = paths[0];
if (!applications.ContainsKey(mainPath)) return false;
SessionManager sessionMgr = applications[mainPath];
ApplicationSettings settings = sessionMgr.Info;
string sessionKey = string.Empty;
switch (settings.SessionType)
{
case ApplicationSessionMode.SingletonSession:
application = sessionMgr.GetOrCreateSingletonInstance();
return true;
case ApplicationSessionMode.BrowserSession:
sessionKey = e.Rawrequest.Connection.IP + "@" + e.Requests["User-Agent"];
break;
case ApplicationSessionMode.IpSession:
sessionKey = e.Rawrequest.Connection.IP.ToString();
break;
}
application = sessionMgr.GetOrCreateInstanceBySessionKey(sessionKey);
return true;
The code is simple and self-explanatory. If TryGetApplicationInstance(reqhttp, out session)
succeeds the HTTP request is processed by the session. Before returning the output we check if the application requires to share the response with other sessions. This feature is useful when the server apps need to know the status about other sessions. If something goes wrong it is important to show in the browser the error details. Every exception thrown in the code is caught and the response becomes a new 404 page filled with error details.
public RawResponse ManageHttpRequest(HttpRequest reqhttp)
{
ApplicationResponse output = null;
try
{
ApplicationInstanceBase session = null;
if (this.appManager.TryGetApplicationInstance(reqhttp, out session))
{
output = session.ProcessRequest(reqhttp);
if (output == null)
throw new InvalidOperationException("Application " +
reqhttp.Path + " not respond.");
if (reqhttp.Type == HttpRequestType.HttpPage)
{
switch (session.Info.ResponseMode)
{
case ApplicationResponseBehavior.ShareAndSend:
this.appManager.ShareApplicationOutput(session, output, reqhttp);
break;
}
}
}
else
{
switch (reqhttp.Type)
{
case HttpRequestType.HttpPage:
if (reqhttp.Paths.Count > 0)
{
throw new InvalidOperationException("Application " +
reqhttp.Path + " not exist");
}
else
{
output = HttpHelper.Generate404Page(reqhttp, "",
"Welcome :)","Server Home Page");
}
break;
case HttpRequestType.HttpStaticRequest:
output = this.appManager.ResponseStaticResource(reqhttp);
break;
}
}
}
catch (Exception ex)
{
this.tracer.trace("ERROR" + ex.Message + "::" + ex.StackTrace);
output = HttpHelper.Generate404Page(reqhttp, ""+ex.Message+"::"+ex.StackTrace,
"Error occured parsing " + reqhttp.Path);
}
return output;
}
After displaying how the HTTP service works, let's take a look at how to make an application on it, like the ChatServer application. We begin by subclassing HttpApplicationBase
and providing an implementation for the two abstract methods: PageLoad
and ApplicationDirectory
. ApplicationDirectory
returns the physical root path where it is possible to find the HTML pages and resources like CSS, JS, and images used by the application. HttpApplicationBase
use this path to satisfy automatically all the requests. The difference between HttpPage
and HttpStaticRequest
is that HttpPage
requests can be resolved in the PageLoads
before. Some kinds of HttpPage
requests cannot be resolved by the HttpApplicationBase
layer, for example, all requests with a query string in the URI. This is the ChatServer PageLoad
:
protected override void PageLoad(HttpRequest req)
{
string page = Request.Paths[Request.Paths.Count - 1];
if (req.UrlParameters.Count > 0)
{
SharedChatMessage sharedresponse = null;
ChatMessage message = null;
string operation = req.GetQueryStringValue("op");
switch (operation)
{
case "login":
string username = req.GetQueryStringValue("username");
string password = req.GetQueryStringValue("password");
if (String.IsNullOrEmpty(username) || String.IsNullOrEmpty(password))
{
BuildChatResponse(new ChatMessage() { MessageCode =
(int)MessageType.alert, Value = "Login request error." }, true);
return;
}
currentUsername = username;
currentPassowrd = password;
isValidUser = true;
BuildChatResponse(new ChatMessage() { MessageCode =
(int)MessageType.eval, Value =
"window.location=\"" + roomPage + "\"" }, true);
return;
case "listen":
if (!sendAdduser)
{
sendAdduser = true;
message = new ChatMessage() { MessageCode =
(int)MessageType.adduser, User = currentUsername, };
BuildChatResponse(message, false);
this.response = sharedresponse =
new SharedChatMessage(Response.ResponseData, Response.AppRequest, message);
return;
}
if (localqueuemessages.Count == 0)
{
System.Threading.Thread.Sleep(500);
BuildChatResponse(new ChatMessage() { MessageCode =
(int)MessageType.skip, Value = "" }, false);
}
else
{
ChatMessage msg = null;
if (localqueuemessages.TryDequeue(out msg))
BuildChatResponse(msg, false);
}
return;
case "message":
string value = req.GetQueryStringValue("value");
message = new ChatMessage() { MessageCode = (int)MessageType.chatmessage,
Value = value, User = currentUsername };
BuildChatResponse(message, false);
sharedresponse =
new SharedChatMessage(Response.ResponseData, Response.AppRequest, message);
Response = sharedresponse;
return;
default:
throw new InvalidOperationException("Invalid request");
}
}
if (page == roomPage)
{
if (!isValidUser)
{
BuildResponseFile(ApplicationDirectory() + "\\" + loginPage,MimeType.text_html);
return;
}
else
{
byte[] room = Helper.GetFile(ApplicationDirectory() + "\\" + roomPage);
string msg = new string(Encoding.UTF8.GetChars(room));
msg = msg.Replace("", this.currentUsername);
BuildResponse(msg);
}
}
}
There are many improvements that can be made to the project. In this article I have focused on the architecture server and I haven't explained the details of how an HTTP request is parsed or an HTTP response is built. For this stuff, there are a lot of solutions, I just implemented one of them. If you use an SslStream
listener instead of a low level socket (which doesn't support native SSL encryption )you can deal with an HTTPS connection to the browser.