Ask Ben: Keeping Close Track Of Who Is Currently Online
Hi Ben, I am trying to attempt to have a "current users online" type of functionality for my company's webportal. I am using a application variable such as "application.useronline" which is a list that contains usernames as they login to my portal. As they sign off or when their session times out, a page runs that flushes all their session variables and removes their username from the application.useronline list. The problem with this method is that I have no control over thos users that leave my site and then ultimatly just close their browser without signing off. This keeps their username in the application.useronline list. Is there any way of creating a script that only the application or server runs? Like a javascript timeout function but runs as a global application request instead of per client? If I can get this to work and have an accurate account of who is currently signed on to my portal, then maybe a chat application will be in the works. Any thoughts? Thanks!
I've never had to build a system to keep track of the users currently online, so what I'm about to demonstrate has not been field tested in any way. As such, take it with a grain of salt. It seems to me, the problem you are having is that your session timeout is too large. The closer your session timeout gets to zero, the more accurate your "online user" tracking will get, right? After all, if the session timeout were, say, two minutes, then a user would be considered "offline" after only two minutes of inactivity.
Of course, the smaller the session timeout gets, the less usable an application becomes because it might take more than two minutes for someone to effectively use a page (ex. read an email or an article). And, we don't want a user to be considered "offline" if they are actively sitting on a page, consuming the content. So, how can we keep the session timeout small enough to quickly weed out inactive users, but still keep the system usable for people who have a page open but have not walked away from their computers?
The easy answer is to keep the session timeout small, but prevent improper deactivation of users through a "heart beat." A "heart beat" is an AJAX request (or other type of asynchronous request such as an image load) that a rendered page makes back to the server with the intent of doing nothing more than keeping the user's current session alive. Essentially, it's a way for the current page to ping the server to let it know that the user is still there.
To demonstrate this, I have set up a very simple Application.cfc that keeps track of the currently active session objects:
Application.cfc
<cfcomponent
output="false"
hint="I define the application settings and event handlers.">
<!--- Define the application settings. --->
<cfset this.name = hash( getCurrentTemplatePath() ) />
<cfset this.applicationTimeout = createTimeSpan( 0, 0, 5, 0 ) />
<cfset this.sessionManagement = true />
<!---
Keep the session timeout small. This will allow us to
keep a more accurate account of which session are truly
still active.
NOTE: In order to prevent people from being logged out,
we will need to keep a "heart beat" going on the rendered
page to touch the session and keep it alive.
--->
<cfset this.sessionTimeout = createTimeSpan( 0, 0, 2, 0 ) />
<!--- Define page request settings. --->
<cfsetting showdebugoutput="false" />
<cffunction
name="onApplicationStart"
access="public"
returntype="boolean"
output="false"
hint="I initialize the application.">
<!---
Create a container for our sessions. This will be a
struct keyed by the ID of the session.
--->
<cfset application.activeSessions = {} />
<!--- Return true so the page can be processed. --->
<cfreturn true />
</cffunction>
<cffunction
name="onSessionStart"
access="public"
returntype="void"
output="false"
hint="I initialize the session.">
<!---
Create a UUID for the session. We could have used
the CFID/CFTOKEN for this, but I just felt like
using UUIDs.
--->
<cfset session.uuid = createUUID() />
<!---
Add this session to the application's active
session collection.
--->
<cfset application.activeSessions[ session.uuid ] = session />
<!--- Return out. --->
<cfreturn />
</cffunction>
<cffunction
name="onSessionEnd"
access="public"
returntype="void"
output="false"
hint="I clean up a session.">
<!--- Define arguments. --->
<cfargument
name="session"
type="any"
required="true"
hint="I am the session that is ending."
/>
<cfargument
name="application"
type="any"
required="true"
hint="I am the application scope for the given session."
/>
<!---
Remove the session from the active sessions contained
in the application scope.
--->
<cfset structDelete(
arguments.application.activeSessions,
arguments.session.uuid
) />
<!--- Return out. --->
<cfreturn />
</cffunction>
</cfcomponent>
As you can see, when the session starts, it creates a UUID for itself and then adds itself to the ActiveSessions collection using its UUID as the key. Then, when the session times out and OnSessionEnd() is called, the session object is removed from the ActiveSessions collection. Notice that I am keeping the session timeout very small, allowing for only two minutes of inactivity before the session times out. This will help us keep a more accurate account of who is truly still using the application.
Now, let's take a look at our "heart beat" page, heart_beat.cfm. This is the page that the client (browser) will hit periodically to let the server know that the session is still in use:
heart_beat.cfm
<!---
This is the heart beat page that will be called via AJAX to
keep the user's session alive. We want to keep the processing
here as small as possible.
--->
<!---
Stream back the value "True" to return a succes. By passing
in the value as a binary object using CFContent, we will
reset the buffer and ensure that nothing else but our value
is returned to the calling page.
--->
<cfcontent
type="text/plain"
variable="#ToBinary( ToBase64( 'true' ) )#"
/>
As you can see, this page does almost nothing. And, it's not supposed to do much of anything; all it is meant to do is allow a point of contact for the client such that the application will think the session is still active. And, in fact, we don't want it to really have to do any processing since a heart beat leads to more traffic on the server.
Ok, now that we have our application in place and we see the heart beat code, let's look at a client-side page that pulls it all together:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html>
<head>
<title>Current Users Online Demo In ColdFusion</title>
<script type="text/javascript" src="jquery-1.3.2.min.js"></script>
<script type="text/javascript">
// When the DOM is ready to be interacted with, let's
// set up our heartbeat.
$(function(){
// When setting up the interval for our heartbeat,
// we want it to fire every 90 seconds so that it
// doesn't allow the session to timeout. Because
// AJAX passes back all the same cookies, the server
// will see this as a normal page request.
//
// Save a handle on the interval in our window scope
// so that we can clear it when needbe.
window.heartBeat = setInterval(
function(){
$.get( "heart_beat.cfm" );
},
(90 * 1000)
);
// If someone leaves their window open, we might not
// want their session to stay open indeterminetly. As
// such, let's kill the heart beat after a certain
// amount of time (ex. 20 minutes).
setTimeout(
function(){
// Clear the heart beat.
clearInterval( window.heartBeat );
},
(20 * 60 * 1000)
);
});
</script>
</head>
<body>
<h1>
Current Users Online Demo In ColdFusion
</h1>
<p>
Nothing much to see here.
</p>
</body>
</html>
When the page loads, the first thing we do is set up an interval in which our client will ping the server using the "heart beat." This ping is executed using jQuery's $.get() method which simply makes an AJAX request to the target page, heart_beat.cfm. When setting up the interval, it is important that the duration of the interval be less that the time of the session timeout otherwise, the heart beat will not be effective in keeping the user's session alive.
When I define the interval, I store a reference to it in the window object. I am doing this because eventually, I might want to clear it. And, in fact, in this example, I have decided that after 20 minutes of inactivity, I am going to stop pinging the server (ie. kill the heart beat). This way, if someone walks away from their computer, but leaves the page open, the application won't keep their session alive indefinitely.
By keeping the session timeout small, we give our application the ability to more accurately track the users who are truly still online. However, to keep the application usable for people who are actively consuming content, we need to set up a "heart beat" that allows the client (browser) to let the server know that it's still there. This way, when a user closes their browser and walks away, their session quickly times out and the collection of active sessions is kept up to date. Like I said, this has not been field tested, but I hope it helps in some way.
As a final note, the concept of a heart beat can be used outside of short sessions. I will often use this technique to keep users logged-in while filling out very long forms with open-ended answers (such as with an application form).
Want to use code from this post? Check out the license.
Reader Comments
Check out:
http://whosoncfc.riaforge.org/
There is also a version that uses a DB for storage.
One could make the case that the heartbeat and the session timeout could be completely unrelated -- remove users from the online list when their heartbeat stops for more than a specified amount of time, no matter their session freshness. This would also give you the option of, down the road, giving the users a "go offline" button.
If you want to be really smooth, use some sort of variation on exponential backoff to slow the heartbeat over time. Maybe each time it beats, it adds a second to its wait. Whenever they perform an action, reset the heartbeat to its initial pace. This would give you an ability to see not just online status, but also a measure of their attention.
@Bob,
Thanks for the link. Is there anything we should be looking at in particular for the accuracy aspect?
@Rick,
Cool thinking. I like the idea of a heart beat that gets slower over time. Very interesting.
Nicely done! I've been looking at similar mechanisms for some user analytics tools but rather than returning Binary content I'm simply returning a 204 No Content http header and aborting - keeps the load as light as possible!
Bit of a bike shed difference I know :)
@Ben:
Why not just do:
<cfcontent
type="text/plain"
reset="true"
variable="true"
/>
?
That will reset the buffer w/out any overhead of conversion.
As Rick stated earlier, you can keep the heartbeat completely separate from the session life.
Just keep track of the last page hit in the session scope and then to determine if the user is still "active" (or online) just check the time since the last page hit.
While using a heartbeat definitely makes it more accurate, if that isn't overly important, you could just check for sessions that have activity in the last 5-10 minutes and consider other sessions inactive (or "offline".)
Not nearly as sophisticate, but the sophistication of the heartbeat isn't always necessary.
Have you looked at WhosOnCFC?
http://whosoncfc.kisdigital.com/
Good tip. This is a common problem and a common solution, and you presented it nicely.
If you want to play around more with the session information, check out Terry Palmer's blog entries on using the cold Fusion java session methods.
http://cfteeps.blogspot.com/2008/09/undocumented-goodness.html
http://cfteeps.blogspot.com/2008/09/i-shot-session.html
The article on "I Shot the Session" is a great article. If your application needs the functionality of an admin to kill the session then you can use those methods combined with the WhosOnCfc to handle that.
@Dan,
Good point; there was no real need for the type conversion. I just do that out of habit as I am used to returning JSON objects.
@Justice,
Thanks man.
@Jordan,
Oh cool, I'll take a look at those.
I do it using all appl vars and scrub them of idle users in a global footer like the following
[code]
---this is in application.cfm---
<!--- Users Online --->
<cfif isdefined("Session.User_Name")>
<cfif Len(session.User_Name)>
<cfif StructKeyExists(application.Members_Online, Session.User_Name)>
<cfset application.Members_Online[Session.User_Name] = now()>
<cfelse>
<cfset dummy = structinsert(application.Members_Online, Session.User_Name, now())>
</cfif>
<cfelse>
<cfif StructKeyExists(application.Guests_Online, cgi.REMOTE_ADDR)>
<cfset application.Guests_Online[cgi.REMOTE_ADDR] = now()>
<cfelse>
<cfset dummy = structinsert(application.Guests_Online, cgi.REMOTE_ADDR, now())>
</cfif>
</cfif>
</cfif>
---this is in the global footer of every page---
<cfloop collection="#application.Guests_Online#" item="key">
<cfif datediff('n', application.Guests_Online[key], now()) GTE 10>
<cfset dummy = structdelete(application.Guests_Online, key)>
</cfif>
</cfloop>
<cfloop collection="#application.Members_Online#" item="key">
<cfif datediff('n', application.Members_Online[key], now()) GTE 10>
<cfset dummy = structdelete(application.Members_Online, key)>
</cfif>
</cfloop>
[/code]
Also those vars will need to be defined if empty before the code in application.cfm
<cfif NOT IsDefined("application.Members_Online")>
<cfset application.membersonline = Structnew()>
</cfif>
<cfif NOT IsDefined("application.Guests_Online")>
<cfset application.guestsonline = Structnew()>
</cfif>
@Chewbacca,
Looks good - I think we're attacking the problem in very similar ways.