Skip to main content
Ben Nadel at CFUNITED 2009 (Lansdowne, VA) with: Kyle Dodge
Ben Nadel at CFUNITED 2009 (Lansdowne, VA) with: Kyle Dodge

ColdFusion 10 - Creating A ColdFusion WebSocket AMD Module For Use With RequireJS

By
Published in , Comments (12)

AJAX (Asynchronous JavaScript and XML) is awesome. It has allowed us to revolutionize our web-based applications by minimizing the overhead inherent with the standard Request-Response lifecycle. The next evolutionary step in web application development is Push technology - allowing the server to communicate directly with the client outside of the request-response lifecycle. Services like Pusher and PubNub and libraries like NowJS have given us a taste of what can be done with push technology. ColdFusion 10 opens up native Push technology to ColdFusion developers through an integrated WebSocket server and client-side JavaScript library.

NOTE: At the time of this writing, ColdFusion 10 was in public beta.

WebSockets are complicated! Not only is the technology fairly new (and still evolving), the use of channel-based communication is a big departure from what I'm used to. The code I have for this demo took me about 10 hours to put together. And, that code only begins to scratch the surface of how applications can or should integrate WebSocket technology. I'm learning as I go here, so take all of the following with a grain of salt.

As I was reading through the "What's New In ColdFusion 10" documentation, I saw that the new client-side WebSocket library was being exposed as a ColdFusion tag, <cfwebsocket>. As someone who's been trying to understand modular, client-side web application architecture, this tag-based interface is problematic. So, I thought it would be an excellent learning experience if I could somehow figure out how to expose the underlying ColdFusion WebSocket library as an AMD (Asynchronous Module Definition) module that can be used with libraries like RequireJS.

When you use the ColdFusion CFWebSocket tag, a number of things happen in your rendered web page:

  • Several JavaScript values are stored in the global scope.
  • Several JavaScript files are loaded from the CFIDE directory.
  • An instance of the WebSocket library is created.

Using this tag-based approach has a number of drawbacks:

  • It has to be used on a ColdFusion page.
  • It loads all of the required JavaScript files on page load.
  • It requires you to define your event handlers before you instantiate your WebSocket object.

I've attempted to remedy these issues by encapsulating the logic in an AMD module, CFWebSocket.js. By using an AMD module, I can lazy-load the WebSocket library, including its dependencies; and, I can provide much more fine-tuned control and monitoring of the WebSocket streams. Furthermore, I can create a more intuitive, Deferred-based functionality for data access that would otherwise be exposed through asynchronous WebSocket events.

What I created was the AMD module, CFWebSocket. The code for this module (and the demo) can be found on my GitHub account. Here is the public API for the module:

  • ColdFusionWebSocket( [channel [, channel]] )
  • authenticate( username, password )
  • closeConnection()
  • getClientID() :: Int
  • getSubscriberCount( channel ) :: Deferred
  • getSubscriptions() :: Deferred
  • isConnectionOpen() :: Boolean
  • off( eventType, callback )
  • on( eventType [, channel], callback [, context] )
  • openConnection()
  • parse( json ) :: Any
  • publish( channel, data [, headers] )
  • stringify( value ) :: JSON
  • subscribe( channel [, headers] )
  • unsubscribe( channel )

One of the things that I like most about this implementation is the on() and off() methods. I took inspiration from the jQuery library and exposed WebSocket events through on() and off() bindings. So, for example, if you wanted to listen for the "welcome" event, you could use the following binding:

socket.on( "welcome", function( event ){ ... } );

Or, if you wanted to listen for the "message" event on the channel, "chat", you could use the following binding:

socket.on( "message", "chat", function( event, data ){ ... } );

For this module, the notion of a "message" diverges greatly from the underlying ColdFusion socket connection. In the underlying library, most things are considered "messages"; however, in my module, the "message" events only carry Data. Other types of underlying messages, like "authenticate", "publish", "subscribeTo", and "welcome" are filtered out and trigger as separate event types (that can be monitored using on() and off()).

