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 cf.Objective() 2014 (Bloomington, MN) with:

Using Socket Gateways To Communicate Between ColdFusion And Node.js

By Ben Nadel on

For the past week or two, I've been really trying to get my feet wet with Node.js - a server-side Javascript runtime environment. It's been a lot of fun; but, the question in the back of my mind has always been - OK, now how do I get this to talk to ColdFusion? After all, using Node.js without ColdFusion is like buying a really sexy pair of sweatpants for your girlfriend and then never letting her wear them - it would just be sad. Unfortunately, ColdFusion and Node.js exist in two completely different containers: ColdFusion in its Java / JRUN environment and Node.js in its V8 Javascript engine. Without direct communication available, I decided to explore some sort of messaging intermediary. And, since ColdFusion comes with and handful of example Gateways, I figured this would be a perfect time to explore the ColdFusion Socket Gateway.

 
 
 
 
 
 
 
 
 
 

From what I can understand, a Socket is just an end-point in a bi-directional communication link between two systems (a server and a client) that agree to communicate over a given IP address and port number. Honestly, my conceptual understanding doesn't go much deeper than that. But, what I do know is that ColdFusion provides us with the ability to create Socket-based Gateways that provide a communication bridge between our ColdFusion applications and external systems over ports other than 80 (the standard HTTP port). In this demo, we're going to use one of these ColdFusion Socket Gateways to allow quasi-bi-directional communication between a ColdFusion server and a Node.js server (located on the same machine).

 
 
 
 
 
 
ColdFusion and Node.js applications communication over Socket connections on the same machine using ColdFusion Socket Gateway. 
 
 
 

Ok, socket to me, baby!

The ColdFusion Side Of The Socket Gateway

I've never used a ColdFusion Socket Gateway before (or any ColdFusion Gateway for that matter); so, I have some good news and some bad news. The bad news is that the documentation for ColdFusion Socket Gateways is extremely poor. I literally had to read the documentation from three different releases (CF7, CF8, CF9) of ColdFusion before I could start to make sense of how any of it was put together. The good news is that once I did figure it out, getting the ColdFusion side up and running was actually super easy!

The first thing I had to do was make sure that Event Gateways were enabled for my ColdFusion server:

 
 
 
 
 
 
Enable ColdFusion Event Gateways on your ColdFusion server. 
 
 
 

Then, I had to create an instance of the ColdFusion Socket Gateway. ColdFusion comes installed with some "example" gateways that are supposedly for demo purposes only. The documentation even explicitly states that these out-of-the-box gateways are not meant to be stable for production; I assume, however, that this is just a legal disclaimer.

 
 
 
 
 
 
Create a ColdFusion Socket Gateway instance using the out-of-the-box Socket gateway Java class. 
 
 
 

To create my ColdFusion Socket Gateway, I selected the predefined, example gateway - "Socket." Then, I had to point the gateway to a listener ColdFusion component and a configuration file. The configuration file contains a single line that defines the port on which the socket will listen:

  • port=4445

NOTE: From what I have read, this configuration file is optional. If excluded, the ColdFusion Socket Gateway will default to using port 4445.

The listener ColdFusion component for a socket gateway needs to define a single method:

  • onIncomingMessage( gatewayEvent ) :: gatewayResponse

The structure of the incoming event and the outgoing response depend on the type of ColdFusion Gateway you are using. For the ColdFusion Socket Gateway, the incoming event looks like this:

 
 
 
 
 
 
ColdFusion Socket Gateway event contains information about the request and the incoming message data. 
 
 
 

The response (return value) of the onIncomingMessage() method is a simple struct that contains two values:

  • message
  • originatorID

The message is the string value you want to return over the socket connection. The originatorID is pulled through (manually) from the incoming event so that the ColdFusion application server knows how to route the response (since multiple clients can connect to the same socket gateway).

While the listener ColdFusion component for the socket gateway is not associated with any HTTP request, it can still exist within a ColdFusion application and act, for the most part, as if it were part of a standard request. You can even take advantage of ColdFusion session management; however, socket-based requests do not use the traditional CFID and CFTOKEN session values. Instead, they use a SessionID that looks something like this (broken onto two lines):

