Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
I am the chief technical officer at InVision App, Inc - a prototyping and collaboration platform for designers, built by designers. I also rock out in JavaScript and ColdFusion 24x7.
Meanwhile on Twitter
Loading latest tweet...
Ben Nadel at RIA Unleashed (Nov. 2010) with:

ColdFusion 10 - Using WebSockets To Push A Message To A Target User

By Ben Nadel on
Tags: ColdFusion

With ColdFusion 10's in-built WebSocket server, pushing messages from the server to the client is super simple. If you want to send a message to all users subscribed to a given channel. But, what if you want to push a message to just a single user? Limiting the scope of message broadcasting gets a bit more tricky. ColdFusion 10 has some sort of filtering mechanism built into the publish / subscribe feature-set; however, I was not able to get it working. As such, I came up with an alternate approach to using WebSockets to push messages to a targeted user.

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


 
 
 

 
  
 
 
 

When using WebSockets to Push a message to a target user, two components have to be considered. For one, we need know which WebSocket connections are associated with our target user; and two, we need to have a way to selectively send messages over a subset of WebSocket connections.

As I demonstrated last week, there's no native connection between a standard ColdFusion session and a WebSocket session. This means that there's no inherent way to figure out which WebSocket connections are associated with the target user. Luckily, we can use the pseudo-event, onWSSessionStart(), in conjunction with custom headers, to store relevant user data in our WebSocket session.

Once we do have user-specific data in our WebSocket session, we can use our other pseudo-event, onWSResponseStart(), as a way to limit Push-broadcasting to a subset of WebSocket connections. This event gets called for every subscriber on a given channel; and, our response from this event - TRUE or FALSE - allows us to manage message distribution based on our WebSocket session data.

To demonstrate this approach, let's first look at the Push aspect of the application. In the following code, we have a page that allows a message to be pushed to a selected user. You'll see that the ID of the selected user is wrapped up in the message that we are pushing:

send.cfm - Our Server-Side WebSocket Push

  • <!--- Param our User ID variable. --->
  • <cfparam name="url.id" type="numeric" default="0" />
  •  
  • <!--- Check to see if a user ID has been selected. --->
  • <cfif url.id>
  •  
  • <!---
  • Loop over the application users to find one with the same ID
  • so we can send a message to that user.
  • --->
  • <cfloop
  • index="user"
  • array="#application.users#">
  •  
  • <!---
  • Check to see if this user record is the one we're going
  • to be sending a message to.
  • --->
  • <cfif (user.id eq url.id)>
  •  
  • <!---
  • Push a message to a SPECIFIC user (NOTE: This may
  • be multiple clients, depending on the user's browser
  • configuration).
  • --->
  • <cfset wsPublish(
  • "demo",
  • {
  • text: "Hello #user.name#, I hope you are well.",
  • targetUserID: user.id
  • }
  • ) />
  •  
  •  
  • </cfif>
  •  
  • </cfloop>
  •  
  • </cfif>
  •  
  •  
  • <!--- ----------------------------------------------------- --->
  • <!--- ----------------------------------------------------- --->
  •  
  •  
  • <!--- Turn off debugging output. --->
  • <cfsetting showdebugoutput="false" />
  •  
  • <!--- Reset the output buffer. --->
  • <cfcontent type="text/html; charset=utf-8" />
  •  
  • <!doctype html>
  • <html>
  • <head>
  • <meta charset="utf-8">
  • <title>Using ColdFusion 10 WebSockets To Target A User</title>
  • </head>
  • <body>
  •  
  • <h1>
  • Send A Message To A User
  • </h1>
  •  
  • <ul>
  • <cfoutput>
  •  
  • <!---
  • Output a link to send a static message to each of
  • the users.
  • --->
  • <cfloop
  • index="user"
  • array="#application.users#">
  •  
  • <li>
  • <a href="./send.cfm?id=#user.id#">
  • Send to #user.name#
  • </a>
  • </li>
  •  
  • </cfloop>
  •  
  • </cfoutput>
  • </ul>
  •  
  • </body>
  • </html>