Once I had the module in place, I create a small Chat demo to test the module and figure out how the Server-Client interaction actually worked. This was a grueling process. Debugging WebSocket calls is not the easiest thing. Thank goodness for CFDump and Firebug, am I right?!

Here is the client-side code for my Chat user interface (UI). As you can see, this demo makes use of the RequireJS library which, in turn, bootstraps the application and loads the ColdFusionWebSocket() module.

index.cfm - Our Simple Chat Demo Using RequireJS

<!---
	We need to pass the Application name to the ColdFusion WebSocket
	so that it knows which memory space to use. To use this, we'll
	pass it through with the HTML element.
--->
<cfset appName = getApplicationMetaData().name />

<!doctype html>
<html data-app-name="<cfset writeOutput( appName ) />">
<head>
	<meta charset="utf-8">
	<title>Using ColdFusion 10 WebSockets With RequireJS</title>

	<!-- Load the demo styles. -->
	<style type="text/css">

		div.chatWindow {
			border: 1px solid #666666 ;
			height: 200px ;
			overflow: hidden ;
			position: relative ;
			width: 450px ;
			}

		ol.chatHistory {
			bottom: 0px ;
			left: 0px ;
			margin: 0px 0px 0px 0px ;
			padding: 0px 0px 0px 0px ;
			position: absolute ;
			right: 0px ;
			}

		ol.chatHistory li {
			border-top: 1px solid #CCCCCC ;
			margin: 0px 0px 0px 0px ;
			padding: 5px 5px 5px 5px ;
			}

		ol.chatHistory li.event {
			color: #999999 ;
			font-style: italic ;
			}

		ol.chatHistory span.handle {
			font-weight: bold ;
			margin-right: 10px ;
			}

		ol.chatHistory span.message {}

		form {
			margin: 10px 0px 0px 0px ;
			}

		input.handle {
			font-size: 16px ;
			width: 100px ;
			}

		input.message {
			font-size: 16px ;
			width: 275px ;
			}

		input.submit {
			font-size: 16px ;
			}

		p.chatSize {
			color: #999999 ;
			font-size: 13px ;
			font-style: italic ;
			}

	</style>

	<!--
		Load the script loader and boot-strapping code. In this
		demo, the "main" JavaScript file acts as a Controller for
		the following Chat interface.
	-->
	<script
		type="text/javascript"
		src="./js/lib/require/require.js"
		data-main="./js/main">
	</script>
</head>
<body>

	<h1>
		Using ColdFusion 10 WebSockets With RequireJS
	</h1>

	<div class="chatWindow">
		<ol class="chatHistory">
			<!-- Chat history will be populated dynamically. -->
		</ol>
	</div>

	<form class="chatMessage">
		<input type="text" name="handle" class="handle" />
		<input type="text" name="message" class="message" />
		<input type="submit" value="Send" class="submit" />
	</form>

	<p class="chatSize">
		People in chat room: <span class="count">0</span>
	</p>

</body>
</html>

As you can see, there's no <cfwebsocket> tag on this page. In fact, the only ColdFusion code on this page is the storage of the application Name in the HTML tag. I didn't want to have to do this; but, the underlying WebSocket library needs the ColdFusion application name in order to access the appropriate memory space (I assume).

Once RequireJS loads, it bootstraps the application, loading the main JavaScript file. For simplicity, this main.js file acts as the Controller for the application, both instantiating and using the ColdFusionWebSocket() module.

main.js - Our Application Controller

// Define the paths to be used in the script mappings. Also, define
// the named module for certain libraries that are AMD compliant.
require.config({
	baseUrl: "js/",
	paths: {
		"domReady": "lib/require/domReady",
		"jquery": "lib/jquery/jquery-1.7.1",
		"order": "lib/require/order",
		"text": "lib/require/text",
	}
});


