AJAX Longpolling with ColdFusion and BlazeDS - Subtopics and building a messaging app

I'm sorry to say that I've been putting this post off. Why? Well to be honest I've been struggling with getting my head around how to actually use longpolling. Its all very well understanding the mechanics, but how can you use it effectively.

Part of the problem I've been having is the nature of sending messages and filtering them with BlazeDS. What I keep forgetting is that long poll messaging is "fire and forget". You don't send a message and expect an immediate response. You send a message and a response might be broadcast to all event listeners with a specific filter. If you want a request and response, call a webservice or a make an ajax request. Don't use long polling and event gateways.

Remembering this is pretty important. If you try to get long polling with FDMS, BlazeDS and ColdFusion event gateways to work like Ajax XHR then you're going to end up in a mess.

There are two places you will need to filter messages. Fairly obviously these are in the javascript consumers/event listeners on the client side and in the onIncomingMessage function in your ColdFusion event gateway.

I'm going to start with the client side as this also forms the basis of filtering messages on the server side.

There are two methods for filtering messages; selectors and sub-topics. There is essentially no difference between selectors and sub-topics. They are both a string of characters that the consumer looks for in a broadcast message to see if it should grab that message and trigger a client-side event. The difference comes in flexibility.

Selectors are a single "word", whereas sub-topics have delimited values to which you can apply wildcards. Selectors are available to use without any additional configuration. Sub-topics require a little additional configuration in the destination set up. I've been using sub-topics, so I'm going to cover sub-topics here, but the basic premise is the same for selectors.

Starting at the beginning, the additional configuration is actually only a couple of extra lines in the server setting for the destination.

view plain print about
1ss.setAllowSubtopics(true);
2    ss.setSubtopicSeparator('.');
As it happens, these lines are already in the code from way back when I wrote part 2 of this series. These two lines should be fairly self-explantory. The first setAllowSubtopics tells the destination to allow subtopics to be used in the Producers and Consumers that use it as a destination. The second, setSubtopicSeparator, defines the character that will be used as a domain seperator.

So having set the destination to allow subtopics we now need to update a Consumer/Listener that can listen for specific topics. Lets start with something simple and listen for a specific three part subdomain. The three parts are going to be "app" for the full application, "user" for user related events and a "hello" event.

view plain print about
1consumer = new Consumer();
2    consumer.setDestination('dispatcher');
3    consumer.addEventListener("message", messageHandler);
4    consumer.setChannelSet(cs);
5    consumer.setSubtopic('app.user.hello');
6    consumer.subscribe();

So that's the updated consumer from part two. The only difference is that we've set a subtopic that it will listen for of "app.user.hello". Having updated the listener we need to update the simple sender that I created in part 2 to also use subtopics. This is done by adding a subtopic to the message headers. The name for the subtopic field in the header is "DSSubtopic". This field names in the header of client messages are case sensitive, so make sure that the "DSS" on the front is capitalised and the rest is lower case.

view plain print about
1<cfscript>
2clientMsg = {Destination='dispatcher',body={text='The answer to life, the universe and everything'}};
3clientMsg['headers']['DSSubtopic'] = 'app.user.hello';
4
5sendGatewayMessage('myGateway', clientMsg);
6
</cfscript>

You should now be able to load the listener page, load the sender page in another broswer or rbowser window or tab and you'll recieve an alert of "The answer to life, the universe and everything" on the listener page.

A word of warning here, and one I should have raised before now. If you load the sender page first and haven't previously loaded any of the listener pages from this or previous posts, you will get an error. The error is because the destination has yet to be created by the listener and the sender has no idea where to send the message. [TODO ERROR HANDLING THIS : CREATING THE DESTINATION FROM THE SERVER SIDE]

So fine. I can send a message directly to a listener of a specific name. Whoop de doo. Its no different to what we were doing previously! Change the subtopic in the consumer to "app.user.*", reload the listener page and then refresh the sender page. You should still get the alert. OK, next change the sender subtopic in the sender page to "app.user.owtornowt". To be honest you can change the last part of the subtopic to "asdf" or the name of your wife's mother's brother for this next bit and it will still work. Having changed the subtopic to a "word" of your choice, reload the sender page to get it to send the client message. Once again you should get the alert on the client with out message text in it.

What's happening here is that the listener is now listening for any subtopic that starts with "app.user". It doesn't matter what comes after this stub of the subtopic the listener will pick it up and handle it. What this means is you can send out a broadcast to everyone that is listening. The data that you send from the server will then be processed by the handler.

You can do the reverse of this and send subtopic to the server in exactly the same way.

view plain print about
1function send(subtopic,msg){
2    var message = new AsyncMessage();
3    message.setBody(msg);
4    
5    producer.setSubtopic(subtopic);
6    producer.send(message);
7}
This updated send() function allows you to set the subtopic of the message you want to send the server. As you can see its as simple as "setSubtopic('mytopic.here')".

