AJAX Longpolling with ColdFusion and BlazeDS - Connecting and Messaging

In my last post on long polling I went through the basic set up of ColdFusion and BlazeDS before getting into too much code.

This part I want to get a basic connection and a couple of simple messages being sent and received.

Before I go on, I should say that this is all done in ColdFusion 9, as BlazeDS is installed by default, but running BlazeDS on ColdFusion 8 is actually pretty simple to do.

First things first. You need to add a function to the gateway.cfc that will handle any incoming messages, cunningly named "onIncomingMessage". At this point it doesn't actually need to do a whole lot, so I've simply got a dump of the event to a file and return the data back to the caller.

view plain print about
1<cffunction name="onIncomingMessage" access="remote" returntype="any">
2    <cfargument name="event" required="true" type="struct">
3
4    <cfdump var="#arguments#" format="html" output="d:/dumps/incomingEvent_#dateformat(now(),"yyyymmdd")#_#timeformat(now(),"HHMMSS")#.html">
5
6    <cfreturn arguments.event.data>
7</cffunction>

That is it... Next...

In order to be able to connect to a BlazeDS channel, you need a destination. Like the channel definition I created in the previous post, you can manually create a destination in "messaging-config.xml", but I worked this all out from code from Joao's blog and Marko's Best of CF9 application, both of which use dynamic destinations.

Creating a destination is a little convoluted. To be able to create destinations dynamically you need to get to the Message Service via java. To get to the Message Service you need to go through the Message Broker.

NOTE : all of the code below is intended to be in a cfscript block (see attached files)

view plain print about
1brokerClass = createObject('java','flex.messaging.MessageBroker');
2broker = brokerClass.getMessageBroker( javacast('null','') );
3brokerControl = createObject('java','flex.management.runtime.messaging.MessageBrokerControl').init(broker);
4service = broker.getService('message-service');

Having got the message service we can now create the destination, but we need three bits of information and lets do a bit of validation.

The three bit of information we need are the name of the channel and gateway created in the previous part and the name of the destination we want to create. In this case I'm going to call my destination "dispatcher", my channel was "longpolling-amf" and the gateway id was "myGateway".

view plain print about
1channelName = "longpolling-amf";
2gatewayID = 'myGateway';
3destinationName = 'dispatcher';

If the destination already then you don't need to create it again. Destinations are created at a server level and persist, so only the first person accessing the application needs to create it.

view plain print about
1destinations = service.getDestinations();
2if (!structKeyExists(destinations,destinationName)){ //check if destination already exist
3
4}

getDestinations() returns a struct of all the destinations on the server and their properties. The destination name is the key to the struct entries at the top level, so we just check to see if the destination we want to create already exists in our "destinations" struct.

If there's no destination, then you'll need to create it.

view plain print about
1//Create the destination and connect it to the ColdFusion adapter defined in the flex/messaging-config.xml file.
2destination = service.createDestination(destinationName);
3destination.createAdapter('cfgateway');
4
5// Now apply some settings to the destination - these are the same one's you'd see in
6// the flex/remoting-config.xml file
7ss = createObject("java","flex.messaging.config.ServerSettings");
8ss.setAllowSubtopics(true);
9ss.setDurable(true);
10ss.setSubtopicSeparator('.');
11destination.setServerSettings(ss);
12
13
14//Initializes the adapter with the gateway properties.
15adapter = destination.getAdapter();
16configMap = createObject('java','flex.messaging.config.ConfigMap').init();
17configMap.addProperty('gatewayid',gatewayID);
18adapter.initialize('cfgateway',configMap);
19
20// connect the destination to the channel/endpoint we're using and start the destination
21destination.addChannel(channelName);
22destination.start();

Congratulations you now have a destination to which you can connect your javascript/flex bridge.

In the previous post I added a load function which had a call back to a initPolling function. This is where we need to look next. Now, before I do I should say that this is going to be a _really_ simple example. The creation of the producer/consumer (broadcaster/listener) should be done somewhere more appropriate, but for the purposes of this series of posts my init function will suffice. Lets crack on...

