Introduction
With this tutorial, I want to explain how you can create a WebSocket application. I have chosen Silverlight as my client, but you can use any framework that interacts with JavaScript.
Prerequisites
- Silverlight 5 (like I mentioned above, you do not have to use Silverlight but I have chosen the latest version of Silverlight as my client)
- SuperWebSocket (you do not have to download this WebSocket framework, but it is worth visiting, to view the designer's intentions of use etc.)
- Visual Studio 2010 (you can also use the Express edition)
- VS2010 Silverlight Tool
- JMeter (performance testing)
Project Structure
- In the above solution, there is a 'Client' (Silverlight 5 project) that prompts the user for a unique store name.
- The 'Client.Web' project that hosts the Silverlight (Client) project and contains the JavaScript code (that makes the WebSocket calls) within the hosting ASPX page.
- A common 'SharedClasses' project, where a class is shared between the WCF service and the Silverlight 'Client' project.
- A WCF service called 'WcfServicereverse' that will perform some processing.
Running the Application
If you run the application, you will be present with a log in screen (below) where you just enter a unique name - the name is not validated here - but that is something that can be easy implemented - all we are after is a unique name that can be later used to indicate who pushed an update from the client on to the GUI.
Once logged into the application, the main screen will be displayes. It contains a grid, with cell foreground colors converted based on their cell values. A number of gauges are displayed below the grid that reflect the values within the grid itself. Below the gauges is a textbox and button to update the 'Fan' gauge (push the value to the server and then onto each connected session). At the bottom is a textblock that will display all the transactions from other stores\users or auto generated by the server and pushed to all the clients.
To update a 'Fan' and have it displayed in all the stores, enter a value between -20 and 20 and click 'Update Fan' (I do not validate the user input). This will force an update to the server that will push the update to all the clients; see below where the temperature was updated to -10.
If you have multiple browsers open, this change will also be reflected in those browsers.
Below, you can see the data that is being generated in Visual Studio IDE and pushed to the clients:
Code Explanation
Client (Silverlight) Code
namespace Client
{
[ScriptableType]
public partial class ClientPage : Page
{
private ObservableCollection<ThermoTemps> thermoCollection;
public ClientPage()
{
InitializeComponent();
ThermoCollection = new ObservableCollection<ThermoTemps>();
this.gridThermo.ItemsSource = ThermoCollection;
HtmlPage.RegisterScriptableObject("myObject", this);
}
private void button1_Click(object sender, RoutedEventArgs e)
{
HtmlPage.Window.Invoke("sendMessage", this.txtFan.Text);
}
[ScriptableMember]
public void UpdateText(string result)
{
try
{
string jsonString = result.Substring(result.IndexOf('{'));
ThermoTemps myDeserializedObj = new ThermoTemps();
DataContractJsonSerializer dataContractJsonSerializer =
new DataContractJsonSerializer(typeof(ThermoTemps));
MemoryStream memoryStream =
new MemoryStream(Encoding.Unicode.GetBytes(jsonString));
myDeserializedObj =
(ThermoTemps)dataContractJsonSerializer.ReadObject(memoryStream);
ThermoCollection.Add(myDeserializedObj);
this.radialBarCoolVent.Value = myDeserializedObj.CoolingVent;
this.radialBarFan.Value = myDeserializedObj.Fan; this.radialBarFreezer.Value = myDeserializedObj.Freezer; this.radialBarFridge.Value = myDeserializedObj.Fridge; this.radialBarIceMaker.Value = myDeserializedObj.IceMaker; }
catch (Exception ex) { }
mytextblock.Text += result + Environment.NewLine;
}
}
}
The above code is run within the client browser as a Silverlight object, but all that is happening is that it makes a hook call to the JavaScript method SendMessage
when the user clicks the 'Update Fan' button, passing the value as an object. The UpdateText
method is scriptable, meaning that it can be called from the JavaScript code - this is were the JSON string is passed to and deserialised into the shared class Thermotemps
and added to the observable collection, which is bound to the DataGrid
.
The DataGrid
performs a converter on each cell to give the text its colour (passing the cell value as a parameter).
Server Side Session Management
public class Global : System.Web.HttpApplication
{
private List<WebSocketSession> m_Sessions = new List<WebSocketSession>();
private List<WebSocketSession> m_SecureSessions = new List<WebSocketSession>();
private object m_SessionSyncRoot = new object();
private object m_SecureSessionSyncRoot = new object();
private Timer m_SecureSocketPushTimer;
private CommunicationControllerService.WebSocketServiceClient commService;
void Application_Start(object sender, EventArgs e)
{
LogUtil.Setup();
StartSuperWebSocketByConfig();
var ts = new TimeSpan(0, 0, 5);
m_SecureSocketPushTimer = new Timer(OnSecureSocketPushTimerCallback,
new object(), ts, ts); commService = new CommunicationControllerService.WebSocketServiceClient();
}
void OnSecureSocketPushTimerCallback(object state)
{
lock (m_SessionSyncRoot)
{
ThermoTemps temp = commService.GetTemperatures(null);
System.Web.Script.Serialization.JavaScriptSerializer oSerializer =
new System.Web.Script.Serialization.JavaScriptSerializer();
string sJSON = oSerializer.Serialize(temp);
SendToAll("Computer Update: " + sJSON);
}
}
void StartSuperWebSocketByConfig()
{
var serverConfig =
ConfigurationManager.GetSection("socketServer") as SocketServiceConfig;
if (!SocketServerManager.Initialize(serverConfig))
return;
var socketServer =
SocketServerManager.GetServerByName("SuperWebSocket") as WebSocketServer;
Application["WebSocketPort"] = socketServer.Config.Port;
socketServer.CommandHandler += new CommandHandler<WebSocketSession,
WebSocketCommandInfo>(socketServer_CommandHandler);
socketServer.NewSessionConnected +=
new SessionEventHandler<WebSocketSession>(socketServer_NewSessionConnected);
socketServer.SessionClosed +=
new SessionClosedEventHandler<WebSocketSession>(socketServer_SessionClosed);
if (!SocketServerManager.Start()) SocketServerManager.Stop();
}
void socketServer_NewSessionConnected(WebSocketSession session)
{
lock (m_SessionSyncRoot)
m_Sessions.Add(session);
}
void socketServer_SessionClosed(WebSocketSession session, CloseReason reason)
{
lock (m_SessionSyncRoot)
m_Sessions.Remove(session);
if (reason == CloseReason.ServerShutdown)
return;
}
void socketServer_CommandHandler(WebSocketSession session,
WebSocketCommandInfo commandInfo)
{
int? value = (int.Parse(commandInfo.Data.ToString()));
ThermoTemps temp = commService.GetTemperatures(value);
System.Web.Script.Serialization.JavaScriptSerializer oSerializer =
new System.Web.Script.Serialization.JavaScriptSerializer();
string sJSON = oSerializer.Serialize(temp);
SendToAll(session.Cookies["name"] + ": " + sJSON);
}
void SendToAll(string message)
{
lock (m_SessionSyncRoot)
{
foreach (var s in m_Sessions) s.SendResponseAsync(message);
}
}
void Application_End(object sender, EventArgs e)
{
m_SecureSocketPushTimer.Change(Timeout.Infinite, Timeout.Infinite);
m_SecureSocketPushTimer.Dispose();
SocketServerManager.Stop();
}
}
In Global.axa, where each new session comes in, you will create new handlers for each event that the WebSocket
is interested in (CommandHandler
, NewSessionConnected
, SessionClosed
). To improve on this, you would move the code into a separate class rather than execute it all in Global.axa. Here, I make calls to the WCF service to perform some processing and the return is pushed to the open (session) connections that is in our collection - the method socketServer_CommandHandler
initiates most of the pushing for us.
JavaScript (On Hosting Page)
The following JavaScript code is executed when the main page is loaded, thus creating the glue between the client (Silverlight) and the server-side websockets. The onMessage
method would be the main method here, as it will push the data it receives from the server into the Silverlight C# method and update the GUI.
<script type="text/javascript">
var noSupportMessage = "Your browser cannot support WebSocket!";
var ws;
function connectSocketServer() {
if (!("WebSocket" in window)) {
alert(noSupportMessage);
return;
}
ws = new WebSocket('ws://<%= Request.Url.Host %>:' +
'<%= WebSocketPort %>/Sample');
ws.onmessage = function (evt) {
var control = document.getElementById("silverlightControl");
control.Content.myObject.UpdateText(evt.data);
};
ws.onopen = function () {
var control = document.getElementById("silverlightControl");
control.Content.myObject.UpdateText('Connection open');
};
ws.onclose = function () {
var control = document.getElementById("silverlightControl");
control.Content.myObject.UpdateText('Connection closed');
}
}
function sendMessage(message) {
if (ws) ws.send(message);
else alert(noSupportMessage);
}
window.onload = function () {
connectSocketServer();
}
</script>
Shared Class
public class ThermoTemps
{
public int IceMaker { get; set; }
public int Fridge { get; set; }
public int Freezer { get; set; }
public int Fan { get; set; }
public int CoolingVent { get; set; }
}
The above class is used by the service and the client - serializing JSON string to a class object that is added to a collection, which is bound to a grid.
Service Code
The service just performs some action\process to manipulate the data that is then returned to the server and onto the client. Here, we would generally listen to database changes (SQLNotification
or have a data access layer inform us of a change - that we would then forward to the respective clients).
public class WebSocketService : IWebSocketService
{
public string ReverseCommunication(string communication)
{
return communication.Aggregate("", (acc, c) => c + acc);
}
public ThermoTemps GetTemperatures(int? fan = null)
{
Random rnd = new Random();
ThermoTemps temperatures = new ThermoTemps();
if (fan != null) temperatures.Fan = (int)fan;
else temperatures.Fan = rnd.Next(-20, 20);
temperatures.CoolingVent = rnd.Next(-20, 20);
temperatures.Freezer = rnd.Next(-20, 20);
temperatures.Fridge = rnd.Next(-20, 20);
temperatures.IceMaker = rnd.Next(-20, 20);
return temperatures;
}
}
Performance Testing
The end caveat for using WebSockets is multiple open connections. This is something that has been available in the Java world for a number of years now (non-blocking IO.Jar or using DWR, for example). But, in the .NET world, this is something that has lacked behind other languages, until now. To test that you can handle 1000+ open connections, download JMeter and run a script that will open multiple instances of the main page (alter your code so that it has a cookie already saved and bypass the log-in page). You will see that it can handle well over 1000 connections.
Improvements
The improvements come more so from what we can do with WebSockets. In the attached project, we are sending all connected sessions the same data. Ideally, we would like to differentiate between sessions. This is possible by having a collection of classes and one of the properties is the web session object, but having other properties in the class to allow us to determine what this session should be sent etc.
Future Work
When Microsoft does release a scale-able version of their Web Sockets, you can easily alter the above project - as stated within the W3C consortium for HTML5, there are methods that have to be implemented (server-side) by any flavor of WebSockets - in that OnMessage
, OnError
etc. The above project has already implemented these methods. All that needs to be changed are the DLLs that your will point to.