Lets put this to the test and build a little app that uses subtopics on both the client and server side. This app will send a message from the server to a "app.user.message" and will only be displayed by a randomly selected available client.

To start with we need to register the client with the server when we open the page.

First I'm adding in a couple of javascript functions from http://note19.com/2007/05/27/javascript-guid-generator/ to create a UUID that I can send to the server. NOTE: I could just hard code a createuuid() into the javascript, but I'm trying to always seperate client side code from server side code. I could do something like create a web service that I could call from javascript to register and return a clientID and this would be a better solution in the long term, but for simplicity and for the sake of this demo I'm just going to create GUIDs on the client side and send them to the server.

view plain print about
1function S4() {
2 return (((1+Math.random())*0x10000)|0).toString(16).substring(1);
3}
4function guid() {
5 return (S4()+S4()+"-"+S4()+"-"+S4()+"-"+S4()+"-"+S4()+S4()+S4());
6}

I'm going to get rid of the sendTest function and replace it with a "register" function. This function will grab a globally available ClientID and send it to the server under the subtopic of "app.user.register".

view plain print about
1function register() {
2    regData = new Object();
3    regData.clientid = ClientID;
4    send('app.user.register',regData);
5}

The register function can't be called until the polling has been initialised, so I'm going to add the creation of "ClientID" and a call to the register() function into the initPolling function.

view plain print about
1ClientID = guid();
2    register();

Now over to the server side. We're going to be storing the client ids in the application scope, so we're going to need an Application.cfc. The one over on Ben Nadel's blog is as good as any and I've just dropped that into this demo code with a change of application name. I'm also going to create an empty array in the application scope called "clientregister". This is where I'm going to store my client ids when they register.

Next we need to update the onIncomingMessage function in the gateway.cfc. I'm not going to go nuts with this, so I'm just going to assume that there will always be a DSSubtopic in the header of any message and I'll put the data from the body section of the message into a variable for easy access.

Next I'll set up a simple switch to look at the subtopic and do "something" based on that subtopic. In this case I'm going to add the client id to the end of the array and send back a quick response. To do this I've also added a "sender" method based on the code in our sender.cfm file. So here's what my gateway looks like now

view plain print about
1<cfcomponent name="eventgateway handler" output="false">
2    <cffunction name="onIncomingMessage" access="remote" returntype="any">
3        <cfargument name="event" required="true" type="struct">
4        <cfset var SubTopic = arguments.event.data.headers.dssubtopic>
5        <cfset var data = arguments.event.data.body>
6
7        <cfswitch expression="#SubTopic#">
8            <cfcase value="app.user.register">
9                <cfset ArrayAppend(application.clientregister,data['clientid'])>
10                <cfset dataPacket = {text='registration complete',clientid=data['clientid']}>
11                <cfset sender(dataPacket,"app.user.hello")>
12            </cfcase>
13        </cfswitch>
14
15        <cfreturn>
16    </cffunction>
17    
18    <cffunction name="sender" access="private">
19        <cfargument name="body" required="true" type="struct">
20        <cfargument name="subtopic" require="false" type="string" default="app">
21        
22        <cfscript>
23        clientMsg = {Destination='dispatcher',body=arguments.body};
24        clientMsg['headers']['DSSubtopic'] = arguments.subtopic;
25        
26        sendGatewayMessage('myGateway', clientMsg);    
27        
</cfscript>
28    </cffunction>
29</cfcomponent>

At the minute the message from the sender is going to display for every client hooked into the gateway, so we'll need to update the handler on the client side. First though, take a quick look at the data packet I'm sending back. I've included the clientid that I was sent in the packet I'm sending out. This means that I'll be able to see this on the client side when I get a message to handle, so lets update the handler to look at this value.

view plain print about
1function messageHandler(event){
2    var eventBody = event.getMessage().getBody();
3    var subTopic = event.getMessage().getHeaders().DSSubtopic;
4    if (eventBody.CLIENTID != undefined && eventBody.CLIENTID === ClientID) {
5        alert(subTopic+": "+eventBody.TEXT);
6    } else {
7        if (subTopic == 'app.user.hello') alert('Someone just registered!');
8    }
9}

In the function I grab the event body and, as the handler is for a wild card subtopic, I also grab the subtopic from the message header. I'm going to quickly check that CLIENTID does exist in the event body and check to see if it is the same as this client's global "ClientID". If it is I'm going to display the subtopic and the message. If its not then I'll display a message on all the other connected clients of "Someone just registered!".

Its 2am and I've just realised how huge this post is, so I'm going to leave this application build here and continue on with it in the next post.

Have fun with the attached code!

TweetBacks
Comments
Great stuff, Stephen. Thanks for taking the time to do such a thorough write-up.
# Posted By Marc Esher | 6/24/10 1:07 PM