58501018CA0AFC057534BB997E8CA4EF_Socket
_NodeJSSocketGateway_9144478

NOTE: Since there is no HTTP request associated with a socket connection, there is no FORM scope (just like with SOAP requests).

That's about as much as I understand about ColdFusion gateways at this time; so, rather than trying to dig deeper on that topic, let's actually put what we know so far into effect. For this demo, I am going to create a very simple ColdFusion application that allows you build up a list of girls names. You can do this either through a ColdFusion page (CFM); or, through a socket-based connection.

First, let's look at the Application.cfc - the ColdFusion framework component:

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, 10, 0 ) />
  • <cfset this.sessionManagement = true />
  • <cfset this.sessionTimeout = createTimeSpan( 0, 0, 10, 0 ) />
  •  
  •  
  • <cffunction
  • name="onApplicationStart"
  • access="public"
  • returntype="boolean"
  • output="false"
  • hint="I initialize the application.">
  •  
  • <!---
  • Create a collection of girls. For this demo, we're going
  • to keep it super SIMPLE. Each girl will be a struct with
  • a name and createdBy property:
  •  
  • name = {string}
  • createdBy = {cfid-cftoken}
  •  
  • NOTE: For socket-based users, the createdBy will have
  • slightly different session tokens (sessionID).
  • --->
  • <cfset application.girls = [] />
  •  
  • <!--- Return true so the application can load. --->
  • <cfreturn true />
  • </cffunction>
  •  
  •  
  • <cffunction
  • name="onRequestStart"
  • access="public"
  • returntype="boolean"
  • output="false"
  • hint="I initialize the application.">
  •  
  • <!--- Check to see if we need to reset the application. --->
  • <cfif structKeyExists( url, "init" )>
  •  
  • <!--- Reset the application manually. --->
  • <cfset this.onApplicationStart() />
  •  
  • <!--- Redirect to page without init flag. --->
  • <cflocation
  • url="#cgi.script_name#"
  • addtoken="false"
  • />
  •  
  • </cfif>
  •  
  • <!--- Return true so the request can load. --->
  • <cfreturn true />
  • </cffunction>
  •  
  • </cfcomponent>

As you can see, nothing special is going on here. We have an application-scoped Girls array that aggregates a collection of structs. Each girl value contains the name of the girl and the session tokens of the user that created it (so that we can see where the request came from).

The index file of our application simply outputs the current collection:

Index.cfm

  • <!DOCTYPE html>
  • <html>
  • <head>
  • <title>ColdFusion And Node.js Socket Gateway Communication</title>
  • </head>
  • <body>
  •  
  • <h1>
  • ColdFusion And Node.js Socket Gateway Communication
  • </h1>
  •  
  • <h2>
  • Girls Collection
  • </h2>
  •  
  • <!--- Output the current collection. --->
  • <cfdump
  • var="#application.girls#"
  • label="Girls"
  • />
  •  
  • <p>
  • <cfoutput>
  • <a href="#cgi.sciprt_name#?init">Reset Application</a>
  • </cfoutput>
  • </p>
  •  
  • </body>
  • </html>

To augment the collection of girls, I have created two endpoints. One HTTP based and one Socket based. Here is the HTTP version:

Create.cfm

  • <!--- Param the URL variables. --->
  • <cfparam name="url.name" type="string" />
  •  
  • <!---
  • Create a new girl with the given name. When doing this, track
  • the user's session information. Since this is a "standard" user,
  • the session with have the standard CFID and CFTOKEN.
  • --->
  • <cfset girl = {
  • name = url.name,
  • createdBy = "#session.cfid#-#session.cftoken#"
  • } />
  •  
  • <!--- Add the girl to the collection. --->
  • <cfset arrayAppend( application.girls, girl ) />
  •  
  •  
  • <!--- ----------------------------------------------------- --->
  • <!--- ----------------------------------------------------- --->
  •  
  •  
  • <!--- Return a success response. --->
  • <cfcontent type="text/html" />
  •  
  • <cfoutput>
  •  
  • Girl Created: #girl.name#<br />
  •  
  • </cfoutput>

As you can see, this simply takes the incoming URL parameter, name, and uses it to create another entry in our Girls collection.

