More than a decade ago, web pages were just statics. Some years ago, they were statics with some partial reloading. Now, users expect notifications, chats, events. Users expect you to provide realtime content.
Fortunately, there is a new standard for that. Having a streaming channel between the server and the client can be done using the WebSocket API.
Unfortunately, there is one major issue with realtime: scalability. Realtime means more connections, more data exchanges. Most web servers today are using threads to handle requests. Threads based networking doesn’t scale, as you will face performance and locking issues. Java web servers like Jetty, Tomcat, Glassfish etc. are using threads.
In fact, most web frameworks today will make you create web sites that are going to be deployed on thread based web servers. There is a fundamental problem here if you want to provide realtime stuff. You have to look for others technologies, and the big challenger today is NodeJS.
I wanted since many weeks to try NodeJS, especially in order to bring realtime through WebSockets to my applications. NodeJS’ goal is to provide scalables network programs. Basically it’s JavaScript plus an I/O layer. You write in Javascript, and your code is executed using V8, the Google’s open source JavaScript engine, the one used by Google Chrome. NodeJS tends to be the solution to scalability problems.
That’s fine, but does it means that you have to stop using your favorite web framework in order to do only NodeJS? Nop. You can employ NodeJS to just handle the realtime aspects of your applications, like broadcasting notifications. What you need then is just a bridge between NodeJS and your favorite web framework.
As I’m starting my own business, I do some Ruby now, and I started to play with NodeJS and Ruby on Rails. I made the bridge between the two platforms with Redis, using the publish/subscribe feature. Redis is a lightweight and very fast advanced key-value datastore. Benchmarks show performances about “110000 SETs/second, 81000 GETs/second in an entry level Linux box“. In fact, Redis has been designed for performance. Since Redis 2.0, there is now a publish/subscribe feature, especially designed to trap changes. Redis is supported by tons of languages, as NodeJS, Ruby and Java.
So let’s summarize what could be the global architecture. Your base application will publish events using Redis. NodeJS will subscribe to this Redis channel and as soon as an event is caught, NodeJS will relay it to subscribed end users through WebSocket API.
I wanted to experiment the same technique with Java instead of Ruby. I used the Tapestry 5 Hotel Booking application to achieve that. And here is how.
Overview
The goal is to provide a monitor page that will display in realtime every booking made by users.
Code provided is an experiment, please don’t use it as it in production. It should be used only for testing purpose.
The environment
Get Redis and NodeJS for your platform. They are both designed to run on POSIX platforms, which basically means *not Windows*, but they are some binaries for Windows avaible for both here and here, and they should integrate your cygwin quite well (just make them available in your PATH).
Checkout the Tapestry 5 Hotel Booking application.
Edit pom.xml to add the Jedis artifact which is a Java Redis client. There are some others distributions, but there are not all available as Maven artifacts. Some of them are respecting the JDBC standard, but JDBC was designed for relational databases, and Redis is not a relational one.
<dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> <version>1.3.0</version> <type>jar</type> <scope>compile</scope> </dependency>
Make Redis available in your application
We want to have a service in our application that will makes it possible to publish messages on channels. Let’s define the service respecting the Tapestry way of things.
RedisPublisher
package com.tap5.hotelbooking.services;
public interface RedisPublisher
{
void publish(String channel, String message);
}
RedisPublisherImpl
package com.tap5.hotelbooking.services;
import org.apache.tapestry5.ioc.ScopeConstants;
import org.apache.tapestry5.ioc.annotations.Scope;
import redis.clients.jedis.Jedis;
@Scope(value = ScopeConstants.PERTHREAD)
public class RedisPublisherImpl implements RedisPublisher
{
private Jedis jedis;
public RedisPublisherImpl()
{
this.jedis = new Jedis("localhost");
}
public void publish(String channel, String message)
{
jedis.publish(channel, message);
}
}
Note that @Scope(value = ScopeConstants.PERTHREAD) is used here. It means that we are going to make one connection to Redis per request. That’s a good balance between a persistent connection and a connection for every Redis action.
Let’s add the service declaration in the application module.
HotelBookingModule.java
public static void bind(ServiceBinder binder)
{
binder.bind(Authenticator.class, BasicAuthenticator.class);
binder.bind(RedisPublisher.class, RedisPublisherImpl.class);
}
Each time a booking is made, we want it to be published to Redis. Let’s edit the Book page.
Book.java
...
@OnEvent(value = EventConstants.SUCCESS, component = "confirmForm")
public Object confirm()
{
// Create
dao.create(booking);
userWorkspace.confirmCurrentBooking(booking);
publisher.publish("booking:" + booking.getId() + ":hotel", booking.getHotel().getName());
booking = null;
// Return to search
return Search.class;
}
...
I suggest you to learn Redis if you want to understand the channel declaration "booking:" + booking.getId() + ":hotel"
The Monitor page
This page is fairly simple. You just need to include some scripts. I used jquery, a simple websocket client and a custom script to display messages.
Monitor.java
package com.tap5.hotelbooking.pages;
import org.apache.tapestry5.annotations.Import;
@Import(library =
{ "context:/js/jquery.1.4.3.min.js", "context:/js/ws.js",
"context:/js/scripts.js" })
public class Monitor
{
}
Monitor.tml
<html t:type="layout" t:pageTitle="Hotel details"
xmlns:t="http://tapestry.apache.org/schema/tapestry_5_1_0.xsd"
xmlns:p="tapestry:parameter">
<!-- Most of the page content, including <head>, <body>, etc. tags, comes from Layout.tml -->
<h2>Monitor booking... in realtime!</h2>
<div id="log"></div>
<p:sidebar>
<p>
Tapestry has an existing set of components that will help you to quickly
handle common stuff. Displaying Javabean properties, generate form from Javabean,
display tabular datas... All of these to get a clean and efficient template's code.
</p>
</p:sidebar>
</html>
src/main/webapp/js/ws.js
var conn;
var connect;
(function($) {
connect = function() {
if (window["WebSocket"]) {
conn = new WebSocket("ws://localhost:8000/test");
conn.onmessage = function(evt) {
$(window).trigger('WSmessage',evt.data);
};
}
};
})(jQuery);
src/main/webapp/js/script.js
var $j = jQuery.noConflict();
(function($) {
jQuery.fn.log = function(msg) {
if (window.console != undefined)
console.log("%s: %o", msg, this);
return this;
};
$(window).load(function() {
$(window).bind("WSmessage", function(e, data) {
$("#log").append(data + "<br />");
});
if (!conn) {
connect();
}
});
})(jQuery);
Don’t forget to get JQuery 1.4.3 from the official website, and put it in src/main/webapp/js.
Server side JavaScript
Finally, here comes the time to play with NodeJS!
I’ve put everything under src/nodejs
First, let’s download some libraries. A Websocket server implementation, and a Redis client.
Move redis-client.js, ws.js and ws/ to src/nodejs/lib
Let’s now create the NodeJS server itself in src/nodejs. Code is based on one of the examples distributed with the WebSocket server implementation repository.
realtime-server.js
var sys = require("sys"),
fs = require("fs"),
path = require("path"),
http = require("http"),
ws = require('./lib/ws'),
redis = require("./lib/redis-client");;
var pubsub = redis.createClient();
var connect;
pubsub.stream.addListener('connect', function() {
pubsub.subscribeTo('booking:*:hotel', function(channel, data) {
var bid = channel.toString().split(':')[1];
sys.debug("Publishing new booking : " + bid);
if (connect) {
connect.write(data);
}
});
});
/*-----------------------------------------------
logging:
-----------------------------------------------*/
var months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
function pad(n) {
return n < 10 ? '0' + n.toString(10) : n.toString(10);
}
function timestamp() {
var d = new Date();
return [
d.getDate(),
months[d.getMonth()],
[ pad(d.getHours())
, pad(d.getMinutes())
, pad(d.getSeconds())
, (d.getTime() + "").substr( - 4, 4)
].join(':')
].join(' ');
};
function log(msg) {
sys.puts(timestamp() + ' - ' + msg.toString());
};
function serveFile(req, res){};
/*-----------------------------------------------
Spin up our server:
-----------------------------------------------*/
var httpServer = http.createServer(serveFile);
var server = ws.createServer({
debug: true
}, httpServer);
server.addListener("listening", function(){
log("Listening for connections.");
});
// Handle WebSocket Requests
server.addListener("connection", function(conn){
log("opened connection: "+conn.id);
connect = conn;
conn.broadcast("<"+conn.id+"> connected");
conn.addListener("message", function(message){
log("<"+conn.id+"> "+message);
conn.broadcast(message);
});
});
server.addListener("close", function(conn){
log("closed connection: "+conn.id);
conn.broadcast("<"+conn.id+"> disconnected");
});
server.listen(8000);
Run everything and test!
Run redis-server
Run the Java application with mvn jetty:run
Run the NodeJS server with node realtime-server.js in src/nodejs
Open two browser windows (with a browser that support WebSocket of course, like Google Chrome). First window should go to http://localhost:8080/tapestry5-hotel-booking/monitor. Second window should search for an hotel and make a booking.
As the booking is confirmed, you should see it appearing in the first window.
Get all the sources on Github
If you just want to grab the code and run it, I’ve put everything on Github.
Final thoughts
I believe there is something to do here. What about a new Tapestry 5 contribution, including a service that facilitate publishing to Redis, and a component or a mixin with a JS file providing easy subscription to websockets? What do you think?


