HTML5 Zone is brought to you in partnership with:

Jos works as Architect for JPoint. In the last couple of years Jos has worked on large projects in the public and private sector. Ranging from very technology focusses integration projects to SOA/BPM projects using WS-* and REST based architectures. Jos has given many presentations on conferences such as Javaone, NL-JUG, Devoxx etc., and has written two books for Manning: Open Source ESBs in Action and (published in the next couple of months) SOA Governance in Action. In this last book Jos shows how, with some good practical governance approaches, you can create great WS-* and REST based services and APIs. Besides this he has his own blog where he writes about interesting technologies and shares his ideas about REST, API Design, Scala, Play and more. Jos is a DZone MVB and is not an employee of DZone and has posted 51 posts at DZone. You can read more from them at their website. View Full User Profile

HTML5: Server-sent events with Angular.js, Node.js and Express.js

12.18.2012
| 7762 views |
  • submit to reddit

I was playing around with Node.js and Express.js and happened to run across an article on server-sent events. Server-sent events is a W3C specification that describes how a server can push events to browser clients. All this using the standard HTTP protocol. A couple of years ago this was a very promising specification, but then websockets came along and interest for this specification diminished a bit. It is however a very nice, easy to use, light weight way of pushing updates from a server to a number of connected clients. The code, especially on the client side, is very trivial and you can be up and running in a couple of minutes.
So I decided to dive a bit deeper into this specification and I'll show you in this article how to use server-sent events in Angular.js. I'll create a simple server in Node.js and Express.js that'll send the events to our Angular.js frontend.

Basically what we're going to do in this article are the following two things:

  • Create a minimal Angular.js application that shows system stats
  • Build a node.js and Express.js backend that pushes these stats to connected browsers

When completed it will look something like this:

statsangular.png

Building the Angular.js application

I won't go into too much detail on how Angular.js works. If you read this you probably know the basics about Angular.js so I'll just skip to the server-sent events part. If we want to listen to server-sent events (sse) we only have to include a small piece of javascript:

        var source = new EventSource('/stats');
        source.addEventListener('message', handleCallback, false);

This will create a listener that automatically tries to connect to /stats, using a standard HTTP request. If it fails or loses its connection it will immediately try to reconnect without you having to do anything. When the server sends an event, the handleCallback function will be called with the received data. Unlike websockets we can only receive text data using sse. This, however, shouldn't be too big of an issue, since a lot of data is sent as JSON strings anyways.

So how to we use this, lets start by looking at the angular stuff first:

    // define the module we're working with
    var app = angular.module('sse', []);
 
    // define the ctrl
    function statCtrl($scope) {
 
        // the last received msg
        $scope.msg = {};
 
        // handles the callback from the received event
        var handleCallback = function (msg) {
            $scope.$apply(function () {
                $scope.msg = JSON.parse(msg.data)
            });
        }
 
        var source = new EventSource('/stats');
        source.addEventListener('message', handleCallback, false);
    }

Edit: changed $scope.$apply as suggested by Jim Hoskins (see first comment)
Note: This is a very trivial example. Normally it's good practice to wrap up this specific event functionality into a service which you inject in the controller, so the controller only contains application logic.
As you can see we've created a single controller, that, when initiated, registers the listener and adds a callback. In this callback all we do is assign the received data to a scoped variable. This, however, isn't enough to trigger the updates in our model. Since the events are received outside the Angular.js lifecycle, we need to tell Angular.js that the "msg" value has changed. This is done by using $scope.$apply. With this piece of code, every time we receive an event sent by the server, our model is updated, and any view expressions are updated.

For completeness, lets quickly look at the table definition:

div class="container main" ng-controller="statCtrl">
    <div class="row">
        <div class="span10 offset1" style="text-align: center">
            <h2>System details updated using server-sent events</h2>
        </div>
    </div>
    <div class="row">
        <div class="span8 offset2">
            <table class="table table-striped">
                <thead>
                <tr>
                    <th>Property</th>
                    <th>Value</th>
                </tr>
                </thead>
                <tbody>
                <tr>
                    <td>Hostname:</td>
                    <td>{{msg.hostname}}</td>
                </tr>
                <tr>
                    <td>Type:</td>
                    <td>{{msg.type}}</td>
                </tr>
                <tr>
                    <td>Platform:</td>
                    <td>{{msg.platform}}</td>
                </tr>
                <tr>
                    <td>Arch:</td>
                    <td>{{msg.arch}}</td>
                </tr>
                <tr>
                    <td>Release:</td>
                    <td>{{msg.arch}}</td>
                </tr>
                <tr>
                    <td>Uptime:</td>
                    <td>{{msg.arch}}</td>
                </tr>
                <tr>
                    <td>Load avg.:</td>
                    <td>{{msg.loadaverage}}</td>
                </tr>
                <tr>
                    <td>Total mem:</td>
                    <td>{{msg.totalmem}}</td>
                </tr>
                <tr>
                    <td>Free mem:</td>
                    <td>{{msg.freemem}}</td>
                </tr>
                </tbody>
            </table>
        </div>
    </div>
</div>

