Search This Blog

Saturday, May 3, 2014

HTML5: Real-Time Push Notifications from .NET Application

Simple example showing publish-subscribe scenario between HTML5 JavaScript and .NET without using polling or long-polling mechanisms.


The source code for this example can be downloaded from here.


Introduction

The example bellow demonstrates a publish-subscribe communication scenario between JavaScript and a .NET application. The HTML5 JavaScript client needs to subscribe for an event (or more events) in the .NET application. When the event occurs the .NET application notifies subscribed clients.

The communication is realized via WebSockets (full-duplex single socket connection) and therefore it does not use a polling or long-polling mechanism which would be used in case of the HTTP protocol.

Notification messages are pushed from the service to all subscribed clients instantly when the event occurs.

Example Application

The example application is based on Eneter.Messaging.Framework the lightweight cross-platform framework for the interprocess communication which is very easy to use.
The example also uses the Chart.js library. A nice free library for drawing charts.

To Run Example


  • Download and unzip this example.
  • Download Eneter for .NET from http://www.eneter.net/ProductDownload.htm.
  • Download Eneter for Javascript from http://www.eneter.net/ProductDownload.htm.
  • Copy Eneter for Javascript into CpuUsageClient directory.
  • Open index.html file in an editor and ensure it uses the same version of eneter-messaging as you downloaded.
  • Open the example project in Visual Studio and add the reference to Eneter.Messaging.Framework.dll that you downloaded.
  • Build the application and execute it.
  • Open index.html (from CpuUsageClient directory) in an internet browser.
  • Press 'Open Connection' button and then 'Subscribe' button.
  • Web-page starts to get notifications and the chart is displayed.

In order to demonstrate the publish-subscribe scenario between JavaScript and .NET the example bellow implements a simple .NET console application and a simple HTML5 web-page.
The console application regularly checks the CPU usage and notifies its value.
The HTML5 web-page subscribes to be notified about CPU usage. When it receives the notification it updates the value in the chart.



Using Duplex Broker

The core idea of the example is using the duplex broker component from Eneter Messaging Framework. It provides functionality for sending notification events (publishing) as well as for subscribing to desired events.
When then the duplex broker receives a notification message it forwards it all subscribers which are interested in this type of notification.
The cross-platform aspect of the Eneter framework ensures messages between JavaScript and .NET are understood (e.g. it takes care about UTF16 vs UTF8 or little-endian vs big-endian).


.NET Service Application

The service application is a simple .NET console application that regularly checks the CPU usage. Then it uses the broker component to publish the value. The broker searches which clients are subscribed for this type of events and forwards them the message.
Also because JavaScript uses the JSON serializer the service uses the JSON serializer too. It also sets the duplex broker to use the JSON serializer.
The whole implementation is very simple:
using System;
using System.Diagnostics;
using System.Threading;
using Eneter.Messaging.DataProcessing.Serializing;
using Eneter.Messaging.MessagingSystems.MessagingSystemBase;
using Eneter.Messaging.MessagingSystems.WebSocketMessagingSystem;
using Eneter.Messaging.Nodes.Broker;

namespace CpuUsageService
{
    // Message that will be notified.
    public class CpuUpdateMessage
    {
        public float Usage { get; set; }
    }

    class Program
    {
        static void Main(string[] args)
        {
            // JavaScript uses JSON serializer so set using JSON.
            ISerializer aSerializer = new DataContractJsonStringSerializer();

            // Create broker.
            IDuplexBrokerFactory aBrokerFactory = new DuplexBrokerFactory();
            IDuplexBroker aBroker = aBrokerFactory.CreateBroker();

            // Communicate using WebSockets.
            IMessagingSystemFactory aMessaging = new WebSocketMessagingSystemFactory();
            IDuplexInputChannel anInputChannel =
                aMessaging.CreateDuplexInputChannel("ws://127.0.0.1:8843/CpuUsage/");

            anInputChannel.ResponseReceiverConnected += (x, y) =>
            {
                Console.WriteLine("Connected client: " + y.ResponseReceiverId);
            };
            anInputChannel.ResponseReceiverDisconnected += (x, y) =>
            {
                Console.WriteLine("Disconnected client: " + y.ResponseReceiverId);
            };

            // Attach input channel and start listeing.
            aBroker.AttachDuplexInputChannel(anInputChannel);

            // Start working thread monitoring the CPU usage.
            bool aStopWorkingThreadFlag = false;
            Thread aWorkingThread = new Thread(() =>
                {
                    PerformanceCounter aCpuCounter =
                       new PerformanceCounter("Processor", "% Processor Time", "_Total");

                    while (!aStopWorkingThreadFlag)
                    {
                        CpuUpdateMessage aMessage = new CpuUpdateMessage();
                        aMessage.Usage = aCpuCounter.NextValue();

                        //Console.WriteLine(aMessage.Usage);

                        // Serialize the message.
                        object aSerializedMessage = aSerializer.Serialize<CpuUpdateMessage>(aMessage);

                        // Notify subscribers via the broker.
                        // Note: The broker will forward the message to subscribed clients.
                        aBroker.SendMessage("MyCpuUpdate", aSerializedMessage);

                        Thread.Sleep(500);
                    }
                });
            aWorkingThread.Start();

            Console.WriteLine("CpuUsageService is running press ENTER to stop.");
            Console.ReadLine();

            // Wait until the working thread stops.
            aStopWorkingThreadFlag = true;
            aWorkingThread.Join(3000);

            // Detach the input channel and stop listening.
            aBroker.DetachDuplexInputChannel();
        }
    }
}


