Skip to main content
Ben Nadel at CF Summit West 2024 (Las Vegas) with: Steven Benjamin
Ben Nadel at CF Summit West 2024 (Las Vegas) with: Steven Benjamin

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

By
Published in Comments (3)

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.

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

Reader Comments

11 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."

15,848 Comments

@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.

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