Vert.x 3 real time web apps

One of the interesting features of Vert.x is the SockJS event bus bridge. This piece of software allows external applications to communicate with Vert.x event bus using Websockets and if your browser does not support it then it gracefully degrades to pooling AJAX calls.

WebSockets bring a new level of interaction to the web, they really bring real time to web applications due to the fact that its communication model is bi-directional in contrast to the traditional HTTP model where a client can initiate a data request to a server but not the other way around.

In this small post I will demonstrate how you can create a simple collaborative drawing app. The idea is simple, all users that open the app will be be presented with a empty canvas and what they draw or is drawn on other canvas is shared in real time on their screen.

For the sake of simplicity and making this post light there is no security involved so, everyone is free to listen to what is being drawn, however the external application has limited read write access to a single address on Vert.x event bus, ensuring that other services running on the cluster will not be exposed.

This is what you should expect to see:

Screencast

Bootstrap a project

If you followed the previous series on Vert.x development, you saw that Java and Maven were the main topic, since Vert.x is polyglot I will focus on JavaScript and NPM as my programming language and package management tool.

With NPM start by creating a package.json, in order to do this we should run:

npm init

This will present a selection of questions and in the end you should have a basic package.json file. This configuration is very basic so you need to add a dependency to Vert.x so you can run the application. You can add it to the dependencies property and it should look more or less like this:

{
  "name": "draw",
  "private": true,
  "dependencies": {
    "vertx3-full": "3.0.0-1"
  },
  "scripts": {
    "start": "vertx run server.js"
  },
  "version": "1.0.0",
  "main": "server.js",
  "devDependencies": {},
  "author": "",
  "license": "ISC",
  "description": "A Real Time Drawing App"
}

If you do not know why there is the dependency on vertx3-full or why the added scripts property please check the older blog post about it.

Project Structure

This post has no preference over project structure, so if you do not agree with the structure used here feel free to use what you feel best. For this example I will keep it to:

├── package.json
├── server.js
└── webroot
  ├── assets
  │   └── js
  │     ├── script.js
  │     └── vertxbus.js
  └── index.html
 
3 directories, 5 files

As you can imagine server.js will be our Vert.x application and everything under webroot will be the client application.

The client application is not really Vert.x specific and could in theory be used by any other framework so I will go lightly over its code.

Client Application

Our application main entry point is as one can expect index.html. In the index file define the following HTML:

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8"/>
  <title>Real time drawing App</title>
  <!--[if lt IE 9]>
  <script src="http://html5shiv.googlecode.com/svn/trunk/html5.js"></script>
  <![endif]-->
</head>
 
<body>
<canvas id="paper" width="1900" height="1000">
  Your browser needs to support canvas for this to work!
</canvas>
 
<!-- JavaScript includes. -->
<script src="http://code.jquery.com/jquery-1.8.0.min.js"></script>
<script src="//cdn.jsdelivr.net/sockjs/0.3.4/sockjs.min.js"></script>
<script src='assets/js/vertxbus.js'></script>
<script src="assets/js/script.js"></script>
 
</body>
</html>

As I previously wrote, the idea is to keep it as simple as possible so it is all about having a canvas element and a application main script script.js. All the rest are files served by CDNs that provide common web application libraries such as jQuery, HTML5 shim for older browsers, SockJS client and vertxbus bridge.

The main code is on script.js file:

$(function () {
 
  // This demo depends on the canvas element
  if (!('getContext' in document.createElement('canvas'))) {
    alert('Sorry, it looks like your browser does not support canvas!');
    return false;
  }
 
  var doc = $(document),
    canvas = $('#paper'),
    ctx = canvas[0].getContext('2d');
 
  // Generate an unique ID
  var id = Math.round($.now() * Math.random());
 
  // A flag for drawing activity
  var drawing = false;
 
  var clients = {};
  // create a event bus bridge to the server that served this file
  var eb = new vertx.EventBus(
      window.location.protocol + '//' + window.location.hostname + ':' + window.location.port + '/eventbus');
 
  eb.onopen = function () {
    // listen to draw events
    eb.registerHandler('draw', function (data) {
      // Is the user drawing?
      if (data.drawing && clients[data.id]) {
 
        // Draw a line on the canvas. clients[data.id] holds
        // the previous position of this user's mouse pointer
 
        drawLine(clients[data.id].x, clients[data.id].y, data.x, data.y);
      }
 
      // Saving the current client state
      clients[data.id] = data;
      clients[data.id].updated = $.now();
    });
  };
 
  var prev = {};
 
  canvas.on('mousedown', function (e) {
    e.preventDefault();
    drawing = true;
    prev.x = e.pageX;
    prev.y = e.pageY;
  });
 
  doc.bind('mouseup mouseleave', function () {
    drawing = false;
  });
 
  var lastEmit = $.now();
 
  doc.on('mousemove', function (e) {
    if ($.now() - lastEmit > 30) {
      eb.publish('draw', {
        'x': e.pageX,
        'y': e.pageY,
        'drawing': drawing,
        'id': id
      });
      lastEmit = $.now();
    }
 
    // Draw a line for the current user's movement, as it is
    // not received in the eventbus
 
    if (drawing) {
 
      drawLine(prev.x, prev.y, e.pageX, e.pageY);
 
      prev.x = e.pageX;
      prev.y = e.pageY;
    }
  });
 
  // Remove inactive clients after 10 seconds of inactivity
  setInterval(function () {
 
    for (var ident in clients) {
      if (clients.hasOwnProperty(ident)) {
        if ($.now() - clients[ident].updated > 10000) {
          // Last update was more than 10 seconds ago.
          // This user has probably closed the page
          delete clients[ident];
        }
      }
    }
 
  }, 10000);
 
  function drawLine(fromx, fromy, tox, toy) {
    ctx.moveTo(fromx, fromy);
    ctx.lineTo(tox, toy);
    ctx.stroke();
  }
 
});

The most important part in this code is all the code related to eb. The variable eb is our bridge to the event bus, Start by creating a bridge using the vertx.EventBus object and define where to connect, using the details of the current window location.

Then add a onopen listener that will subscribe to the address draw on the event bus so it can listen to all messages regarding drawing and perform the drawing actions. Since listening is not enough I also add a mouse listener to the document so when it moves it publishes events to the draw address.

Note that I am using publish and not send, the reason should be obvious, I want everyone to know this users mouse movements, I am not interested on sending the events to just a single user. You can see now that if you want to have a drawing app in a one on one user basis then instead of publish() you should use send().

Server Application

The server code is quite straight forward, all you need is:

var Router = require("vertx-web-js/router");
var SockJSHandler = require("vertx-web-js/sock_js_handler");
var StaticHandler = require("vertx-web-js/static_handler");
 
var router = Router.router(vertx);
 
// Allow outbound traffic to the draw address
 
var options = {
  "outboundPermitteds" : [{"address" : "draw"}],
  "inboundPermitteds" :  [{"address" : "draw"}]
};
 
router.route("/eventbus/*").handler(SockJSHandler.create(vertx).bridge(options).handle);
 
// Serve the static resources
router.route().handler(StaticHandler.create().handle);
 
vertx.createHttpServer().requestHandler(router.accept).listen(8080);

We start with the usual imports, we import a reference to the Router object and a couple of helper handlers SockJSHandler and StaticHandler. As their names should tell you one handler will be responsible to handle all SockJS data and the other all HTTP file serving requests.

We then add then to a router and start a HTTP server that will handle all incoming request using the handler accept function. Finally we listen on port 8080 and we are ready.

Note that there is a options object where a couple of properties are defined outbound/inbound permitted addresses. Without this configuration the external application will not be allowed to connect to the vert.x bus, in fact the default configuration of the SockJSHandler is deny all. So you must specify explicitly which address are allowed to receive messages from SockJS and which ones are allowed to send/publish to SockJS.

Now you can start your application, don’t forget to install the dependencies for the first time:

npm install

And then run the application:

npm start

If you now open 2 browser windows you will be able to draw nice pictures and see the drawing showing in “real time” on the other window, if you then draw on the second you should get the mirror effect on the first window.

Have fun!

Posted on 31 August 2015
in guides
7 min read

Related posts