// Load the application. In order for the Chat controller to run,
// we need to wait for jQuery and the CFWebSocket module be available.
require(
	[
		"jquery",
		"cfwebsocket",
		"domReady"
	],
	function( $, ColdFusionWebSocket ){


		// I activate all the form fields.
		function activateForm(){

			dom.handle.removeAttr( "disabled" );
			dom.message.removeAttr( "disabled" );
			dom.submit.removeAttr( "disabled" );

			// Focus the handle input.
			dom.handle.focus().select();

		}


		// I add the given message to the chat history.
		function addMessage( handleData, messageData ){

			var handle = $( "<span />" )
				.text( handleData + ":" )
				.addClass( "handle" )
			;

			var message = $( "<span />" )
				.text( messageData )
				.addClass( "message" )
;

			var item = $( "<li />" )
				.addClass( "message" )
				.append( handle )
				.append( message )
			;

			// Add the new history item.
			dom.chatHistory.append( item );

		}


		// I deactivate all the form fields.
		function deactivateForm(){

			dom.handle.attr( "disabled", "disabled" );
			dom.message.attr( "disabled", "disabled" );
			dom.submit.attr( "disabled", "disabled" );

		}


		// I log the given event to the chat history.
		function logEvent( description ){

			var item = $( "<li />" )
				.text( description )
				.addClass( "event" )
			;

			// Add the new history item.
			dom.chatHistory.append( item );

		}


		// I select a random name and return it.
		function getRandomHandle(){

			var names = [
				"Sarah", "Joanna", "Tricia", "Ben", "Dave", "Arnold",
				"Kim", "Anna", "Kit", "Sly", "Vin", "Dwayne"
			];

			// Return a random name.
			return(
				names[ Math.floor( Math.random() * names.length ) ]
			);

		}


		// I update the room count.
		function updateRoomSize(){

			// Get all the users who are subscribed to the chat
			// room. Since we can't subscribe to the main "chat"
			// channel AND a sub-channel at the same time, just get
			// all the users that are subscribed to the message sub-
			// channel. That should be good enough.
			var countPromise = socket.getSubscriberCount( "chat.message" );

			// When the result comes back, update the room count.
			countPromise.done(
				function( count ){

					dom.roomSize.text( count );

				}
			);

		}


		// -------------------------------------------------- //
		// -------------------------------------------------- //


		// Cache the DOM elements that we'll need in this demo.
		var dom = {};
		dom.chatHistory = $( "ol.chatHistory" );
		dom.form = $( "form" );
		dom.handle = $( "input.handle" );
		dom.message = $( "input.message" );
		dom.submit = $( "input.submit" );
		dom.roomSize = $( "p.chatSize span.count" );

		// Create an instance of our ColdFusion WebSocket module
		// and subscribe to several "chat" sub-channels. We're using
		// sub-channels so we can have more fine-tuned control over
		// how we respond to messages.
		var socket = new ColdFusionWebSocket(
			"chat.message",
			"chat.userlist.subscribe",
			"chat.userlist.unsubscribe"
		);

		// Set a random handle.
		dom.handle.val( getRandomHandle() );

		// Let the user know that we are connecting.
		logEvent( "Connecting to ColdFusion server." );

		// Disable the form elements until the socket has been
		// connected. We don't want people trying to push messages
		// until the subscription is open - that would cause an error.
		deactivateForm();

		// When the socket has connected, activate the form.
		socket.on(
			"open",
			function(){

				// Let the user know we have connected.
				logEvent( "Connected." );

				// Activate the form.
				activateForm();

				// Update the room-size.
				updateRoomSize();

			}
		);


		// When a message comes down in the "message" sub-channel, we
		// want to display it in the chat history.
		socket.on(
			"message",
			"chat.message",
			function( event, responseData ){

				// Deserialize the response.
				var response = socket.parse( responseData );

				// Add the message to the chat.
				addMessage( response.handle, response.message );

			}
		);


		// When the a new user has entered or left the chat room, we
		// want to announce the event and update the subsriber count.
		socket.on(
			"message",
			"chat.userlist",
			function( event, responseData ){

				// Check to see which sub-channel we are using.
				if (event.channel === "chat.userlist.subscribe"){

					// Deserialize the data for our new user.
					var user = socket.parse( responseData );

					// Log subscription event.
					logEvent(
						"A new user has entered the chat [ " +
						user.clientid + " ]."
					);

				} else {

					// Log the unsubscription event.
					logEvent( "A user has left the chat." );

				}

				// Update the room size.
				updateRoomSize();

			}
		);


		// Bind to the form submission so we can pipe the request
		// through our ColdFusion WebSocket connection.
		dom.form.submit(
			function( event ){

				// Prevent the form submission.
				event.preventDefault();

				// Get the cleaned form values. For this demo, we're
				// not going to do any real error handling. No need
				// to further complicate an ALREADY complex system.
				var handle = (dom.handle.val() || "User");
				var message = (dom.message.val() || "");

				// Publish the message, including the handle that
				// the user has chosen.
				socket.publish(
					"chat.message",
					{
						handle: handle,
						message: message
					}
				);

				// Post the local copy directly to the chat history.
				addMessage( handle, message );

				// Clear the message form and re-focus it.
				dom.message
					.val( "" )
					.focus()
				;

			}
		);


		// When the window closes (unloads), unsubscribe the user
		// from the various channels. This way, any other user in
		// the chat room can see what is happening.
		$( window ).bind(
			"beforeunload",
			function( event ){

				// Unsubscribe from all the channels.
				socket
					.unsubscribe( "chat.message" )
					.unsubscribe( "chat.userlist.subscribe" )
					.unsubscribe( "chat.userlist.unsubscribe" )
				;

			}
		);


	}
);

