Thursday 1 October 2009

Getting Started with Reverse Ajax and Comet

In this article I'll be covering what Reverse Ajax and Comet are, how to use them, and how to get up and running with an example in no time at all.

Reverse Ajax and Comet

Reverse Ajax and Comet are essentially the same thing - long-lived HTTP requests allowing your browser to receive real-time events from the server without refreshing the page or installing any plugins. Chances are you use Comet every day without even realizing it: GMail use it for displaying new e-mails as they're received, Facebook uses it for chat and friend updates, last.fm will be using it to show you what your friends are currently listening to, basically its pretty widespread in Web 2.0 land.

Quick Overview of the Innards of Reverse Ajax and Comet

Initially maligned as a 'hack' by some, Comet has since matured, turning itself into the protocol du jour for Web 2.0. Implemented under the covers via long-polling XMLHttpRequests, hidden iframes, Bayeux, or HTML 5 it all works over HTTP - either keeping a connection open to the server or polling the server for the most recent updates. Luckily there are plenty of servers around now that hide all the nitty gritty details away and wrap them up in a reliable publish/subscribe framework.

Getting Started - 'Hello World' in Comet and Reverse Ajax

The first example we'll be creating will be the obligatory 'Hello World!'. For this example grab yourself a copy of the free Community Edition of StreamHub Reverse Ajax & Comet Server. This will limit us to 20 users but that should be plenty for our purposes. If you need more, you can request the free 60-day evaluation. To get started, create a directory called CometTutorial or similar, and create a new file helloworld.html, containing the following HTML:



<head>
<title>Hello World Comet Application</title>
<script src="streamhub-min.js" type="text/javascript"></script>
</head>
<body>
<h1>Hello World Comet Application</h1>
<input type="button" value="Say hello" onclick="start()">
<div id="streamingData"></div>
<script>
function topicUpdated(sTopic, oData) {
var newDiv = document.createElement("DIV");
newDiv.innerHTML = "Update for topic '" + sTopic + "' Response: '" + oData.Response + "'";
document.getElementById('streamingData').appendChild(newDiv);
}

function start() {
var hub = new StreamHub();
hub.connect("http://localhost:7878/");
hub.subscribe("HelloWorld", topicUpdated);
}
</script>
</body>
</html>


All we've done here is create a simple HTML host page for our 'Hello World' app and added a bit of code to the onclick of the 'Say hello' button to connect to the Comet server and subscribe to a topic, 'Hello World'. Notice the topicUpdated function is never called but instead passed into the subscribe function. The StreamHub Ajax API will call the function every time a new update is received from the server. Inside the topicUpdated function we create a new div and append it to the page to show the contents of the new event from the server. With the client-side done, we'll now need to create the server-side.

Create a new file called HelloWorld.java containing the following code:



import java.io.File;
import com.streamhub.api.PushServer;
import com.streamhub.nio.NIOServer;
import com.streamhub.api.Client;
import com.streamhub.api.SubscriptionListener;
import com.streamhub.api.JsonPayload;

public class HelloWorld implements SubscriptionListener {

public static void main(String[] args) throws Exception {
new HelloWorld();
}

public HelloWorld() throws Exception {
PushServer server = new NIOServer(7878);
server.addStaticContent(new File("."));
server.start();
server.getSubscriptionManager().addSubscriptionListener(this);
System.out.println("Comet server started at http://localhost:7878/.");
System.out.println("Press any key to stop...");
System.in.read();
server.stop();
}

public void onSubscribe(String topic, Client client) {
JsonPayload payload = new JsonPayload(topic);
payload.addField("Response", "Hello World!");
client.send(topic, payload);
}

public void onUnSubscribe(String topic, Client client) {
// no need to do anything here
}
}


In this bit of code we create a new PushServer, add the current directory as static content so we can serve up our helloworld.html page, and start 'er up.

Its important to note at this point that for a production app you'll want to server your static files from a separate HTTP server such as Apache, IIS or Tomcat. StreamHub is engineered for highly-scalable Comet & Reverse Ajax and although it can serve your static content, its not the primary purpose. The most typical setup is to serve static content via your usual HTTP server from www.<domain>.com and real-time content via StreamHub from push.<domain>.com.

The key part of the code is in the onSubscribe function, here we reply to any subscription requests from the client by sending them a JsonPayload containing our 'Hello World' message. Now we've created the server and client, we'll need to copy some files over from the StreamHub download and compile everything. If you haven't yet, download StreamHub Reverse Ajax & Comet Server, and extract the archive (there's a big download button on the website - you can't miss it). Inside the streamhub-java-adapter-sdk folder you should see something like this:




Copy the conf folder and the license.txt file over to the same folder our HelloWorld app is located in. This will stop StreamHub moaning about licenses and logging when we start it up later. Now navigate into the examples/lib/jar directory and copy over streamhub-2.0.8.jar, log4j-1.2.14.jar and json-20080701.jar to the same place as before. While you're there, copy streamhub-min.js from examples/lib/js. You should end up with a folder which looks something like this:




Now to compile, you'll need Java 5 or greater, if you don't have it, get it here (Download the latest Java SE Development Kit - JDK 6 at time of writing). To compile, open a command prompt in the HelloWorld directory and run:


javac -cp streamhub-2.0.8.jar HelloWorld.java

To start the server run (Windows):


java -cp .;streamhub-2.0.8.jar;log4j-1.2.14.jar;json-20080701.jar HelloWorld


On Unix, you'll need to replace all ';' with ':':


java -cp .:streamhub-2.0.8.jar:log4j-1.2.14.jar:json-20080701.jar HelloWorld

Now open a browser to http://localhost:7878/helloworld.html and click the 'Say hello' button to see it in action. You should see something like this (I clicked it a few times):




There we go! 'Hello World' for Reverse Ajax & Comet in a few lines of Java and JavaScript, no complicated configuration required. However, if you're like me, 'Hello World' is never enough and to be honest this simple app hardly shows off Comet - so lets create something a bit more exciting.

Getting Advanced - Reverse Ajax & Comet Stock Ticker

In this example, we're going to create a stock ticker table, a bit like the one on the StreamHub homepage. To start, create a file called stockticker.html in the same folder as the HelloWorld example. Add the following HTML to this file:



<head>
<title>Stock Ticker Comet Application</title>
<script src="streamhub-min.js" type="text/javascript"></script>
</head>
<body>
<h1>Stock Ticker Comet Application</h1>
<input type="text" value="AAPL" id="stockSymbol">
<input type="button" value="Subscribe" onclick="subscribe()">

<table id="tickerTable">
<tr><th align="left" width="150">Symbol</th><th align="left" width="100">Last Price</th></tr>
<tr><td>GOOG</td><td id="GOOG_Price">-</td></tr>
<tr><td>MSFT</td><td id="MSFT_Price">-</td></tr>
</table>

<script>
function topicUpdated(sTopic, oData) {
var priceDiv = document.getElementById(sTopic + "_Price");
priceDiv.innerHTML = oData.Last;
}

function subscribe() {
var topic = document.getElementById('stockSymbol').value;
var rowEl = document.createElement('tr');
var symbolEl = document.createElement('td');
symbolEl.innerHTML = topic;
var priceEl = document.createElement('td');
priceEl.id = topic + "_Price";
rowEl.appendChild(symbolEl);
rowEl.appendChild(priceEl);
tickerTable.appendChild(rowEl);
hub.subscribe(topic, topicUpdated);
}

var tickerTable = document.getElementById('tickerTable');

var hub = new StreamHub();
hub.connect("http://localhost:7878/");
hub.subscribe("GOOG", topicUpdated);
hub.subscribe("MSFT", topicUpdated);
</script>
</body>
</html>


This is a little bit more complicated than the 'Hello World' example. Here, we define a table in the HTML markup and give two of the cells the IDs of GOOG_Price and MSFT_Price. We then subscribe to GOOG and MSFT passing in a topicUpdated function as before. Now, in this topicUpdated, we find an element prefixed by topic and ending in _Price, so when we receive an update for topic GOOG, the element with ID GOOG_Price is updated with the latest price received from the server. Essentially whenever we receive an update from the server, the table is automatically updated with the new price. You'll notice we've added a textbox and a button to subscribe to a ticker symbol. This button calls the subscribe function and dynamically adds a new row to the table and subscribes to the topic via the hub. Now onto the server-side code.

Create a new file called StockTicker.java, this will provide random price events to our client. Insert the following code:



import java.io.File;
import com.streamhub.api.PushServer;
import com.streamhub.nio.NIOServer;
import com.streamhub.api.Client;
import com.streamhub.api.SubscriptionListener;
import com.streamhub.api.JsonPayload;
import java.util.Set;
import java.util.HashSet;
import java.util.Random;
import java.text.NumberFormat;
import java.text.DecimalFormat;

