Wednesday, April 11, 2012

NancyFx combined with WebSocket server in one executable

Wednesday, April 11, 2012 Posted by Andre Broers , , , , ,
For a project here at OrionSoftware we needed a test application that displays realtime results to a webpage. Nice moment to digg a bit deeper in the WebSockets protocol. It only works on certain browsers so you can't demo on all browsers. At least chrome works, so that is the browser I use during this sample.

SuperWebSocket


To get websockets working in c# I will use a library because the implementation details of the different protocols are quite technical and subject to change as you can see on http://superwebsocket.codeplex.com/. You can download the binaries from this location: http://superwebsocket.codeplex.com/releases/view/84246.

Start


We begin with the creation of a new console application project in VisualStudio 2010 and call it MyWebSocket.Change the profile to a Full .Net Profile in the project properties. I always create an assemblies map in the root of the solution and add all my external referenced assemblies in here that aren't included via nuget. Unfortunately the SuperWebSocket server library isn't in nuget at this time so I extract the binary assemblies from the Net40 map of SuperWebSocket in this map.

Now I create references to the libraries by using Add Reference - Browse - and selecting all the assemblies in the assemblies folder.

We also need a reference to the System.Configuration assembly.

Now we need to add the NancyFX libraries by using nuget:
PM> install-package Nancy
Successfully installed 'Nancy 0.10.0'.
Successfully added 'Nancy 0.10.0' to MyNancy.

PM> install-package Nancy.Hosting.Self
Attempting to resolve dependency 'Nancy (= 0.10.0)'.
Successfully installed 'Nancy.Hosting.Self 0.10.0'.
Successfully added 'Nancy.Hosting.Self 0.10.0' to MyNancy.

PM>

Now it is time to show the code of the servers. One for NancyFX to host our webapplication and one for our WebSocket server.

NancyFX Server


We create a standard nancy host that works with a standard module displaying the index.html as the default route.
var host = new NancyHost(new Uri("http://127.0.0.1:8080"));
host.Start();  // start hosting

Console.WriteLine("Nancy Started at http://127.0.0.1:8080");
while (true) { Thread.Sleep(1000); }
host.Stop();  // stop hosting

And our module MainModule.cs:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Nancy;
using System.Dynamic;

namespace MyWebSocket
{
    public class MainModule : NancyModule
    {
        public MainModule()
        {
            Get["/"] = x =>
            {
                dynamic Model = new ExpandoObject();
                return View["index", Model];
            };
        }
    }
}

Now we create a new folder in our project and call it Views and add a new index.html view in it. Remember to set the CopyAlways property to this file. For now we leave the file as is. We will edit this file in the client section of this sample.

SuperWebSocket Server


Now create our second server which will respond to our websockets call from the client.
var socketServer = new WebSocketServer();
socketServer.Setup(new RootConfig(),
    new ServerConfig
    {
        Name = "SuperWebSocket",
        Ip = "Any",
        Port = 8181,
        Mode = SocketMode.Async,
    }, SocketServerFactory.Instance);

socketServer.NewMessageReceived += new SessionEventHandler<WebSocketSession, string>(socketServer_NewMessageReceived);
socketServer.NewSessionConnected += new SessionEventHandler<WebSocketSession>(socketServer_NewSessionConnected);
socketServer.SessionClosed += new SessionEventHandler<WebSocketSession, CloseReason>(socketServer_SessionClosed);
socketServer.Start();
while (true) { Thread.Sleep(1000); }
socketServer.Stop();

This will create a socketserver which reacts to three events. When a new session connection comes in it will send a message to all clients. When a session disconnects it will inform the other clients. When a message comes in it will send this message to all clients.
        private static List<WebSocketSession> m_Sessions = new List<WebSocketSession>();
        private static object m_SessionSyncRoot = new object();

        static void socketServer_NewMessageReceived(WebSocketSession session, string e)
        {
            SendToAll(session.IdentityKey + ": " + e);
        }

        static void socketServer_NewSessionConnected(WebSocketSession session)
        {
            lock (m_SessionSyncRoot)
                m_Sessions.Add(session);

            SendToAll("System: " + session.IdentityKey + " connected");
        }

        static void socketServer_SessionClosed(WebSocketSession session, CloseReason reason)
        {
            lock (m_SessionSyncRoot)
                m_Sessions.Remove(session);

            if (reason == CloseReason.ServerShutdown)
                return;

            SendToAll("System: " + session.IdentityKey + " disconnected");
        }

        static void SendToAll(string message)
        {
            lock (m_SessionSyncRoot)
            {
                foreach (var s in m_Sessions)
                {
                    s.SendResponseAsync(message);
                }
            }
        }