As you can see, this file requires the loading of the "cfwebsocket.js" module. This exposes the ColdFusionWebSocket() constructor which is then instantiated and invoked in response to UI-based events within the Chat demo.

I won't try to explain this code as this is probably too much to wrap your head around in one sitting (my head is still swimming). But, if you look at the demo Video, you'll see that this code works on the standard desktop browsers as well as on my iPhone simulator with mobile Safari. That's pretty badass!

My exploration of ColdFusion 10 WebSockets didn't just stay on the client - I did a bunch of digging around on the server as well. I wanted to think about WebSocket requests as being similar to all other requests going to the server. As such, I wasn't comfortable with having a bunch of different listener components; rather, I'd like to see some new "request" event handler in the Application.cfc.

To simulate this, I create one listener component, WSApplication.cfc, that intercepts all the incoming WebSocket requests and forwards them onto the core Application.cfc ColdFusion framework component. The WSApplication.cfc component looks for the following, optional event handlers:

  • onWSRequestStart( type, channel, user ) :: Boolean
  • onWSRequest( channel, user, message ) :: Any
  • onWSResponseStart( channel, subscriber, publisher, message ) :: Boolean
  • onWSResponse( channel, user, message ) :: Any

In this scenario, the "request"-oriented event handlers manage the authorization of requests (subscribe, unsubscribe, publish); the "response"-oriented event handlers manage the publishing of messages to the client(s). I tried to map the native WebSocket events to the existing Application.cfc event handlers.

Application.cfc - Our ColdFusion Application Framework Component

<cfscript>
// NOTE: The CFScript tag is added purely for Gist color-coding.