Loaded the FDMSBridge.swf flex object on to the page triggers an external call to a set of initialisation function in the two javascript libraries that come with the bridge. These calls create javascript facades to the flex equivalent objects. Take a look in the FDMSLib.js file and the BlazeDS docs for the objects that are available to you.

The objects we are going to use now are ChannelSet, Producer and Consumer. First up connect to your endpoint using a ChannelSet().

view plain print about
1<script type="text/javascript">
2function initPolling (){
3    var server = '<cfoutput>#cgi.http_host#</cfoutput>';
4    var cs = new ChannelSet();
5    cs.addChannel(new AMFChannel("longpolling-amf","http://"+server+"/flex2gateway/cfamflongpolling"));
6}
7</script>

I've been moving this code around between a few servers where I know that the end point is on the same server as my code, so I've "hacked" in a cgi.http_host, but equally I could use document.domain in javascript to specify my server. Set this anyway you wish, hardcode it, use my hack, whatever. Do be aware that you'll need to make sure that your calling domain is in the crossdomain.xml file if you are accessing different domain. I'm not going to go into this here.

What this javascript does is creates an AMFChannel from javascript, through the flex bridge to the channel/endpoint in BlazeDS on your ColdFusion server. Impressed? You should be. If you've written any flex or flash listeners that talk to coldfusion through the flash remoting or flex2 gateway you now have a reasonably complete set of objects that you would use there available to you in javascript.

Lets extend this. We need to be able to send messages to our event gateway to do this we need a broadcaster. "Producer" is the proper term in this case.

view plain print about
1producer = new Producer();
2producer.setDestination('dispatcher');
3producer.setChannelSet(cs);

This goes in the initPolling function after the addChannel function. It creates your Producer object, hooks it up to the destination we created and tells it to use the ChannelSet to our endpoint. This allows our javascript to send a message to our event gateway on ColdFusion.

Any message sent head down the channel set, through the flex bridge, to the endpoint, to the destination that routes the message to our ColdFusion event gateway, where it will be handled by the onIncomingMessage method in our cfc.

Sending a message is done using the AsyncMessage object from the FDMSLib. Its pretty simple; you create an AsyncMessage object, set the message body and then use the producer to send the message.

view plain print about
1function send(msg){
2    var message = new AsyncMessage();
3    message.setBody(msg);
4    producer.send(message);
5}

A message can be any data you wish to send. In this example I'm just sending some random strings and numbers.

view plain print about
1function sendTest() {
2    testData = new Object();
3    testData.andnow='for';
4    testData.something='completely';
5 testData.different=42;
6    send(testData);
7    return false;
8}

Jury rig a link to call the sendTest function in the HTML body and we're all set to send our test message to the server.

view plain print about
1<a href="" onclick="sendTest();">
2    Send the Test
3</a>
For the purposes of this simple example the gateway.cfc in the attached zip has a cfdump in it that writes to a file. When you click on the "send" link a dump of the data received by the gateway will be written to the dumps folder.

long polling event gateway data

In there you should be able to see the message we sent along with information about how the message was sent to the server and a unique identifier for where the message came from (originator). All this information can be used when replying to a message sent to the event gateway.

For the minute I'm going to skip over replies and just start with a simple broadcast from the server to all connected clients. Sending messages back to the client from the server requires a listener or consumer. Add the following to your initPolling function to create a consumer:

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

Consumer() is another object available in the FDMSLib. The code above connects a consumer to our destination and end point, adds a event listener of "message" and then tells the event gateway its ready and waiting by "subscribing".

The data I'm going to send back to the server will contain a string in a variable called "TEXT" and my handler is simply going to alert() that that text.

view plain print about
1function messageHandler(event){
2    alert(event.getMessage().getBody().TEXT);
3}

I strongly recommend using something like firebug to have a dig around in the event object to see all the data that is available in the event and message.

To send a message to all connected clients I need another template with 2 lines of cfscript in it.

view plain print about
1<cfscript>
2clientMsg = {Destination='dispatcher',body={text='The answer to life, the universe and everything'}};
3sendGatewayMessage('myGateway', clientMsg);
4
</cfscript>