Ok, now let's take a look at our Socket endpoint - our ColdFusion Socket Gateway listener. This ColdFusion component lives in the same directory as our Application.cfc and can take full advantage of its location within a greater application context.

SocketGateway.cfc

  • <cfcomponent
  • output="false"
  • hint="I listen for socket gateway events.">
  •  
  • <cffunction
  • name="onIncomingMessage"
  • access="public"
  • returntype="struct"
  • output="false"
  • hint="I respond to incoming socket messages.">
  •  
  • <!--- Define arguments. --->
  • <cfargument
  • name="event"
  • type="any"
  • required="true"
  • hint="I am the socket gateway event."
  • />
  •  
  • <!--- Define the local scope. --->
  • <cfset var local = {} />
  •  
  • <!---
  • The incoming gateway event object has a standard
  • structure but depends slightly on the type of gateway
  • and the type of event. For this socket connect, it has
  • the following fields:
  •  
  • - cfcMethod: onIncomingMessage
  • - cfcPath: /Sites/.../cf_socket/SocketGateway.cfc
  • - cfcTimeout: 10
  • - data:
  • - message: {message}
  • - gatewayID: NodeJSSocketGateway
  • - gatewayType: Socket
  • - originatorID: 9144478
  •  
  • The "data.message" string is the value sent over the
  • socket connection to our ColdFusion application.
  • --->
  •  
  • <!---
  • Create a new girl with the given name (socket data
  • message). When doing this, track the user's session
  • information. Since this is a "socket gateway" user, the
  • session will have a sessionID in lieu of the standard
  • CFID and CFTOKEN values. This is managed automatically
  • for you by the ColdFusion application server.
  •  
  • NOTE: Since this ColdFusion component is re-instantiated
  • on every request, we have to break encapsulation to
  • access both the session and application scopes.
  • --->
  • <cfset local.girl = {
  • name = arguments.event.data.message,
  • createdBy = session.sessionID
  • } />
  •  
  • <!--- Add the girl to the collection. --->
  • <cfset arrayAppend( application.girls, local.girl ) />
  •  
  • <!---
  • Create a socket gateway return value. This needs the
  • originatorID (so ColdFusion knows where to route the
  • response) and the message to return. The response is
  • handled automatically by the ColdFusion application
  • server.
  • --->
  • <cfset local.response = {
  • message = ("Girl Created: " & local.girl.name),
  • originatorID = arguments.event.originatorID
  • } />
  •  
  • <!--- Return the socket gateway response. --->
  • <cfreturn local.response />
  • </cffunction>
  •  
  • </cfcomponent>

As you can see, although this CFC is being invoked as a gateway, it can still take full advantage of both the Application and Session scopes. With this automagical wiring in place, our socket gateway listener does exactly the same thing as our CFM-based endpoint: it creates a new Girl struct and adds it to the existing collection. The only difference is that it uses requires a different session ID (sessionID vs. CFID/CFTOKEN) and returns a struct rather than writing to the HTTP response buffer.

That's literally all there is on the ColdFusion side; the ColdFusion Event Gateways made this really easy to setup. And, since ColdFusion comes with an example socket gateway, we didn't even have to mess around with any Java code or compiling.

The Node.js Side Of The Socket Gateway

Now that we've seen how to set up our ColdFusion Socket Gateway and integrate it seamlessly with a ColdFusion application, let's take a look at our Node.js code. For this demo, we're going to set up a simple HTTP server that uses incoming HTTP requests in order to initiate socket-based communication with our ColdFusion application.

Before I get into the details, I just wanted to give a huge shout-out to Tim Branyen and Brian White; I had been staring at the Node.js code for about 2 hours without success before these two swooped in and saved the day. Without them, I am sure that this blog post would have never happened.

In Node.js, I believe that each socket connection is built on an instance of the EventEmitter class; that is, it provides hooks into socket-events for asynchronous communication. As with pretty much all I/O (input/output) in Node.js, socket communication is asynchronous. So, rather than sending data over the socket and getting a return value, you have to send data over the socket and "listen" for an asynchronous "data" event.