component
	output="true"
	hint="I define the application settings and event handlers."
	{


	// Define the application settings.
	this.name = hash( getCurrentTemplatePath() );
	this.applicationTimeout = createTimeSpan( 0, 0, 20, 0 );
	this.sessionManagement = false;

	// Set up the WebSocket channels. For this demo, I'm only
	// going to use one channel listener for all my channels. The
	// WSApplication component simply acts as a proxy and points
	// ass WebSocket requests back to the Application.cfc, as if
	// they were standard requests. Using four new methods:
	//
	// - onWSRequestStart() :: Boolean
	// - onWSRequest() :: Any (message)
	// - onWSResponseStart() :: Boolean
	// - onWSResponse() :: Any (message)
	//
	// NOTE: These are *NOT* core WebSocket methods. This is simply
	// the approach I've used to learn about ColdFusion 10 WebSockets.
	this.wsChannels = [
		{
			name: "chat",
			cfcListener: "WSApplication"
		}
	];


	// I authenticate the given WebSocket user....
	// NOT SURE WHAT THIS DOES JUST YET.
	function onWSAuthenticate( username, password, connection ){

		// Authenticate all users.
		connection.authenticated = true;
		connection.role = "anyUser";
		return( true );

	}


	// I initialize the incoming WebSocket request. The possible
	// types are [ subscribe | unsubscribe | publish ]. If I return
	// False, the request will not processed and the given request
	// (subscribe | publish) will be refused.
	function onWSRequestStart( type, channel, user ){

		// Check to see if the current request is for a new
		// subscription to the Chat. If so, we'll want to announce
		// the new user to the rest of the chat room.
		if (
			(type == "subscribe") &&
			(channel == "chat.message")
			){

			// Publish a new subscription notice to all users.
			wsPublish( "chat.userlist.subscribe", user );

		} else if (
			(type == "unsubscribe") &&
			(channel == "chat.message")
			){

			// Publish a new unsubscription notice to all users.
			wsPublish( "chat.userlist.unsubscribe", user );

		}

		// Return true so the request will be processed.
		return( true );

	}


	// I execute the incmoing WebSocket request (to publish). A
	// message must be returned (which will initialize the response)
	// that gets published to all relevant subscribers.
	function onWSRequest( channel, user, message ){

		// Check to see if the message ends in "!". If so, we'll
		// upper-case the entire value.
		if (
			(channel == "chat.message") &&
			reFind( "!$", message.message )
			){

			// Upper-case the EXCITED message!
			message.message = ucase( message.message );

		}

		// Return the message to publish to all users.
		return( message );

	}


	// I initialize the outgoing WebSocket response from the given
	// publisher to the given subscriber. This is called for every
	// subscriber on the given channel. Return True to allow the
	// message to be published to the given client. Return False to
	// prevent the message from being publisehd to the given client.
	function onWSResponseStart( channel, subscriber, publisher, message ){

		// We don't want to post BACK to the same user. So, only let
		// response (publication) through if the publisher and the
		// subscriber are NOT the same person.
		if (
			(channel == "chat.message") &&
			(publisher.clientID == subscriber.clientID)
			){

			// Prevent message echo.
			return( false );

		}

		// Return true so the message will be published.
		return( true );

	}


	// I execute the outgoing WebSocket response. A message must be
	// returned (which is what will be sent to the given user). This
	// provides a chance to format a message for an individual user.
	function onWSResponse( channel, user, message ){

		// Return the message to publish to THIS user.
		return( message );

	}


	// ------------------------------------------------------ //
	// ------------------------------------------------------ //


	// I log the arguments to the text file for debugging.
	function logData( data ){

		// Create a log file path for debugging.
		var logFilePath = (
			getDirectoryFromPath( getCurrentTemplatePath() ) &
			"log.txt"
		);

		// Dump to TXT file.
		writeDump( var=data, output=logFilePath );

	}


}

// NOTE: The CFScript tag is added purely for Gist color-coding.
</cfscript>

If you're interested in how the WSApplication.cfc component translates the WebSocket events into Application.cfc events, take a look at the GitHub repository. I'm not sure this is a good approach; but, it really helped me pick apart and understand the mechanics of the ColdFusion 10 WebSocket lifecycle (which is still fairly fuzzy!).

WebSockets are complicated. WebSocket servers are really complicated. The ColdFusion 10 WebSocket implementation has done a great deal to encapsulate much of that complexity; and still, it's really complex. This AMD-module exploration has helped me start to wrap my head around the WebSocket lifecycle; I can see, however, that I've only just begun to scratcht the surface. Very exciting stuff, though!

Want to use code from this post? Check out the license.

Reader Comments

1 Comments

Hi Ben,

Thanks for the post, I'll be putting this together to try out myself as soon as I can. The thing that I am curious about is whether it would be possible to write a mail server application in CF now that there is some capacity to read and write at the socket level. What do you think? Does the implementation allow you to address things like port numbers?

Thanks again!

Brett
B)

23 Comments