Nothing special. Just a simple table (using bootstrap for the layout) with angular expressions.

Now that we've seen the front end, we need to have a server that can send events. For this example I've used Node.js and Express.js since I'm experimenting with these two technologies. But, as you'll see, implementing this in any other HTTP server will be trivial.

Building the server side with node.js and express.js

In the sse specification it is explained how a server should behave if it wants to support sse. It needs to keep the HTTP connection open and respond with a specific answer, so that the browser knows it can expect events to arrive over this open connection. I'll list the complete 'app.js' used in this example and highlight the interesting parts:

// most basic dependencies
var express = require('express')
  , http = require('http')
  , os = require('os')
  , path = require('path');
 
// create the app
var app = express();
 
// configure everything, just basic setup
app.configure(function(){
  app.set('port', process.env.PORT || 3000);
  app.set('views', __dirname + '/views');
  app.set('view engine', 'jade');
  app.use(express.favicon());
  app.use(express.logger('dev'));
  app.use(express.bodyParser());
  app.use(express.methodOverride());
  app.use(app.router);
  app.use(express.static(path.join(__dirname, 'public')));
});
 
// simple standard errorhandler
app.configure('development', function(){
  app.use(express.errorHandler());
});
 
//---------------------------------------
// mini app
//---------------------------------------
var openConnections = [];
 
// simple route to register the clients
app.get('/stats', function(req, res) {
 
    // set timeout as high as possible
    req.socket.setTimeout(Infinity);
 
    // send headers for event-stream connection
    // see spec for more information
    res.writeHead(200, {
        'Content-Type': 'text/event-stream',
        'Cache-Control': 'no-cache',
        'Connection': 'keep-alive'
    });
    res.write('\n');
 
    // push this res object to our global variable
    openConnections.push(res);
 
    // When the request is closed, e.g. the browser window
    // is closed. We search through the open connections
    // array and remove this connection.
    req.on("close", function() {
        var toRemove;
        for (var j =0 ; j < openConnections.length ; j++) {
            if (openConnections[j] == res) {
                toRemove =j;
                break;
            }
        }
        openConnections.splice(j,1);
        console.log(openConnections.length);
    });
});
 
setInterval(function() {
    // we walk through each connection
    openConnections.forEach(function(resp) {
        var d = new Date();
        resp.write('id: ' + d.getMilliseconds() + '\n');
        resp.write('data:' + createMsg() +   '\n\n'); // Note the extra newline
    });
 
}, 1000);
 
function createMsg() {
    msg = {};
 
    msg.hostname = os.hostname();
    msg.type = os.type();
    msg.platform = os.platform();
    msg.arch = os.arch();
    msg.release = os.release();
    msg.uptime = os.uptime();
    msg.loadaverage = os.loadavg();
    msg.totalmem = os.totalmem();
    msg.freemem = os.freemem();
 
    return JSON.stringify(msg);
}
 
// startup everything
http.createServer(app).listen(app.get('port'), function(){
  console.log("Express server listening on port " + app.get('port'));
})

As you can probably see and read from the comments nothing to special happens. We've created a "/stats" route that respons to GET requests. This is the endpoint our Angular.js application calls when it setups it's listener. You can see in this function that we respond with a specific HTTP header. This informs the browser that this server will be using sse to send events.

    res.writeHead(200, {
        'Content-Type': 'text/event-stream',
        'Cache-Control': 'no-cache',
        'Connection': 'keep-alive'
    });
    res.write('\n');

You can also see that we keep an array of open connections. We use this to push these updates to all open connections and to remove connections when they are closed (done very primitively in this example):

    // push this res object to our global variable
    openConnections.push(res);
 
    // When the request is closed, e.g. the browser window
    // is closed. We search through the open connections
    // array and remove this connection.
    req.on("close", function() {
        var toRemove;
        for (var j =0 ; j < openConnections.length ; j++) {
            if (openConnections[j] == res) {
                toRemove =j;
                break;
            }
        }
        openConnections.splice(j,1);
        console.log(openConnections.length);
    });

Updates are sent as a JSON string to the connected browsers every second using the setInterval function:

setInterval(function() {
    // we walk through each connection
    openConnections.forEach(function(resp) {
        var d = new Date();
        resp.write('id: ' + d.getMilliseconds() + '\n');
        resp.write('data:' + createMsg() +   '\n\n'); // Note the extra newline
    });
 
}, 1000);
 
function createMsg() {
    msg = {};
 
    msg.hostname = os.hostname();
    msg.type = os.type();
    msg.platform = os.platform();
    msg.arch = os.arch();
    msg.release = os.release();
    msg.uptime = os.uptime();
    msg.loadaverage = os.loadavg();
    msg.totalmem = os.totalmem();
    msg.freemem = os.freemem();
 
    return JSON.stringify(msg);
}

And that's it. With these couple of lines of code we've setup a "server-sent event" server and created a client application that automatically updates its view using Angular.js.




Published at DZone with permission of Jos Dirksen, author and DZone MVB. (source)

(Note: Opinions expressed in this article and its replies are the opinions of their respective authors and not those of DZone, Inc.)