Recently, I've been using Pusher to send realtime data to the client over native HTML5 WebSockets. This has been a lot of fun; but, in order to get a real sense of just how good Pusher makes my web development life, I wanted to see what kind of pain I would experience if I tried to use older, realtime techniques like long polling. In long polling, the client makes a request to the server as it normally would; however, rather than simply responding with data and then closing the connection, the server holds the connection open and periodically flushes data to the client whenever it becomes available. The client then monitors this open connection for any updates to the responseText buffer. I wanted to see if could implement this kind of communication channel using jQuery and ColdFusion.
I went looking for the pain of long polling and it is certainly pain that I found. Even after hours and hours of going over this code, I simply could not get long polling to work consistently. Some page requests seemed to work fine while others failed miserably. Some chunks of data were flushed to the browser; other chunks of data mysteriously disappeared into the ether of HTTP connections. I present this code not as an example of how things can be done, but rather, as a demonstration of why services like Pusher need to exist!
This experiment was meant to be as simple as I could possibly make it. My Application.cfc ColdFusion framework component set up an application-cached array of messages:
<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, 10, 0 ) /> <cffunction name="onApplicationStart" access="public" returntype="boolean" output="false" hint="I initialize the application."> <!--- Init the application message log. ---> <cfset application.messages =  /> <!--- Return true so the page can process. ---> <cfreturn true /> </cffunction> </cfcomponent>
This array - application.messages - could then be added-to using a separate ColdFusion page:
Add.cfm - Adds Messages To The Queue
<!--- Lock access to the message queue since we adding to it here and clearing it out on the long-poll page. We don't want to get some race condition bewteen these two pages. ---> <cflock name="messageQueue" type="exclusive" timeout="5"> <!--- Create the message. ---> <cfset message = "Hey there, it's now #timeFormat( now(), 'hh:mm:ss TT' )#." /> <!--- Add a message to the collection. ---> <cfset arrayAppend( application.messages, message ) /> <!--- Output the message - for debugging purposes only. ---> <cfoutput> #message# </cfoutput> </cflock>
The client would then make a request to the long polling page, poll.cfm. This long polling page would then hold the connection with the client open. Periodically, the long polling page would check for updates to the cached message queue; and, if there were messages present, it would flush them to the client (as JSON) and clear the message queue.
Poll.cfm - The Long Polling Page
<!--- Set a larger request timeout so we can keep the long polling going, which will allow us to send on-demand data down to the user over the wire. ---> <cfsetting requesttimeout="#(60 * 1)#" enablecfoutputonly="true" /> <!--- At some point this long-poll page is going to timeout. Wrap it in a try/catch so we can cleanly handle that timeout. ---> <cftry> <!--- Just start looping indefinitely. This will allow us to periodically check to see if information needs to be flushed to the client. ---> <cfloop condition="true"> <!--- Lock access to our message queue since we are clearing it here and adding to it on another page. I want to make sure I don't get some odd race condition. ---> <cflock name="messageQueue" type="exclusive" timeout="5"> <!--- Check to see if we have any messages that have yet to be flushed to the client. ---> <cfif arrayLen( application.messages )> <!--- Output the serialized data to the client. Be sure to include are data chunk delimiter so the client knows how to parse valid JSON packets. ---> <cfoutput> #serializeJSON( application.messages )# </cfoutput> <!--- Clear the message queue so we don't flush duplicate entries to the client. ---> <cfset application.messages =  /> </cfif> <!--- Add a line break so we always have something to flush to the client. ---> <cfoutput>::DATA::#chr( 13 )##chr( 10 )#</cfoutput> <!--- Flush the content to the client. ---> <cfflush interval="1" /> </cflock> <!--- Allow the thread to sleep for a moment so we don't constantly hammer the client with flushing. Alos, this gives the server a small rest and provides an opportunity for data to actually change on the server. ---> <cfthread action="sleep" duration="250" /> </cfloop> <!--- Catch the page timeout. ---> <cfcatch> <!--- Simply abort - there's nothing that we can do at this point. The client will have to make a subsequent request for another long-poll connection. ---> <cfabort /> </cfcatch> </cftry>
As you can see, the long polling page enters an indefinite loop - this is how it maintains the connection with the client. Inside each loop iteration, it checks for queued messages and then sleeps the request for 250 milliseconds. If it finds queued messages, it serializes them as JSON and flushes them over the connection to the client. It then becomes the client's responsibility (as you'll see below) to monitor the connection and parse individual data flushes as valid JSON packets.
If you watch the video above, you can see that this approach is really hit or miss. Sometimes, it appears to work nicely; other times, it's a complete failure. But that's OK - that is exactly what I wanted this experiment to be; I wanted to see how the old-school approaches like long polling result in pain such that I might further appreciate the ease and power provided by HTML5 WebSockets. I know that this was my first time trying long polling; and, I am sure that professional solutions like BlazeDS are able to create consistent functionality with long polling. But, with the realtime usability that services like Pusher provides, I am not sure at this point why I would ever want to use long polling.
Want to use code from this post? Check out the license.