Building a chat system using Node.js/Socket.IO
This is a simple chat service that uses Node.js on the server side and the wonderful socket.IO library to leverage the HTML5 WebSockets API on modern browsers. Note that we don’t use the core WebSocket API directly, as it is very likely to change. socket.IO wraps the core API and exposes a similar one that can be used instead. Also, this app will probably run fine on a primitive browser as well, as behind the scenes, Socket.IO will use things like AJAX polling as fallback solutions if WebSockets are not natively supported by the browser.
0x00 - Initial setup
Make a directory for the app. Let’s say it’s called websock_chat.
I assume npm is installed already. If not, do
curl http://npmjs.org/install.sh | sh
Now, we need to serve some static files(the main page, the stylesheet and some images precisely). Since node.js is pretty low level, it doesn’t provide such functionality out-of-the-box. But there’s a node package called node-static that’s built to do just that.
Now cd into websock_chat and do
npm install node-static
This should install node-static into a directory called node_modules in the current directory. Next, we need the workhorse, Socket.IO. To install it, do
npm install socket.io
0x01 - The Server
After the initial setup is done, the server can be written. Here’s the server.js file:
var stat = require('node-static'),
http = require('http'),
io = require('socket.io');
exports.start= function(staticpath, ipaddr, port){
var staticServer = new stat.Server(staticpath);
var server = http.createServer(function(request, response){
request.addListener('end', function(){
staticServer.serve(request, response);
});
});
server.listen(port, ipaddr);
var ws = io.listen(server);
ws.sockets.on('connection', function(client){
client.send('<span style="color: red"><server>: Type in a user name...</span>');
var username;
client.on('message', function(message){
if(!username){
username = message;
ws.sockets.send(username + ' has entered.');
return;
}
var bc_msg = "<" + username + ">: " + message;
ws.sockets.send(bc_msg);
});
});
};
On line 6, we create a static-files server to serve files off the local path passed in as the staticpath argument.The arguments ipaddr and port mean what they would normally mean. For example, if, from some other module, after require()-ing our server.js, we do server.start("./client", "127.0.0.1", 4000);, the static files server will serve requests for static files in a directory called client in the current directory. In lines 7 through 11, we create an http server for us, and on line 12, we make the http server listen on the port and internal IP passed in. Line 8 registers a callback for the end event on the request object, which in turn, delegates the request to our static-files server. The http.createServer() method takes a callback as its argument which is called everytime a request hits the server, with proper request and response objects passed in.
Now, on line 14, we create a WebSockets server - an endpoint that implements the WebSockets protocol for the server side, and associate it with the http server we just created.
ws is an object representing a WebSockets server, and a server may be serving multiple clients at a time(concurrency is a big speciality of Node.js) - Those mulitple connection endpoints, or “sockets”, are available to us via the ws.sockets property. On line 15, we set an event handler for the connection event on all the connections associated with the WebSockets server. Note that this does not apply only to the currently active connections - the handler is a property of ws.sockets and hence affects any future connections that might be pushed in to it. So the short story is, every time a client connects to our server, the “connection” event is fired on ws.sockets, and when this happens the callback we passed is called with the details of the client which connected to us.
Whenever we have a new connection to the server, we want to ask the user to type in a nickname. Line 16 does that. Now we know that the next string the user sends after this is to be interpreted as the user’s handle/nick. In JavaScript, when a variable is declared using var like:
var x;
x holds the special JS value undefined, which evaluates to false in a boolean context. Now notice that the callback we passed to the connection handler takes an argument client. This represents the client endpoint that connected to us, and is a stream we can read from as write to. Since, in Node.js, all I/O is asynchronous, we do not do something like this to read from the client stream:
var msg = client.read()Since the JS runtime and hence Node.js are single threaded, this will block the server until the client really sends in something. Not good. So instead of that, we ask the client stream to call a function whenever it sees a message coming, while passing the contents of the message to that function. Whenever there’s a message coming from a client, the
message event is fired on the client stream. So all we need to do is add a handler for this event. Line 19 does that. Notice how the function accepts an argument which will contain the text of the message when this function is called.
Now, the variable username is undefined until we receive the first message from the client, after which username is set to whatever the user sends. When line 20 is hit as a result of any subsequent messages after the first one, username evaluates to true and so the body of the if is not executed, hence we jump to line 25. We want to let all other users logged into chat know what this user typed. So we want to “broadcast” this message to all the active connections. The string will be of the form:
<username>: the-message
Remember, we’re outputting HTML and “<” and “>” have special meanings in HTML. So, we have to use the HTML codes for these symbols, “<” and “>” respectively. So line 25 constructs the message to be sent over all open connections and line 26 does the broadcast magic. ws.sockets, as mentioned earlier, refers to all the active client connections to the server. This object has a method .send() that takes a message and sends it over each of the active connections, essentially “broadcasting” the message to all users connected to the chat server.
The main server code is done. Now we need someone to actually start this server. This is called bootstrapping. Separating your main code and bootstrapping code is good practice. So here’s the index.js file that forms the “entry point” for out chat app on the server side(put it in the same directory as server.js):
var server = require("./server");
server.start("./client", "127.0.0.1", 4000);
As you can probably tell, this bit calls the server’s start() method passing it the path to a directory that’ll hold our HTML and CSS files, that is, “./client” - you can call it anything you want. Also, we pass the IP address to bind to(may seem weird, but remember some machines have multiple network cards installed) and the port to listen on. The server can be now started with:
$ node ./index.js The "sys" module is now called "util". It should have a similar interface. info - socket.io startedKill it with Ctrl+C. That’s about it for the server. Now the client.
0x02 - The Client
Here’s index.html(Place this in a folder called “client” in your project directory - not required, but it’s good to arrange things)
<!DOCTYPE html>
<html>
<head>
<title>WebSockets experiment</title>
<meta charset="utf-8" />
<link rel="stylesheet" href="/css/style.css" />
<link rel="icon" type="image/vnd.microsoft.icon" href="/favicon.ico" />
</head>
<body lang="en">
<section id="main">
<header>
<hgroup>
<h1>Chat using WebSockets</h1>
</hgroup>
<section id="username"></section>
</header>
<div id="messaging">
<section id="sendMessage">
<input type="text" id="messageText" placeholder="message"/>
<button id="sendButton">send</button>
</section>
<section id="messages">
<img src="/ajax-loader.gif" id="loader" alt="Loading..." />
<ul id="messageList">
</ul>
</section>
</div>
<footer>
Yati Sagade(<a href="http://www.twitter.com/yati_itay">@yati_itay</a>) | <a href="http://rand-yapp.rhcloud.com/">Experiments</a>
</footer>
</section>
<script src="/socket.io/socket.io.js"></script>
<script>
function addMessage(msg){
var messages = document.getElementById("messageList");
var m = document.createElement("li");
m.innerHTML = msg;
m.className = "splashcolor";
messages.appendChild(m);
window.setTimeout(function(){m.className = "";}, 1000);
}
document.body.onload = function(){
var loc = document.location.toString();
var sock = io.connect("");
var mt = document.getElementById("messageText");
var sendBn = document.getElementById("sendButton");
mt.disabled = true;
sendBn.disabled = true;
sock.on("connect", function(){
document.getElementById("loader").style.display = "none";
mt.disabled = false;
sendBn.disabled = false;
mt.focus();
sendBn.onclick = onSend;
addMessage("*** Connected to the server.");
});
sock.on("message", function(msg){
addMessage(msg);
});
sock.on("disconnect", function(){
addMessage("*** Disconnected from the server.");
});
function onSend(){
var message = document.getElementById("messageText");
sock.send(message.value);
message.value = '';
message.focus();
};
mt.onfocus = function(){
this.className = 'active';
};
mt.onblur = function(){
this.className = '';
};
};
</script>
</body>
</html>
Now a brief note on static files. I have some CSS (/css/style.css) and an AJAX loader GIF that indicates that the page is loading when a user first opens it. I’ve put the CSS file “style.css” in client/css and the AJAX loader image in client/. Remember we passed the path to this directory “client/” as the staticpath argument when starting the server. The node-static package makes sure any requests for resources that are not handled explicitly by us in our server code, are served from this directory. For example, when the browser requests “http://ourserver/index.html”, since we do not explicitly handle a request for “index.html”, node-static finds a file by that name in the static directory specified, which in this case is “./client/”. If found, it serves this file(find out what happens if the file is not found). Now style.css resides in client/css, so the correct way to access that file is “/css/style.css”. Here’s a tree of my project directory:
{{PROJECT_DIRECTORY}}
├── client
│ ├── ajax-loader.gif
│ ├── css
│ │ └── style.css
│ ├── favicon.ico
│ └── index.html
├── index.js
└── server.js
Enough of diversion now. On to the code. The main action happens in the <div> with an id of “messaging”. It contains two <section> elements, one for the input box and the send button and the other for displaying messages from all users. In the second section(id=”messages”), we show the AJAX loader GIF, as we want the user to see this until we connect to the chat server.
Before Web 2.0, all the communication between a client and a server had to be initiated by the client, and this typically meant submission of forms using HTTP POST method and requesting/refreshing pages and resources using HTTP GET method(Well, there were other methods, but these two were and still are the most common ones). Then came AJAX - using XMLHttpRequest (XHR) objects to make asynchronous requests to servers without refreshing the current page. But the model was basically the same - the client requests data, the server sends it. In our chat app, we want to have the browser display any new messages that are sent to the server. One method(the most common one, used in GMail) is called AJAX polling or Periodic refresh which is essentially sending an AJAX request at certain intervals to fetch the current list of (new) messages. But it would be better if the server could itself somehow “push” the updates to the client, right? There is a solution that makes this possible known as comet or server-push. But HTML5 provides us with a more elegant and most importantly, standard way to achieve this, using the HTML5 WebSockets API. A WebSocket is similar to BSD-UNIX sockets - it is an endpoint representing one end of a connection. It is possible to establish cross-domain connections using WebSockets. Now this API is currently very young, and is subject to change. We can use the API directly if we wanted to, but we’ll go for a more general approach - Socket.IO.
Socket.IO is a library that exposes an API very similar to the WebSockets API, but comes with an advantage of being cross-platform. On browsers that support WebSockets, Socket.IO will use them under the hood, while on older browsers, traditional AJAX polling (or maybe some other solution) will be used. This is transparent to the programmer. If you really want to use bare WebSockets, this and this are good resources. I was suggested strongly against using the bare WebSockets API (until it gets finalized) on IRC.
When writing the server, we had associated a WebSockets server with our HTTP server, right? That makes the client side Socket.IO JavaScript library available at the path “/socket.io/socket.io.js”. So we include that script on line 37. The main function here is document.body.onload. On line 50, we connect to the default address(wherefrom the current HTML page came) and default port(80). Until we actually get connected, we want to keep the user from typing in the input box or clicking the send button. So we disable them. The variable sock holds the return value from io.connect(). This, however, does not mean that we are connected to the server. When we’re connected to the server, the “connect” event will be fired on the socket(JavaScript == async). So you know what we need - a handler for that event. This handler(set on line 56) does these things:
- Enable the input box and the send button
- Bind a function (onSend(), described later) to the “click” event of the send button
- Indicate to the user that we’re connected
When the user types in something and clicks the send button, onSend() is called, as it is the click event handler for the send button. In onSend() we
- Take the text of the input box(line 74) and send it to the server using sock.send() (line75)
- Clear the input box.
- Set focus to the input box so the user can start typing in without having to click in the box first.
0x03 - The End
That’s really all there is to write a WebSockets app. Writing a chat app is how I learned socket programming(on UNIX and Win32) and WebSockets. This particular app is hosted here. I personally think the WebSockets API is wonderful and will transform how rich web apps are written. As a profound example, check this out. I learnt this from this blog, the Socket.IO docs (I needed the latter as the former uses code that no longer works with the current version of Socket.IO). Guys who hang out in #node.js on Freenode are supercool ;)