Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C#

C# WebServer Using Sockets

5.00/5 (27 votes)
23 Jan 2014CPOL7 min read 105.9K   9.9K  
How to make a simple web server which supports GZIP compression, applications, and sessions.

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):

  1. http://localhost:8080/chat/ to test the ChatServer application,that use HTTP long polling client side, you need to open two or more browsers.

    Sample Image

    Sample Image

  2. 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. 

    Sample Image

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.

  • SocketEngine: This component takes care to accept a sockets connection and rebound up all requests. For better performance I left the classic TCP listener implementation for the new socketasynceventargs pattern. I took a simple socket engine from the MSDN repository adding two things in the code: an event proper handler on the receive callback and a socket loop receive.
  • ComunicationLayer: Contains the SocketComunicator component that abstracts the SocketEngine and exposes an input-output communication interface.
  • BizApplication: This is the business logic layer, leads the request from the listener to an application and maintains the sessions. When a new request is reached, the first operation is a validation test, then the system generates the right application instance to assign the request and sends a response back to the browser. If not exists, an error page (404) is sent. The details of this BizApplication follows in the article.
  • Service: This is the point where we need a component to connect all the pieces together. This component is based on a simple pattern: Response(Elaborate(Request)). The purpose of the service depends exclusively on the application layer built-in over. So the final code would be something like this:
    C#
    Server server=new Server();
    server.AddService<HttpService<myLogger>>(8080);

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:

XML
<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:

C#
///Server/Service.sc

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()
    {
     //Dispose resources
    }
}

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:

C#
///Server/WebServer.sc

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.

C#
///ComunicationLayer/SocketComunicator.cs
...
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.

C#
public RawResponse GetResponse(RawRequest req)
{
    RawResponse service_output = null;
    HttpRequest httpreq = null;
    if (HttpApplicationManager.TryValidate(req, out httpreq))
    {
        ///
        /// ManageHttpRequest method deals corrects http requests
        ///                       
        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).

C#
///MainBiz/Http/HttpApplicationManager.cs

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];
    ///
    /// Immediately check if exist an ApplicationUniqueName equal to mainpath
    ///
    if (!applications.ContainsKey(mainPath)) return false;
    //Ok the applicatoin Exist!
    
    SessionManager sessionMgr = applications[mainPath];
    
    ApplicationSettings settings = sessionMgr.Info;
    string sessionKey = string.Empty;

    switch (settings.SessionType)
    {
        case ApplicationSessionMode.SingletonSession:
            ///
            /// SingletonSession 
            ///
            application = sessionMgr.GetOrCreateSingletonInstance();
            return true;
        case ApplicationSessionMode.BrowserSession:
            ///
            /// We need a session key to identfy one particolar browser
            ///                    
            sessionKey = e.Rawrequest.Connection.IP + "@" + e.Requests["User-Agent"];
            break;
        case ApplicationSessionMode.IpSession:
            ///
            /// We need a session key to identfy one Ip address
            ///                    
            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.

C#
///Server/Services/HttpServices.cs

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)
                ///
                /// application error
                ///
                throw new InvalidOperationException("Application " + 
                          reqhttp.Path + " not respond.");
            if (reqhttp.Type == HttpRequestType.HttpPage)
            {
           
                switch (session.Info.ResponseMode)
                {
                    case ApplicationResponseBehavior.ShareAndSend:
                        ///
                        ///  We share the application response with others sessions
                        ///
                        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
                    {
                        ///  htpp://localhost:port:/
                        output = HttpHelper.Generate404Page(reqhttp, "",
                                     "Welcome :)","Server Home Page");
                    }
                    break;
                case HttpRequestType.HttpStaticRequest:
                     ///
                     /// No application has been found,
                     /// but in this case of  HttpStaticRequest (css, js, png ecc.. ) 
                     /// we try responding anyway chaining the root directrory with the url path , 
                     /// obvioulsy if the file not exist we send 404 response header.
                     ///
                    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:

C#
protected override void PageLoad(HttpRequest req)
{
    ///
    /// The ChatServer application is strictly relate with the javascript implementation,
    /// so if the request contains query url parameters means
    /// that is invoked by the XMLHttpRequest object (ajax pattern).
    /// You can choose a different way to recognize an pageload event 
    /// (called by Asp.net postback) instead of ajax event.
    ///
    ///  The query url request has this pattern:
    ///   ?op=<operation>&<par1>=<value1>&<par2>=<value2>
    ///   where op stands for 'operation'.
    ///
    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":
                ///
                /// Login operation process steps:
                ///    1) Check username and password , if one of these is empty we respond with an alert
                ///    2) Validation of the user
                ///    3) Respond with a redirect
                ///
                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":
                ///
                /// When the room page is loaded start sending 'listen' operation request in loop,
                /// at every listen request we respond with a chatmessage getting
                /// from the queue or,if is empty ,with a skip action message.
                /// But firstable we sent adduser action message
                /// with SharedChatMessage envelop for notify the new user.
                ///
                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":
                ///
                /// A chat message has been sent by the user,
                ///     firstable we build ChatMessage packet replacing
                ///     the response with SharedChatMessage envelop,
                ///     that is why SharedChatMessage is visible
                ///     to the other session (see OnNewShareResponse).
                ///
                ///

                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 the user not perform the login the roomPage
        /// will not be loaded, will be sent a login page
        ///
        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.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)