The first line is a structure for my message to the client. Its made up of destination and body. Destination is the same destination we created previously and body is the data of the message. The second line is the ColdFusion function sendGatewayMessage(). You simply specify the gateway it will be sending the message through and the message it needs to send.

With your main index.cfm loaded and listening, load the page that you put this bit of cfscript into in another browser window. Within a few moments, if all your code is correct, you should get an alert appearing the first window.

And that is it for this post.

This is a very crude server-side sender. If you were to have more than one window open with the index.cfm in it, all of those windows would display the alert message. This isn't especially useful, so in the next post I'll cover replying to messages received by the event gateway and routing messages based on subtopics on both the client and server.

Attached to this post is the complete code for this blog post. Enjoy!

*UPDATE* I've sorted out the two issues that Anthony raised with the file path for dumps and the erroneous messages.

TweetBacks
Comments
Great series! Cant wait for the next (hopefully it is soon?), CF is so amazing... love it.
# Posted By Anthony Webb | 2/13/10 12:02 AM
Can you expand a little on this line?

cs.addChannel(new AMFChannel("longpolling-amf","http://"+server+"/flex2gateway/cfamflong...;));   

Is the second param the URL of my app? Some other dir? Looking at firebug and I see some polling going on to the above URL with 404 responses, where should that URL actually point?
# Posted By Anthony Webb | 2/13/10 12:53 AM
Hi Anthony,

Glad you're enjoying these posts. I'll try not to take too long over the next post.

The line you've highlighted creates the "funnel" from javascript, through flex to the endpoint that I created in the service-config.xml. The first attribute is the channel definition ID. The second is the endpoint uri.

I hope that helps.
# Posted By Stephen Moretti | 2/13/10 2:03 AM
I guess I was confused because in part 1 where you define the channel in services-config.xml you then say:

For now I'm manually creating this channel and endpoint in the services-config.xml. Later I want to remove this and dynamically create an end point, so that we don't have to do any manual configuration and for one other reason I'll come to later.

So in part 2 it looked like in the CFSCRIPT that you did it the automated way?

Should I go put the channel definition in my xml file and try that?
# Posted By Anthony Webb | 2/13/10 2:09 AM
Ah okay. In part one I add the channel/endpoint in the service config file. In part two I create a _destination_ using CFScript.

At some point I'll work out how to use cfscript to create the endpoint, but I haven't yet.
# Posted By Stephen Moretti | 2/13/10 2:17 AM
Yep that did it, adding the channel from part one of this series solved my issue.

Something odd about this demo. When I click the test link, it sends the test to the gateway.cfc... but then I get a JS alert box as well. What makes it more odd is that EVERY browser I have up gets that same "undefined" JS alert box. So there must be some sort of a message coming back through to the entire group of subscribers to that channel, and the eventHandler is picking it up and dumping it to the alert box as per the handler. Seems like an odd behavior. I would only imagine a "message" being sent if the user requested, this appears to be a phantom message?

One note for you mac users, in the gateway.cfc the log files will not be written to the correct "dumps" folder. Here is something more cross platform:

output="#expandPath('dumps')#/incomingEvent_#dateformat(now(),"yyyymmdd")#_#timeformat(now(),"HHMMSS")#.html"
# Posted By Anthony Webb | 2/13/10 2:27 AM
Ah, I see. I saw the "channelName" inside the cfscript and thought that was replacing the setup in part 1.

As for the curious, this phantom message I was talking about in my last post contains: ({fb_instance_id:"9"})

Looks like the instanceID in one that increments each time I click.
# Posted By Anthony Webb | 2/13/10 2:37 AM
Hmm I may have inadvertently put some code into the archive at half past midnight this morning. I'll check tonight.

With regards the dumps file path. The code should be cross platform already, but might have put a windows slash in the path.
# Posted By Stephen Moretti | 2/13/10 3:07 AM
Been waiting a long time to give this a shot. Took 10 minutes. Hardest part was compiling the SWF. Thanks dude!
# Posted By Raymond Camden | 6/23/10 6:10 PM