The Node.js website describes it as having “an event-driven, non-blocking I/O model that makes it lightweight and efficient”. Sounds lovely, but what’s it actually for?
Modulus’s excellent blog post – An Absolute Beginner’s Guide to Node.js provides some rather tasty examples. After covering the trivial examples (Hello world! and simple file I/O), it gets to the meat of what we’re about – an HTTP server. The simple example demonstrates a trivial HTTP server in Node.js in 5 lines of code. Not 5 lines of code compiled to an executable or deployed into an existing web server. 5 lines of code that can be run from a simple command. It then goes on to describe the frameworks and libraries that let you do really useful stuff.
This looks just the thing for implementing a new feature in the Spanners demo app: push notifications to all logged-in users when a spanner is changed.
The user story
The Spanners demo app is an application that manages an inventory of spanners. Every user can view a list of all spanners in the inventory (including the spanner’s name, size and owner) and can add new spanners or update or delete a spanner that they own. Mr Smith logs into the Spanners demo app and views a list of all spanners. Mr Jones also logs in and updates one of his spanners. Immediately after Mr Jones saves his change, Mr Smith is informed with a notification message on screen.
The technologies
Standard HTTP us a ‘pull’ technology. When a user clicks a link to a page in their browser, they make a request to the server and pull the response. In this example though, we’re looking for a ‘push’ notification. A number of technologies exist to allow a server to push notifications to the client browser without having the user request a page. I’ve chosen WebSocket. A WebSocket connection can be opened by the client browser when a page is opened. In this case the WebSocket connection is to a Node.js server. Node.js maintains a list of all active WebSocket connections. When our Node.js server is informed of an updated spanner, it broadcasts the message to all open WebSocket connections. This allows every active browser session to be informed of updates.
The solution – browser side
Lets start with the browser side. Making a WebSocket connection and listening for update requires only some very straightforward Javascript on the page:
<script type="text/javascript"> var myWebSocket = new WebSocket("ws://localhost:9090"); myWebSocket.onmessage = function(evt) { Msg.info("A spanner has been updated. Please refresh the page to see changes."); }; </script>
In this trivial example, we just display a message every time any message is sent through the socket. We could easily extend to read the (JSON) message received and display a more detailed message. This example assumes that our WebSocket server is running on localhost port 9090 (ws is the protocol).
The solution – Node.js
We require a server that listens for REST notifications from the Spanners server side application and then broadcasts the notification to all open WebSocket connections. In Node.js, this is remarkably simple:
var WebSocketServer = require('ws').Server , wss = new WebSocketServer({ port: 9090 }); var express = require('express'); var app = express(); // Start REST server on port 8081 var server = app.listen(8081, function () { var host = server.address().address var port = server.address().port console.log("Websocket event broadcaster REST API listening on http://%s:%s", host, port) }); // Broadcast updates to all WebSocketServer clients app.post('/notify/spanner/:spanner_id/update', function (req, res) { var spanner_id = req.params.spanner_id; console.log('Event: spanner %s updated', spanner_id); wss.clients.forEach(function each(client) { client.send("broadcast: spanner " + spanner_id + " updated"); }); res.sendStatus(200); });
Everything up to line 11 is initialization. First, we define the two npm libraries we’ll use: ws for WebSockets and express for the REST service. Then we define the WebSocket server port as 9090 and the REST server port as 8081. We log a startup message on line 10 for good measure.
Line 13 onwards is where the action is. When our REST service receives a POST request to endpoint /notify/spanner/<spannerid>/update, it will send a message through the WebSocket connections for every currently connected client. The list of clients is maintained for us by the WebSocketServer library. We could send a full JSON message, but in this simple case, just a plain text string message is sent.
That’s it. That’s really it. The 21 lines here are a fully functioning HTTP (REST) and WebSocket server. Assuming that this is saved in a file called server.js, this can be run as
node server.js
The solution – Java application
All that’s left to do now is to have our Java application call the REST service to trigger the broadcasts. I quite like the REST support in Spring-web, so my solution looks like this:
<bean id="notifyUserEventListener" class="org.dontpanic.spanners.events.NotifyUserEventListener" p:notificationServiceUrl="http://localhost:8081/notify/spanner/42/update" p:restTemplate-ref="restTemplate"/> <bean id="restTemplate" class="org.springframework.web.client.RestTemplate"/>
public class NotifyUserEventListener implements ApplicationListener<SpannerEvent> { @Override public void onApplicationEvent(SpannerEvent spannerEvent) { restTemplate.postForObject(notificationServiceUrl, Integer.toString(spannerEvent.getSpannerId()), String.class); } }
Putting it all together
The Spanners demo app now consists of
- A WAR webapp, running in Tomcat
- A MySQL database
- A (optional) Node.js server
Running all this directly on localhost is certainly possible but the steps to install and configure pre-requisites are now quite complicated. I prefer to run this using Docker. The latest Docker Compose file creates the necessary containers and runs the current version of the application.
If you want to run this application, install docker-compose, download the the current docker-compose.yml file and run
docker-compose up -d
then browse to http://localhost:8080/spanners-mvc/. If you want to view the complete source of this example, it’s on GitHub.
The example opens 2 ports.
In most environment it is not possible and not needed.
There is a possibility to use the some port (see ws documentation: https://github.com/websockets/ws)
var server = require(‘http’).createServer()
, url = require(‘url’)
, WebSocketServer = require(‘ws’).Server
, wss = new WebSocketServer({ server: server })