Ben,

Brilliant! I love the idea of making make the socket life cycle event based using the Application.cfc

Brett, not directly. See:http://dev.w3.org/html5/websockets/

Point 1 and Point 4. Websockets work on port 80 and port 443, if your idea is to check for new mail in real time, you would need a way for the webserver to talk to the mail server first. Which wouldn't be that hard with CFMail or CFExchange.

Tim

16 Comments

Awesome post Ben! I'm very excited about CF 10 native websocket support. I completely agree with your application event approach and the idea of client code being loaded as an AMD vs <cfwebsocket/>. Nice work.

15,848 Comments

@Tim,

Thanks! It just felt like the natural move. The more I can think of every incoming request as "a page" request, the less magical I can make it. I think it helps to really understand how a page request is working.

@Bob,

Thanks! I've since added the various examples to the Git repo. So, hopefully as I keep fleshing out ideas in the ColdFusion WebSocket world, I'll be able to keep this project up-to-date.

3 Comments

Have you tested this application on a browser that doesn't support web sockets like IE8? ColdFusion 10 is supposed to support a fallback of using a Flash Socket, but I have been unable to get it functioning.

I've tried 2 different linux distributions Fedora 16 (not officially supported by Adobe) and Ubuntu 11.10 (which is on the supported list) and received the exact same problem both times.

On your demo, the console error is not displayed because RequireJS doesn't call the socket; however, it still doesn't work. On other attempts, without RequireJS, it throws a JS error:
[WebSocket] cannot connect to Web Socket server at ws://[server name]:8585/cfusion/cfusion (SecurityError)
make sure the server is running and Flash socket policy file is correctly placed

Just curious if you ran into that problem and what OS you were running CF10 on...

Thanks!

4 Comments

Is CF able to connect to out site socket server using the socket gateway?

From what I found after googled, most of the sample that discuss around is the CF actually just listing to the out site socket connection.

Can we establish connection to any socket server from CF Application/CFC?

2 Comments

Hi Ben,

I've run into an issue, possibly due to my inexperience with Web Sockets, but I'm getting the error - Firefox can't establish a connection to the server at ws://localhost/cfusion/cfusion.

Any ideas?

2 Comments

Figured it out... CF 10 Standard limitation. We're running Standard and not Enterprise...

Guess I'll go back to your example using the Pusher service (which I haven't been able to get functioning yet...)

Thx,

1 Comments

I am working with a production environment with multiple load balanced Coldfusion servers. I have been trying to figure out a way to use websockets in this environment - when you never know which server the user is landing on. Do you have any suggestions?

15,848 Comments

@Greg,

Oooh, that's a really interesting problem. To be honest, I don't have a lot of experience with load-balanced servers. I wonder if sticky-sessions will work at the WebSocket level? But, even with sticky-session, it seems like you might run into the problem of only being able to announce events to people "stuck" on a given server.... but that's complete theory on my part (never tried it on multiple servers).

@All,

I've been using Pusher-App to implement my WebSockets these days. I have nothing against the CF implementation. But, I'm on ColdFusion standard and I simply cannot get enough connection on standard (what is the limit, like 100 concurrent connections?).

I'd be interested in creating a Pusher-App "drop-in" replacement for the CF10 WebSockets - so basically offloading the WebSocket-aspect to Pusher, but using the same application-events for CF10.

I wonder if that would even be possible.

15 Comments

Great article!

I am trying to take this approach with an AngularJS app. Any thoughts on removing cfwebsocket tag so that I can put an angular scope around the websocket calls? So basically bringing Angular binding to cfwebsockets.

Also - with the recent security fix wit CF10 HF11 and CF11, you need to define your cfc methods as remote to call them from the client.

I believe in love. I believe in compassion. I believe in human rights. I believe that we can afford to give more of these gifts to the world around us because it costs us nothing to be decent and kind and understanding. And, I want you to know that when you land on this site, you are accepted for who you are, no matter how you identify, what truths you live, or whatever kind of goofy shit makes you feel alive! Rock on with your bad self!
Ben Nadel