As you can see, a user can be selected from a list of generated links. Once selected, we use the wsPublish() method to publish a WebSocket message over the "demo" channel. The message contains both the Text payload as well as a targetUserID property. This targetUserID property will be used to filter the outgoing message broadcast.

Both the WebSocket session initialization and the WebSocket broadcast filtering take place in our ColdFusion application framework component. In the following Application.cfc, notice that we are storing a UserID property in our persistent WebSocket session:

Application.cfc - Our ColdFusion Application Framework Component

  • <cfscript>
  • // NOTE: CFScript added for Gist color-coding only. Remove.
  •  
  • component
  • output="false"
  • hint="I define the application settings and event handlers."
  • {
  •  
  •  
  • // Define the application settings.
  • this.name = hash( getCurrentTemplatePath() );
  • this.applicationTimeout = createTimeSpan( 0, 0, 5, 0 );
  •  
  • // Turn on session management.
  • this.sessionManagement = true;
  • this.sessionTimeout = createTimeSpan( 0, 0, 5, 0 );
  •  
  • // Set up the WebSocket channels.
  • this.wsChannels = [
  • {
  • name: "demo",
  • cfcListener: "WSApplication"
  • }
  • ];
  •  
  •  
  • // I initialize the application.
  • function onApplicationStart(){
  •  
  • // Define some users with different IDs. For this demo, we're
  • // gonna look at Pushing messages to specific clients.
  • application.users = [
  • {
  • id: 1,
  • name: "Joanna"
  • },
  • {
  • id: 2,
  • name: "Sarah"
  • },
  • {
  • id: 3,
  • name: "Tricia"
  • }
  • ];
  •  
  • // Return true to the application can load.
  • return( true );
  •  
  • }
  •  
  •  
  • // I initialize the session.
  • function onSessionStart(){
  •  
  • // Set up the default session values.
  • session.id = 0;
  • session.name = "";
  •  
  • // Return out.
  • return;
  •  
  • }
  •  
  •  
  • // ------------------------------------------------------ //
  • // ------------------------------------------------------ //
  • // -- WebSocket Event Handlers -------------------------- //
  • // ------------------------------------------------------ //
  • // ------------------------------------------------------ //
  •  
  •  
  • // I initialize the WebSocket session. This gives us an
  • // opportunity to associate a WebSocket connection with a given
  • // user in the system.
  • function onWSSessionStart( user ){
  •  
  • // Param the UserID being passed through in the FORM scope
  • // (which is coming through in the custom WebSocket headers).
  • param name="form.userID" type="numeric" default="0";
  •  
  • // Store the User ID in the persistent connection info of
  • // the WebSocket user.
  • user.userID = form.userID;
  •  
  • // Return out.
  • return;
  •  
  • }
  •  
  •  
  • // I initialize the outgoing WebSocket response. This will get
  • // invoked for every subscriber that has subscribed to the given
  • // channel. This means we can determine the pass-through for each
  • // subscirber (based on return of TRUE | FALSE).
  • function onWSResponseStart( channel, subscriber, publisher, message ){
  •  
  • // Check to see if the given subscriber is the intended
  • // target for the given message. For this, we'll use the
  • // targetUserID property in the message.
  • if (
  • isNull( message.targetUserID ) ||
  • isNull( subscriber.userID ) ||
  • (message.targetUserID != subscriber.userID)
  • ){
  •  
  • // The subscriber is NOT the intended target.
  • return( false );
  •  
  • }
  •  
  • // If we made it this far, the subscriber was the intended
  • // target of the message.
  • return( true );
  •  
  • }
  •  
  •  
  • // I execute the WebSocket response.
  • function onWSResponse( channel, subscriber, message ){
  •  
  • // Right now, the message contains superfluous data regarding
  • // the target user. The subscriber doesn't actually need that
  • // message; so, let's unwrap the actual payload.
  • return( message.text );
  •  
  • }
  •  
  •  
  • // ------------------------------------------------------ //
  • // ------------------------------------------------------ //
  •  
  •  
  • // 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: CFScript added for Gist color-coding only. Remove.
  • </cfscript>

Once a WebSocket message is published, the onWSResponseStart() event handler is invoked (for all WebSocket connections on the given channel). This pseudo-event gives us an opportunity to allow or disallow message broadcasting to a given WebSocket connection. As you can see, if the targetUserID of the outgoing message does not match the persisted userID of the given WebSocket connection, the broadcast is denied (ie. the event-handler returns False). This will cause the outgoing WebSocket message to be broadcast to only the targeted user.

Since the target user doesn't need to be concerned with this targeting, we're using the onWSResponse() event handler to unwrap the text payload. Rather than broadcasting the entire message struct, we're extracting the text property and broadcasting only that value to the target user.

Now that we've seen the server-side, user-specific filtering of the WebSocket broadcast, let's take a look at the client-side code. In order for the filtering to work, the client has to pass the UserID to the server when it subscribes to a given WebSocket channel. This UserID is defined and persisted when the user logs into the demo. And, as you'll see in the following code, "logging-in" requires nothing more that selecting a desired persona.

index.cfm - Our Pseudo-Login For The Demo

  • <!--- Param our User ID variable. --->
  • <cfparam name="url.id" type="numeric" default="0" />
  •  
  • <!--- Check to see if a user ID has been selected. --->
  • <cfif url.id>
  •  
  • <!---
  • Loop over the application users to find one with the same ID
  • so we can property configure this user.
  • --->
  • <cfloop
  • index="user"
  • array="#application.users#">
  •  
  • <!---
  • Check to see if this user record is the one we're going
  • to be logged-in as.
  • --->
  • <cfif (user.id eq url.id)>
  •  
  • <!--- Configure the user's session. --->
  • <cfset session.id = user.id />
  • <cfset session.name = user.name />
  •  
  • <!--- Send user to the main page. --->
  • <cflocation
  • url="./user.cfm"
  • addtoken="false"
  • />
  •  
  • </cfif>
  •  
  • </cfloop>
  •  
  • </cfif>
  •  
  •  
  • <!--- ----------------------------------------------------- --->
  • <!--- ----------------------------------------------------- --->
  •  
  •  
  • <!--- Turn off debugging output. --->
  • <cfsetting showdebugoutput="false" />
  •  
  • <!--- Reset the output buffer. --->
  • <cfcontent type="text/html; charset=utf-8" />
  •  
  • <!doctype html>
  • <html>
  • <head>
  • <meta charset="utf-8">
  • <title>Using ColdFusion 10 WebSockets To Target A User</title>
  • </head>
  • <body>
  •  
  • <h1>
  • Select A User
  • </h1>
  •  
  • <ul>
  • <cfoutput>
  •  
  • <!--- Output a link to log-in as each user. --->
  • <cfloop
  • index="user"
  • array="#application.users#">
  •  
  • <li>
  • <a href="./index.cfm?id=#user.id#">#user.name#</a>
  • </li>
  •  
  • </cfloop>
  •  
  • </cfoutput>
  • </ul>
  •  
  • </body>
  • </html>

As you can see, the user simply selects the target record from the list of generated links. Once selected, the user's ColdFusion session data is updated and the user is forwarded to the following page where the user will subscribe to a WebSocket channel.

user.cfm - Our WebSocket Subscription Page

  • <!--- Check to make sure the user has logged-in. --->
  • <cfif !session.id>
  •  
  • <!--- Redirect back to login. --->
  • <cflocation
  • url="./index.cfm"
  • addtoken="false"
  • />
  •  
  • </cfif>
  •  
  •  
  • <!--- ----------------------------------------------------- --->
  • <!--- ----------------------------------------------------- --->
  •  
  •  
  • <!--- Turn off debugging output. It can't help us in WebSockets. --->
  • <cfsetting showdebugoutput="false" />
  •  
  • <!--- Reset the output buffer. --->
  • <cfcontent type="text/html; charset=utf-8" />
  •  
  • <!doctype html>
  • <html>
  • <head>
  • <meta charset="utf-8">
  • <title>Using ColdFusion 10 WebSockets To Target A User</title>
  •  
  • <script type="text/javascript">
  • <cfoutput>
  •  
  • // We need to pass the Application name through with the
  • // WebSocket connection so ColdFusion knows which memory
  • // space to access.
  • var coldfusionAppName = "#getApplicationMetaData().name#";
  •  
  • // Let's pass the user ID through with each WebSocket
  • // request. This way, we can associate the WebSocket
  • // requests with the appropriate session on the server.
  • var coldfusionUserID = #session.id#;
  •  
  • </cfoutput>
  • </script>
  •  
  • <!--
  • Load the script loader and boot-strapping code. In this
  • demo, the "main" JavaScript file acts as a Controller for
  • the following Demo interface.
  • -->
  • <script
  • type="text/javascript"
  • src="./js/lib/require/require.js"
  • data-main="./js/main">
  • </script>
  • </head>
  • <body>
  • <cfoutput>
  •  
  • <h1>
  • Hello, I'm #session.name#
  • </h1>
  •  
  • <p>
  • Check out my <em>JavaScript console</em> - that's where
  • my messages show up.
  • </p>
  •  
  • </cfoutput>
  • </body>
  • </html>

Like all of my other ColdFusion 10 WebSocket demos, this one uses RequireJS to load my ColdFusionWebSocket() AMD module. When we instantiate the ColdFusionWebSocket() module, we can provide a collection of custom headers to be passed-through with subscribe() and publish() requests; in this case, we need to define our persisted UserID as one of the custom headers. Unfortunately, this requires us to create a global JavaScript variable - coldfusionUserID - that represents this persisted value.

In our JavaScript controller, this coldfusionUserID is then defined as the "userID" custom header.

main.js - Our JavaScript Demo 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 demo controller to
  • // run, we need to wait for jQuery and the CFWebSocket module to
  • // become available.
  • require(
  • [
  • "jquery",
  • "../../../cfwebsocket",
  • "domReady"
  • ],
  • function( $, ColdFusionWebSocket ){
  •  
  •  
  • // Create an instance of our ColdFusion WebSocket module
  • // and subscribe to the "Demo" channel. We are setting the
  • // userID as a custom header that will be passed-through with
  • // each socket request. This way, we can relate the WebSocket
  • // session to the native ColdFusion session (or at least to
  • // data within the native ColdFusion session).
  • var socket = new ColdFusionWebSocket(
  • coldfusionAppName,
  • "demo",
  • {
  • userID: coldfusionUserID
  • }
  • );
  •  
  •  
  • // Listen for published messages on the "Demo" channel.
  • socket.on(
  • "message",
  • "demo",
  • function( event, data ){
  •  
  • console.log( "Published:", data );
  •  
  • }
  • );
  •  
  •  
  • // Listen for publish errors.
  • socket.on(
  • "error",
  • function( event, message ){
  •  
  • console.log( "Error:", message );
  •  
  • }
  • );
  •  
  •  
  • }
  • );

Once a WebSocket connection is established with this client, the client subscribes to the "demo" channel. During this subscription request, the userID value is passed to the server as a custom header. This request causes the invocation of the server-side pseudo-event, onWSSessionStart(), where we'll persist the userID in the WebSocket session. This userID is then used to filter WebSocket message broadcasting back to the target user.

Right now, I'm simply trying to wrap my head around the mechanics of WebSocket-based communication. This does not mean that I believe this approach to be the one true way of filtering message broadcasts. So far, however, it's the only working solution that I've come up with. As I start to build non-trivial examples, I'm sure there will be much more to consider.

All of this code is available on my ColdFusion 10 WebSocket module GitHub repository.




Reader Comments

Awesome post Ben! Question about the message push: Would there be a way for you to validate that the user received the message? IE: "Message received by Sara today at 18:35."

Reply to this Comment

@Brian,

Hmm, really interesting question. I don't know if it's possible. Since the server isn't really sending a single-user communication (it's simply filtering the list of "subscribers"), I am not sure that this information is available.

@All,

Sagar Ganatra (of Adobe), wrote up this piece on filtering request/response WebSocket messages:

http://www.sagarganatra.com/2012/03/coldfusion-10-using-filtercriteria-in.html

I'm gonna see if I can get this approach working in this kind of a demo.

Reply to this Comment

Post A Comment

You — Get Out Of My Dreams, Get Into My Comments
Live in the Now
Oops!
Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.