JavaScript Client

The JavaScript client is a simple HTML 5 web-page using the duplex broker client component for subscribing to CPU usage notifications. When the notification message is received it updates the chart displaying values. The chart is moving as new notification values are received.
<!DOCTYPE html>
<html>
    <head>
        <title>CPU Usage Client</title>
        
        <script src="Chart.js"></script>
        <!- ENSURE HERE IS THE SAME ENETER VERSION AS YOU DOWNLOADED -->
        <script src="eneter-messaging-6.0.1.js"></script>

        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
    </head>
    <body onunload="closeConnection();">
        <div>
            <button type="button" onclick="openConnection();">1. Open Connection</button>
        </div>
        <div>
            <button type="button" onclick="subscribe();">2. Subscribe</button>
            <button type="button" onclick="unsubscribe();">3. Unsubscribe</button>
        </div>
        <div>
            <button type="button" onclick="closeConnection();">4. Close Connection</button>
        </div>
        <div id="output">
        </div>
        <div>
            <canvas id="canvas" height="300" width="300"></canvas>
        </div>
        
        <script>
            // Initialize chart.
            var myChartConfig = {
                        animation : false,
                        scaleOverlay : true,
                        scaleOverride : true,
                 scaleSteps : 10,
                        scaleStepWidth : 10,
                        scaleStartValue : 0
            };
            var myLineChartData = {
                labels : ["", "", "", "", "", "", "", "", "", ""],
                datasets : [
                   {
                     fillColor : "rgba(220,220,220,0.5)",
                     strokeColor : "rgba(220,220,220,1)",
                     pointColor : "rgba(220,220,220,1)",
                     pointStrokeColor : "#fff",
                     data : [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
                   }
                ]
            };
            var myChart = new Chart(document.getElementById("canvas").getContext("2d"));
            
            // Create the duplex output channel.
            var myOutputChannel =
                new WebSocketDuplexOutputChannel("ws://127.0.0.1:8843/CpuUsage/", null);

            // Create BrokerClient
            var myBrokerClient = new DuplexBrokerClient();
            
            // Subscribe to notifications.
            myBrokerClient.onBrokerMessageReceived = onBrokerMessageReceived;
            
            function openConnection() {
                // Attach output channel and be able to send messages and receive responses.
                myBrokerClient.attachDuplexOutputChannel(myOutputChannel);
            };

            function closeConnection() {
                // Detach output channel and stop listening to responses.
                myBrokerClient.detachDuplexOutputChannel();
            };

            function subscribe() {
                myBrokerClient.subscribe("MyCpuUpdate");
            };

            function unsubscribe() {
                myBrokerClient.unsubscribe("MyCpuUpdate");
            };
            
            function onBrokerMessageReceived(brokerMessageReceivedEventArgs) {
                // If the notified message is the CPU status update.
                if (brokerMessageReceivedEventArgs.MessageTypeId === "MyCpuUpdate")
                {
                    // Deserialize notified message content.
                    var aValue = JSON.parse(brokerMessageReceivedEventArgs.Message);
                    
                    // Update data and draw the chart.
                    myLineChartData.datasets[0].data.shift();
                    myLineChartData.datasets[0].data.push(aValue.Usage);
                    myChart.Line(myLineChartData, myChartConfig);
                }
            };
        </script>
    </body>
</html>

7 comments:

  1. I found this to be a nice and clear example. I attempted to implement it as a google Chrome app. Sadly, Graph.js is not compatible with a chrome app's Content Security Policy, but I was able to get the network communication to work.

    https://github.com/DATMedia/push-from-dotnet-to-html5

    ReplyDelete
  2. Hi i've tried this exmaple works on firefox, google chrome but fails to work on ie11 on win7. Any suggestions?

    ReplyDelete
    Replies
    1. I think the problem could be IE11 does not allow the loopback connection via WebSockets - there are several comments on internet about that issue.
      It means IE11 should be able to connect a remote service but not local host.
      Maybe you could try to use an IP address which is assigned to your machine within the local network.
      E.g. if your machine has the IP address 192.168.1.10 then instead of 127.0.0.1 make your service listen to 192.168.1.10 and then your web-client can try to open connection to 192.168.1.10 too.

      Delete
    2. Thanks ondrej, I'm actually using my proper ip address of var myOutputChannel = new WebSocketDuplexOutputChannel("ws://156.8.227.194:8843/CpuUsage/", null);.
      I debugged the eneter-messaging-7.0.0.js, and the function where it gets an exception is:
      this.readBytes = function(idx, size)
      {
      var anArrayBuffer = myDataView.buffer;

      // Get bytes in ArrayBuffer.
      var aResult = anArrayBuffer.slice(idx, idx + size);
      return aResult;
      }
      line 2481 and the exception is "Object doesn't support property or method 'slice'"

      Delete
  3. Thanks ondrej, I'm actually using my proper ip address of var myOutputChannel = new WebSocketDuplexOutputChannel("ws://156.8.227.194:8843/CpuUsage/", null);

    ReplyDelete
  4. can i run this example between mobile application build with java script and application on pc build with vb.net..

    ReplyDelete