public class StockTicker implements SubscriptionListener {
private final Random random = new Random();
private final NumberFormat priceFormatter = new DecimalFormat("0.00");
private final Set symbols = new HashSet();
private PushServer server;


public static void main(String[] args) throws Exception {
new StockTicker();
}

public StockTicker() throws Exception {
server = new NIOServer(7878);
server.addStaticContent(new File("."));
server.start();
server.getSubscriptionManager().addSubscriptionListener(this);
new Thread(new RandomStockTicker()).start();
System.out.println("Comet server started at http://localhost:7878/.");
System.out.println("Press any key to stop...");
System.in.read();
server.stop();
}

public void onSubscribe(String topic, Client client) {
JsonPayload payload = new JsonPayload(topic);
payload.addField("Last", "0.00");
client.send(topic, payload);

synchronized(symbols) {
symbols.add(topic);
}
}

public void onUnSubscribe(String topic, Client client) {
// no need to do anything here
}

private class RandomStockTicker implements Runnable {
public void run() {
while(server.isStarted()) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {}

synchronized(symbols) {
for (String symbol : symbols) {
double nextPrice = random.nextDouble() * 100.0;
JsonPayload payload = new JsonPayload(symbol);
payload.addField("Last", priceFormatter.format(nextPrice));
server.publish(symbol, payload);
}
}
}
}
}
}


In the onSubscribe method we send a direct response to the client as we did in the 'Hello World' example, but we also add the client's subscription to a Set (unique list) of symbols. You'll notice in the constructor we kicked off a new Thread called RandomStockTicker. The RandomStockTicker simply loops through the list of symbols in the background and generates a random price for each one. It will continue to loop, sleeping for 1000ms between each burst of updates. Time to compile and run.

You should have all the files you need from the 'Hello World' example, leaving your folder looking something like this:



To compile, run the following:



javac -cp streamhub-2.0.8.jar StockTicker.java


And to run (Windows):



java -cp .;streamhub-2.0.8.jar;log4j-1.2.14.jar;json-20080701.jar StockTicker

On Unix:



java -cp .:streamhub-2.0.8.jar:log4j-1.2.14.jar:json-20080701.jar StockTicker


Now open up a browser to http://localhost:7878/stockticker.html and check it out! Because we used the publish method, the updates will be sent to all subscribed clients - so if you open up a few browsers you can see the same updates being delivered to multiple clients. Below I've opened it in Firefox, IE, Comet and Safari:




That's it for now. If you'd like to take it further, try adding green and red highlighting to the prices depending on whether they've gone up or down (Tip: check out the jQuery Highlight Effect). If you're looking for a more trick table or grid with sorting/filtering etc.., try ExtJS, YUI or the jQuery Grid Plugin. Remember to subscribe to the blog for the latest tutorials, releases and news.


8 comments:

  1. Thanks, nice article - I'd never heard the term Reverse Ajax before. One problem - the subscribe button didn't seem to work in IE 7 - think IE doesn't like the way you are adding elements to the table.

    ReplyDelete
  2. you failed to mention Meebo - one of the first pioneers to use Comet - Facebook, GMail etc.. just copied them.

    ReplyDelete
  3. Yes, Meebo was one of the first. In fact even before Meebo, Microsoft used Comet in Outlook Web Access.

    ReplyDelete
  4. And even before Meebo, DivmodNevow did that. Read more at http://divmod.org/

    ReplyDelete
  5. Hi, I'm trying to get started with streamHub - is it possible to write the server-side in a different language such as .NET?

    ReplyDelete
  6. Hi sharpie,

    Yes it is possible. You'll need to do things slightly differently to this article. Firstly, use one of the overloaded constructor methods NIOServer(InetAddress, InetAddress) to listen for streaming adapter connections. Then, get yourself a copy of the .NET Adapter SDK for Streaming which will allow you to publish from a .NET adapter instead of Java. Currently, this functionality is only available in the Web and Enterprise editions.

    ReplyDelete
  7. Ok thanks. One more thing - how many users can StreamHub handle? I need to be able to support about 1000 users. Are there any widgets I can use teh Jvascript API? - other comet solutions for example have some tables with pagination and highliting built-in to the API.

    ReplyDelete
  8. StreamHub can support as many users as ephemeral ports are available on the host server, anywhere upto ~64000, although it depends on the update frequency as to how many we would recommend. It will handle 1000 easily for even high update rates. If you get in contact with us directly we can run some benchmarks for you to give you an idea of the CPU & Memory Usage for your estimated update rate and server hardware.

    Regarding widgets, you may wish to check out some of the recommendations at the end of this article for open-source widgets which support pagination and highlighting. In future we will be releasing a set of widgets and examples to give users more of a jumpstart. Keep an eye on our blog for the latest releases and news.

    ReplyDelete