This will keep a list of all sessions m_Sessions. We need a lock on this object because it will be read and written to by multiple threads.

Complete the server


To run both servers I choose to create two tasks with both servers as a delegate. A keypress will end the program.

Here is the complete source:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using SuperWebSocket;
using SuperSocket.SocketBase.Config;
using SuperSocket.SocketBase;
using SuperSocket.SocketEngine;
using Nancy.Hosting.Self;
using System.Threading;
using System.Threading.Tasks;

namespace MyWebSocket
{
    class Program
    {
        private static List<WebSocketSession> m_Sessions = new List<WebSocketSession>();
        private static object m_SessionSyncRoot = new object();

        static void Main(string[] args)
        {
            var tokenSource = new CancellationTokenSource();
            CancellationToken ct = tokenSource.Token;

            // Create a task and supply a user delegate by using a lambda expression.
            var taskA = new Task(() =>
            {
                var host = new NancyHost(new Uri("http://127.0.0.1:8080"));
                host.Start();  // start hosting

                Console.WriteLine("Nancy Started at http://127.0.0.1:8080");
                while (!ct.IsCancellationRequested) { Thread.Sleep(1000); }
                host.Stop();  // stop hosting
            }, tokenSource.Token);

            var taskB = new Task(() =>
            {
                var socketServer = new WebSocketServer();
                socketServer.Setup(new RootConfig(),
                    new ServerConfig
                    {
                        Name = "SuperWebSocket",
                        Ip = "Any",
                        Port = 8181,
                        Mode = SocketMode.Async,
                    }, SocketServerFactory.Instance);

                socketServer.NewMessageReceived += new SessionEventHandler<WebSocketSession, string>(socketServer_NewMessageReceived);
                socketServer.NewSessionConnected += new SessionEventHandler<WebSocketSession>(socketServer_NewSessionConnected);
                socketServer.SessionClosed += new SessionEventHandler<WebSocketSession, CloseReason>(socketServer_SessionClosed);
                socketServer.Start();
                while (!ct.IsCancellationRequested) { Thread.Sleep(1000); }
                socketServer.Stop();
            }, tokenSource.Token);

            // Start the task.
            taskA.Start();
            taskB.Start();
            Console.ReadKey();
            tokenSource.Cancel();
        }

        static void socketServer_NewMessageReceived(WebSocketSession session, string e)
        {
            SendToAll(session.IdentityKey + ": " + e);
        }

        static void socketServer_NewSessionConnected(WebSocketSession session)
        {
            lock (m_SessionSyncRoot)
                m_Sessions.Add(session);

            SendToAll("System: " + session.IdentityKey + " connected");
        }

        static void socketServer_SessionClosed(WebSocketSession session, CloseReason reason)
        {
            lock (m_SessionSyncRoot)
                m_Sessions.Remove(session);

            if (reason == CloseReason.ServerShutdown)
                return;

            SendToAll("System: " + session.IdentityKey + " disconnected");
        }

        static void SendToAll(string message)
        {
            lock (m_SessionSyncRoot)
            {
                foreach (var s in m_Sessions)
                {
                    s.SendResponseAsync(message);
                }
            }
        }

    }
}

Client Side


The client side is our created index.html. It's all arranged from javascript.
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <title>MyWebSocket</title>
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js" type="text/javascript"></script>
    <script type="text/javascript">
        $(document).ready(function () {
            $("#button").click(function() { socket.send($("#message").val()); }).attr("disabled", "disabled");
            var socket = new WebSocket('ws://localhost:8181/sample');
            socket.onopen = function () {
                $("#messages").append("handshake successfully established. May send data now...<br/>");
                $("#button").removeAttr("disabled");
            };
            socket.onclose = function () {
                $("#messages").append("connection closed<br/>");
                $("#button").attr("disabled", "disabled");
            };
            // when data is comming from the server, this metod is called
            socket.onmessage = function (evt) {
                $("#messages").append("# " + evt.data + "<br/>");
            };
        });
    </script>
</head>
<body>
    <label for="message">Message: </label>
    <input id="message" type="text" />
    <button id="button" type="button">Send</button>
    <div id="messages"></div>
</body>
</html>

Here we have a simple chat client. It has an input where you can type a message and it will be distributed to all other clients when you click the button. The button will only be active when the connection to our websocket server is established.

In the jquery document ready callback we create a new WebSocket object with the url matching the server. We create three callbacks on this object. And our chat application is complete.

Run the Chat Application


When we run the application from VisualStudio we can browse with two Chrome windows to our NancyFX url: http://127.0.0.1:8080  and we can chat.