Introduction
Everything in the web becomes increasingly real-time and HTML5 finally offers some facilities to build efficient, simple and robust real-time applications on the web. This article serves as a demonstration of such facilities in building a gauge, updated in real time 5 times per second which you can use for server monitoring or building dashboards:
- It uses websockets internally, but abstracted by Spike-Engine, will fallback to flash sockets for older browsers.
- It updates the gauges using a publish-subscribe model with JSON formatted packets.
- It uses ChartJS library for rendering the gauges, the gauges are rendered as SVG.
- It is cross-platform and with a minimized packet payload and message compression.
- The application server is a self-hosted executable and the client is just a plain HTML file.
Background
While browsing around the internet, I stumbled upon a very nice HTML5 charting library by DevExpress, called ChartJs. They provide a nice declarative API for creating various good looking charts that can be potentially used for visual analytics and dashboards. However, none of their examples seemed to show how to update their charts in a real-time client-server way.
Hence, I decided to create a simple gauge that would monitor packets per second rate on my server, using Spike-Engine as a backend.
Making the Server
Let's look at our server implementation, our server needs to perform several things in order to update the gauges:
- Listen on a particular endpoint (IP + port) and create a publish-subscribe channel for posting our messages to the client.
- Compute packets per second, and publish the value to all subscribed clients 5 times per second (every 200 milliseconds).
The first point is rather easy with Spike-Engine, we need to simply call Service.Listen
method in order to start listening on a specific IPAddress
and a port (called endpoint), or on several endpoints for that matter. This line in the Main
method accomplishes it:
Service.Listen(new TcpBinding(IPAddress.Any, 8002));
Next, we create a new PubHub
instance, provided us by Spike-Engine. This implements publish-subscribe model. In publish-subscribe model senders of messages, called publishers, do not program the messages to be sent directly to specific receivers, called subscribers. Instead, published messages are characterized into classes, without knowledge of what, if any, subscribers there may be. Similarly, subscribers express interest in one or more classes, and only receive messages that are of interest, without knowledge of what, if any, publishers there are [Wiki].
We first create our PubHub
instance and give it a name. The name is important as we will need to provide the same name in our clients when we want to subscribe.
var hub = Spike.Service.Hubs.GetOrCreatePubHub("PacketWatch");
Then we schedule a function to be called every 200 milliseconds, this function will publish messages to the PubHub
.
hub.Schedule(TimeSpan.FromMilliseconds(200), OnTick);
That's it for the point 1. By now, we have a publish subscribe channel (PubHub
) and scheduled execution of a function named OnTick
.
Now, let's look at point 2 as we need to compute packets per second rate. We do this by implementing a simple rolling window periodic sampling and we sample every 200 milliseconds.
This can be done fairly easily by maintaining a Queue
of elements and sampling the delta value (difference) to it. We need a queue as the window needs to be shifted, this is accomplished using a Dequeue()
call when there's more than 5 elements in the queue. It allows us to have a precise rate count, with a 200 millisecond resolution. Here's the implementation of our OnTick
method, called 5 times per second.
private static void OnTick(IHub hub)
{
var pubHub = hub as PubHub;
var packetsIn = Monitoring.PacketsIncoming.Value;
var packetsOut = Monitoring.PacketsOutgoing.Value;
var packetsDelta = (packetsIn + packetsOut) - PacketCount;
PacketCount = packetsIn + packetsOut;
PacketSampler.Enqueue(packetsDelta);
if (PacketSampler.Count > 5)
PacketSampler.Dequeue();
pubHub.Publish(PacketSampler.Sum());
}
Making the Client
Now let's have a look at the actual HTML/JavaScript code that accomplishes the following:
- Renders the gauges using
ChartJS
library. - Subscribes to our server and updates the gauges when the server tells it to.
We first include various dependencies: Spike-Engine JavaScript SDK and ChartJS (JQuery, Knockout and Globalize are required by ChartJS).
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
<script src="http://ajax.aspnetcdn.com/ajax/knockout/knockout-2.2.1.js"></script>
<script src="http://ajax.aspnetcdn.com/ajax/globalize/0.1.1/globalize.min.js"></script>
<script src="http://cdn3.devexpress.com/jslib/13.1.6/js/dx.chartjs.js"></script>
<script src="js/spike-sdk.min.js"></script>
Once this is done, we can start putting up the layout of the page. Check out the full source of the HTML page to see the code. After that, we need to create the actual Gauges with ChartJS
library, this is fairly easy as you just need to define various ranges and visual features. We also set the animationDuration
to 200, this is needed so our gauge needle won't take too much time to interpolate between updates.
$("#packetGauge1").dxCircularGauge({
scale: {
startValue: 0,
endValue: 100,
majorTick: {
tickInterval: 25
},
label: {
indentFromTick: 8
}
},
margin: {
left: 10,
right: 10,
top: 10,
bottom: 10
},
rangeContainer: {
width: 4,
backgroundColor: 'none',
ranges: [
{
startValue: 0,
endValue: 24,
color: '#A6C567'
},
{
startValue: 26,
endValue: 49,
color: '#A6C567'
},
{
startValue: 51,
endValue: 74,
color: '#A6C567'
},
{
startValue: 76,
endValue: 100,
color: '#FCBB69'
}
]
},
animationDuration: 200,
animationEnabled: true,
needles: [{
offset: 5,
indentFromCenter: 7,
value: 0,
color: '#43474b'
}],
spindle: {
color: '#43474b'
},
});
After that, we need to actually perform the connection to our server, here we used the local server (127.0.0.1), one should use the public IP ideally. This code snippet does few things:
- It first creates a connection to the remote server.
- After that, when the client is connected to the server, it subscribes to
PacketWatch
hub as we named it. - Next, it hooks up
hubEventInform
event which is invoked by Spike-Engine every time a message arrives from our PubHub
. - Finally, it deserializes the JSON message and updates the gauges.
$(document).ready(function () {
var server = new ServerChannel("127.0.0.1:8002");
server.on('connect', function () {
server.hubSubscribe('PacketWatch', null);
});
server.on('hubEventInform', function (p) {
var value = JSON.parse(p.message);
var count = $('#packetCounter');
count.text(value);
var gauge1 = $('#packetGauge1').dxCircularGauge('instance');
gauge1.needleValue(0, value);
var gauge2 = $('#packetGauge2').dxCircularGauge('instance');
gauge2.needleValue(0, value);
});
});
That's it, I hope you like the article. I am looking forward to your remarks or suggestions on how to improve it!
Server Code
Below, for completeness is the full server implementation:
class Program
{
static void Main(string[] args)
{
Service.Listen(
new TcpBinding(IPAddress.Any, 8002)
);
}
[InvokeAt(InvokeAtType.Initialize)]
public static void Initialize()
{
var hub = Spike.Service.Hubs.GetOrCreatePubHub("PacketWatch");
hub.Schedule(TimeSpan.FromMilliseconds(200), OnTick);
}
private static long PacketCount = 0;
private static Queue<long> PacketSampler
= new Queue<long>();
private static void OnTick(IHub hub)
{
var pubHub = hub as PubHub;
var packetsIn = Monitoring.PacketsIncoming.Value;
var packetsOut = Monitoring.PacketsOutgoing.Value;
var packetsDelta = (packetsIn + packetsOut) - PacketCount;
PacketCount = packetsIn + packetsOut;
PacketSampler.Enqueue(packetsDelta);
if (PacketSampler.Count > 5)
PacketSampler.Dequeue();
pubHub.Publish(PacketSampler.Sum());
}
}
Client Code
The full client code can be viewed in the attached archive. Alternatively, I've provided a live demo for this article.
History
- 23/6/2015 - Source code & article updated to Spike v3
- 9/7/2013 - Initial article
- 9/8/2013 - Updated the code & article formatting