Simply awesome !! :)
Great post! A very nice example on using awesome node.js stuff, however why use Redis to synchronize between Tapestry and Node.js applications? You can simply push notifications directly to node.js and there synchronize with Redis (e.g. for persistency reasons).
Basically Tapestry is the best Java-based web application framework, however extensive JavaScript development nowadays may significantly decrease Tapestry’s value proposition.
@Renat > How would you send directly notifications from Tapestry5 to NodeJS?
Thanks a lot for that extremely cool post.
Thanks for the useful posting. I am evaluating Java Web Frameworks and want something that is not bloated, supports REST, is not component-based, and is efficient (eg. Java). IE. I want to use html and jquery. I think that Play! fits that criteria. Does Tapestry? I have also done some evaluation of Nodejs and while very impressed, the async way of doing things seems very complicated. I have also looked at ROR and decided “maybe not” for a number of reasons – performance vs Java, language quirks (eg. unless clause which IMHO is bad design), to name two. Any comments on Play! vs Tapestry would be good. Regards.
Note: If you want to have multiple connections, store the conn.id in redis under another connection, then when the stream listener gets the message, it can use that to look up the conn.id of the person you want to send to. Then, use server.send(conn.id,message) if using websocket-server. Just something that worked for me. I might be doing it wrong but I found if you set “connect” once as in this example, it gets reset every time someone new connects. Also note: I’m just pulling bits and pieces from this example, I’m not running it as-is so maybe it doesn’t pertain. BUT….this little example here I’m posting should help someone.
This is awesome! Thanks a lot Robin! I think a deeper integration with Tapestry (maybe in form of a module) would push Tapestry as the first pick when choosing a web framework for building feature-rich web apps.
awesome post…
No update since 08 Nov. 2010. Is wooki still under development? Just post, if the wohle project is still active.