I'm a little fuzzy on how, or even if you can reliably link an outgoing message to an incoming response. So, for this demo, I am simply using the once() method to one-time bind a new "data" event listener for each outgoing socket request. Please do not take this as a good practice - I'm so far outside my comfort zone right now, I'm surprised that any of this works!

For this demo, we're just grabbing the incoming URL and using the "script-name" as the name of the girl that we want to add.

Server.js

  • // Include the necessary modules.
  • var sys = require( "sys" );
  • var http = require( "http" );
  • var net = require( "net" );
  •  
  •  
  • // ---------------------------------------------------------- //
  • // ---------------------------------------------------------- //
  •  
  •  
  • // Create a socket-based connection to the ColdFusion server. Since
  • // both ColdFusion and Node.js are located on the same machine, we
  • // can just make that connection over localhost (the domain that is
  • // assumed by default if you exclude the domain argument).
  • var coldfusion = net.createConnection( "4445", "localhost" );
  •  
  • // Set the default encoding to be UTF-8. We'll be sending messages
  • // in text format.
  • coldfusion.setEncoding( "utf8" );
  •  
  •  
  • // ---------------------------------------------------------- //
  • // ---------------------------------------------------------- //
  •  
  •  
  • // Create an instance of the HTTP server.
  • var server = http.createServer(
  • function( request, response ){
  •  
  • // Get the name of the Girl we are creating - this will be
  • // everything after the leading slash.
  • var name = request.url.slice( 1 );
  •  
  • // We are now going to write the name to the ColdFusion
  • // socket connection. This will result in an asynchronous
  • // data event. We will need to bind a listenter for this
  • // data event.
  • //
  • // NOTE: I *believe* that sockets use buffers, so it is
  • // probaby a poor assumption to think that this event
  • // listener will always be for teh most recent write.
  • coldfusion.once(
  • "data",
  • function( data ){
  •  
  • // Log the response.
  • console.log( "SOCKET RESPONSE: " + data );
  •  
  • }
  • );
  •  
  • // Write to the socket connection. When doing this, you MUST
  • // append a NewLine character so that the connection knows
  • // when to flush data to the socket gateway.
  • coldfusion.write( name + "\n" );
  •  
  •  
  • // Write some debugging to the Node.js browser.
  • response.writeHead( 200 );
  • response.write( "Sent to ColdFusion: " + name );
  • response.end();
  •  
  • }
  • );
  •  
  • // Point the server to listen to the given port for incoming
  • // requests.
  • server.listen( 8080 );
  •  
  •  
  • // ---------------------------------------------------------- //
  • // ---------------------------------------------------------- //
  •  
  •  
  • // Output a success message.
  • console.log( "Server is now running on port 8080" );

Notice that when we write to the ColdFusion Socket Gateway connection, we are appending a new-line character to our message. This is where Tim Branyen and Brian White saved the day! Apparently, this new-line character is required to delimit the individual socket messages so that the client and server both know where the messages start and end.

Well, that's all there is to it. This socket connection allows the Node.js server to communicate with the ColdFusion server on the same machine. And, with the auto-wiring of ColdFusion gateways, our ColdFusion application can respond to these socket-based requests almost as if they were regular HTTP requests - seamless and without effort.

At this time, I certainly cannot comment on the pros and cons of using socket-based communication over something like a RESTful HTTP API. This is the first time I have ever 1) used a ColdFusion gateway and 2) used an explicit socket connection. For all I know, using a shared database might be the best way of having two systems on the same machine communicate. But, then again, I haven't used a database with Node.js yet.




Reader Comments

@John,

Intercommunication between systems is always a good thing to have! There are things that Node.js is good at; there are things that ColdFusion is good at. If we can find ways to allow for easy intercommunication to happen, then we have the best of all possible worlds.

Reply to this Comment

@John,

... plus, it was great to finally figure out how to use one of the ColdFusion event gateways. Until last night, these had always been a complete mystery to me.

Reply to this Comment

@Ben They have always been a mystery to me as well :). We have about 50 some applications all written in CF. If one of my clients had something in node.js and they need their website to interact with it.... then it's nice to know of at least one way to so it.

You have definitely made me want to mess around with node.js . Now if I just had the time.

Reply to this Comment

@Ben, FYI, a socket is logically a file that's open for input and output at the same time. This is accomplished by having a pair of file descriptors, one opened to read and the other opened to write.

If you open a socket "in the file namespace", it's not just logically a file open for input and output at the same time; it actually is a file open for input and output at the same time. Most people don't know that that's possible. (The CF Socket Gateway doesn't support it either.) But in C, the other end of the socket can be a file.

If you open a socket "in the Internet namespace", the other end of the socket is a process, not a file. The read and write halves of the socket are connected together in a crossover topology, like a null modem: What Process A writes, Process B reads, and what Process B writes, Process A reads. That's the interesting kind of socket and what 99% of all people mean when they say "socket".

The danger with a socket is that Process A's read could "block" (go into a wait state), waiting on Process B to write something, unaware of the fact that Process B was already also in a wait state, waiting on Process A to write something!! It's exactly analogous to a deadlock in a database, except that there's no DBMS to detect the problem and break you out of it!! Very bad news.

That's why other protocols are layered on top of sockets (http, ftp, telnet, ssh, etc). One of the purposes of the higher level protocol is to guarantee that both sides don't go into an infinite wait state waiting on each other. They allow for handshakes (sequences of steps), timeouts and the like, all to make it absolutely clear who waits on whom and optionally, for how long.

So that's what you need to watch out for when you program your own sockets without benefit of a higher level protocol. Don't just blindly do a read that blocks because there's no data available, unless you've somehow guaranteed that it will eventually read something.

That's very bad news too: My experience is that, when ColdFusion goes to a low level network routine, CF code isn't being processed, so the network request can exceed the RequestTimeout and the CF request won't abort. If it was just a long wait, the CF request immediately aborts after returning from the network request (once it starts processing CF code again). So it's good to be careful with sockets.

I wrote a telnet client using sockets in C over 10 years ago. If there's anything you want to know, just ask. I know sockets cold.

Reply to this Comment

@Jeff,

Agreed. I work directly in ColdFusion so often that ways of networking applications together is such a foggy, mythical land :)

@WebManWalking,

a socket is logically a file that's open for input and output at the same time.... I feel like there's a "That's what she said" joke in there somewhere, but I just can't put my finger on it :)

In all seriousness though, awesome explanation. I only partially understanding what you're saying; but, in a weird way, it kind of makes sense. At the very least, I can totally understand what you are saying about deadlocks; I had a somewhat similar (in feeling) experience when I was trying to understand Actors and Messaging in one of the "Seven Languages" chapters. I remember I had an actor that would block its own thread waiting for events... and if no event every came to it, it would just sit there and wait forever. If I recall correctly, I had to do some funny stuff to make sure that kind of stuff didn't happen.

I can see the benefits of using a higher-order protocol. I thought about using some sort of HTTP proxy; but the problem with that is that my ColdFusion server depends on routing by domain-name; as, "localhost:80" wouldn't even know where to go (actually, it would probably go to the core Apache document root, having nothing to do with ColdFusion at all).

Though, perhaps using a full domain name wouldn't be so bad. I just wanted to create something that was local and didn't require going outside the machine for routing. Plus, isn't there a potential problem with "route-back" funkiness.

I'll do some more playing.

Reply to this Comment

P.S.: The fact that there are 2 file descriptors explains another lesser-known fact about Internet sockets: They're full duplex. Processes A and B can both write at the same time and the messages don't "bang into each other".

So the back and forth messages of a handshake are not to prevent simultaneous writes. They're to prevent simultaneous [blocking] reads.

Some peer-to-peer technologies use pairs of sockets (one client and the other server). That seems so wasteful to me. The only legitimate reason for using 2 sockets, IMHO, is to carry 2 different kinds of traffic, such as commands on one socket and data on the other socket. But establishing 2 sockets just for full duplex is a total waste. They're already full duplex.

Reply to this Comment

@WebManWalking,

Networking stuff is deeply fascinating. Or maybe it just seems that way cause I know so little about it :) I always wish I had taken networking lab in school.

In the Node.js, they do talk about File Descriptors (fd) and about "half-open" connections. But I really didn't have any idea what they were talking about.

Reply to this Comment

@Ben:

Looks like we're online at the same time.

If you really want to get into it, Elliotte Rusty Harold's O'Reilly book Java Network Programming is very good. Probably available cheap, since it's so old.

Reply to this Comment

@WebManWalking,

When I first started this post (or the idea for this post), I initially looked at "JMI". I didn't really know what it was, so I figured it might be helpful. I'll try to look at that book - if my head doesn't explode first.

Reply to this Comment

Good to see you hacking on Node!!

Totally hooked on it myself, though I don't think I would have ever gone to the trouble of trying to link it with CF.

Keep it up!!!!

Reply to this Comment

@Ben:

Another fun gateway worth looking into is DirectoryWatcherGateway. You just define onAdd, onChange and onDelete (representing what happened in the directory). Pretty simple. For example, you can set up a directory to be used as a drop box. Whenever someone puts a new file in it, you can process it immediately.

DirectoryWatcherGateway is documented at: ColdFusion Developer's Guide > Using Event Gateways > Using the example event gateways and gateway applications. (Includes example code.)

I seem to recall that, when a CFC is executed as an event gateway, one of the things you're normally used to using doesn't work in that context. But I forget what it was that didn't work. Maybe directory-relative cfinclude or something like that. But whatever it was, there was a simple workaround. I'll check my watchers when I get back to work tomorrow, then repost here with the workaround.

Reply to this Comment

@WebManWalking,

Yeah, I believe that the Directory Watcher Gateway is the only example gateway that is not described as an "example." Meaning, I think they consider that gateway production-ready.

I've never used it, but I can definitely see it being really useful, especially when you are dealing with 3rd party FTP interaction. If you could have a gateway monitor and FTP directory and then automatically process uploaded files (things like CSV files and what not) that would be sweet.

Reply to this Comment

@Ben:

The unusual behavior I encountered with event gateway CFCs is that non-webservice CFCs invoke Application.cfm. I had an Application.cfm file that was protecting the directory (not allowing you in unless you had gone through login). So the directory watcher tried to do a cflocation to the login page! The solution was to put the directory watcher into a subdirectory with a trivial Application.cfm that didn't enforce login. That intercepted the Application.cfm search chain.

Yes, directory watchers are ideal for processing files uploaded by FTP. They can also be used to do something in all instances of a multiserver installation. I have a Server scope variable to put up a system-wide message. (Example: "Server going down at 7PM for maintenance. Please save your work and log off.") I put the message into a special file, the directory watchers of each instance get the onAdd event, read the file and load its string data into the Server scope variable. So all instances start displaying the system-wide message.

All of our servers run multiple instances, so I have created 5 directory watchers so far for doing things across all instances. Very convenient technique.

Reply to this Comment

Just wondering is CF8 able to do the broadcasting task using the socket gateway?

Like the client subscript to publisher and CF server use the socket to update to web client if any data change at server end.

Reply to this Comment

Interesting article, even more interesting comments.

I've been working on a system with CF & Node. The original logic was tied together via a REST interface, which seemed wasteful to me. When I started, CF didn't have decent support for REST or web sockets. I wrote my own REST wrapper.

However; with CF10 support for web sockets & WS plugins for Railo, it made me think web sockets could be a more effective way of communicating between CF & Node. I was thinking along the lines of an application event bus.

I'm curious if anyone else has come to this conclusion & actually implemented it.

Reply to this Comment

I'm working with the socket gateway, and wonder if it is possible to determine the connecting clients IP address? Any suggestions?

Reply to this Comment

Hi all, probably years too late but i'm just diving into all this gateway stuff,
however could somebody explain me very simple what all the differences and use are between

cfajaxproxy, node.js, websockets, gateways?

also as i'm used to work within the mvc/fusebox model and i would like to know if there are some good books to upgrade my knowledge on frameworks, architecture, and html5 stuff related to cf, used today?

Reply to this Comment

This may be a silly question but how do you force a socket to close after you respond? It doesn't seem to be clearly documented anywhere... it seems like it just stays open until the caller disconnects.

Reply to this Comment

@Drew,

I have the same question. I'd like to be able to ensure the socket is closed after the response back. Any luck on this topic?

Jim

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.