Friday, November 16, 2012

Chat Evolution — Node.js and Socket.IO

Not so long ago, if we wanted to build a simple JavaScript chat we had to work with Ajax, Comet or Flash component in the browser.

Ajax can be a little bad for performance, (in the case of chatting) because it’s based on a request and response. We also needed to hold an interval in the client which asked the server for new messages and this process was repeated again and again.

Comet (also known as reverse Ajax and long polling) is pretty much the same. While using Ajax, we asked the server every time for messages, with Comet we needed to open one connection and wait. This connection remained open in the server for a while until the server received messages for us, then the server responded with the messages and closed the opened connection. The client, when receiving these messages opened a new connection and waited again. One advantage of Comet is the reduction of the request amounts, but still, we needed to open and close many of them for each session (in case the user is active).

And lastly, a Flash component - in this case we placed a small flash component in our page to exchange messages with the JavaScript client, the flash could open a socket to the server and this gave us push communication.
However, we are here to talk about Node.js and Socket.IO, so, basically, the flash component is a good solution but in those days not every device or browser came with flash support.

Node.js and Socket.IO
With node.js and we can enjoy better performance, a bidirectional push communication between a server and a JavaScript client and even the support for all the browsers. can work with several transports in order to support even old browsers like IE6 (RiP(.

In this tutorial, I'll show you how to create a multi chat room server with node.js and and a basic jquery client.

Installing Socket.IO and Express
If you don’t have and/or expressjs node modules installed on your machine, this will be a great time to do it, you can install them with npm:
$ npm install express
$ npm install

Now we are ready to go, let's start with the server...

// creating global parameters and start
// listening to 'port', we are creating an express
// server and then we are binding it with
var express  = require('express'),
    app      = express(),
    server   = require('http').createServer(app),
    io       = require('').listen(server),
    port     = 8080,

    // hash object to save clients data,
    // { socketid: { clientid, nickname }, socketid: { ... } }
    chatClients = new Object();

// listening to port...

// configure express, since this server is
// also a web server, we need to define the
// paths to the static files
app.use("/styles", express.static(__dirname + '/public/styles'));
app.use("/scripts", express.static(__dirname + '/public/scripts'));
app.use("/images", express.static(__dirname + '/public/images'));

// serving the main applicaion file (index.html)
// when a client makes a request to the app root
// (http://localhost:8080/)
app.get('/', function (req, res) {
 res.sendfile(__dirname + '/public/index.html');

// sets the log level of, with
// log level 2 we wont see all the heartbits
// of each socket but only the handshakes and
// disconnections
io.set('log level', 2);

// setting the transports by order, if some client
// is not supporting 'websockets' then the server will
// revert to 'xhr-polling' (like Comet/Long polling).
// for more configurations go to:
io.set('transports', [ 'websocket', 'xhr-polling' ]);

// events, each connection goes through here
// and each event is emited in the client.
// I created a function to handle each event
io.sockets.on('connection', function(socket){
 // after connection, the client sends us the 
 // nickname through the connect event
 socket.on('connect', function(data){
  connect(socket, data);

 // when a client sends a messgae, he emits
 // this event, then the server forwards the
 // message to other clients in the same room
 socket.on('chatmessage', function(data){
  chatmessage(socket, data);
 // client subscribtion to a room
 socket.on('subscribe', function(data){
  subscribe(socket, data);

 // client unsubscribtion from a room
 socket.on('unsubscribe', function(data){
  unsubscribe(socket, data);
 // when a client calls the 'socket.close()'
 // function or closes the browser, this event
 // is built in so we actually dont
 // need to fire it manually
 socket.on('disconnect', function(){

// create a client for the socket
function connect(socket, data){
 //generate clientId
 data.clientId = generateId();

 // save the client to the hash object for
 // quick access, we can save this data on
 // the socket with 'socket.set(key, value)'
 // but the only way to pull it back will be
 // async
 chatClients[] = data;

 // now the client objtec is ready, update
 // the client
 socket.emit('ready', { clientId: data.clientId });
 // auto subscribe the client to the 'lobby'
 subscribe(socket, { room: 'lobby' });

 // sends a list of all active rooms in the
 // server
 socket.emit('roomslist', { rooms: getRooms() });

// when a client disconnect, unsubscribe him from
// the rooms he subscribed to
function disconnect(socket){
 // get a list of rooms for the client
 var rooms = io.sockets.manager.roomClients[];
 // unsubscribe from the rooms
 for(var room in rooms){
  if(room && rooms[room]){
   unsubscribe(socket, { room: room.replace('/','') });

 // client was unsubscribed from the rooms,
 // now we can delete him from the hash object
 delete chatClients[];

// receive chat message from a client and
// send it to the relevant room
function chatmessage(socket, data){
 // by using 'socket.broadcast' we can send/emit
 // a message/event to all other clients except
 // the sender himself'chatmessage', { client: 
           chatClients[], message: data.message, room: });

// subscribe a client to a room
function subscribe(socket, data){
 // get a list of all active rooms
 var rooms = getRooms();

 // check if this room is exist, if not, update all 
 // other clients about this new room
 if(rooms.indexOf('/' + < 0){
  socket.broadcast.emit('addroom', { room: });

 // subscribe the client to the room

 // update all other clients about the online
 // presence
 updatePresence(, socket, 'online');

 // send to the client a list of all subscribed clients
 // in this room
 socket.emit('roomclients', { room:, clients: 
                            getClientsInRoom(, });

// unsubscribe a client from a room, this can be
// occured when a client disconnected from the server
// or he subscribed to another room
function unsubscribe(socket, data){
 // update all other clients about the offline
 // presence
 updatePresence(, socket, 'offline');
 // remove the client from room

 // if this client was the only one in that room
 // we are updating all clients about that the
 // room is destroyed

  // with 'io.sockets' we can contact all the
  // clients that connected to the server
  io.sockets.emit('removeroom', { room: });

// 'io.sockets.manager.rooms' is an object that holds
// the active room names as a key, returning array of
// room names
function getRooms(){
 return Object.keys(io.sockets.manager.rooms);

// get array of clients in a room
function getClientsInRoom(socketId, room){
 // get array of socket ids in this room
 var socketIds = io.sockets.manager.rooms['/' + room];
 var clients = [];
 if(socketIds && socketIds.length > 0){
  socketsCount = socketIds.lenght;
  // push every client to the result array
  for(var i = 0, len = socketIds.length; i < len; i++){
   // check if the socket is not the requesting
   // socket
   if(socketIds[i] != socketId){
 return clients;

// get the amount of clients in aroom
function countClientsInRoom(room){
 // 'io.sockets.manager.rooms' is an object that holds
 // the active room names as a key and an array of
 // all subscribed client socket ids
 if(io.sockets.manager.rooms['/' + room]){
  return io.sockets.manager.rooms['/' + room].length;
 return 0;

// updating all other clients when a client goes
// online or offline. 
function updatePresence(room, socket, state){
 // may add a trailing '/' to the
 // room name so we are clearing it
 room = room.replace('/','');

 // by using 'socket.broadcast' we can send/emit
 // a message/event to all other clients except
 // the sender himself'presence', { client:
                  chatClients[], state: state, room: room });

// unique id generator
function generateId(){
 var S4 = function () {
  return (((1 + Math.random()) * 0x10000) | 
 return (S4() + S4() + "-" + S4() + "-" + S4() + "-" +
                S4() + "-" + S4() + S4() + S4());

// show a message in console
console.log('Chat server is running and listening to port %d...', port);

Don't be afraid of this long code, actually it's not so long if you removes the comment lines :), However, I won't explain it because it's well documented so if you missed something feel free to comment to this post and I will explain...

Now let's see the client side, I will show here only the JavaScript part but if you want to download the full application, see the references at the bottom for a github link and a live demo url.


 // create global app parameters...
 var NICK_MAX_LENGTH = 15,
     ROOM_MAX_LENGTH = 10,
     lockShakeAnimation = false,
     socket = null,
     clientId = null,
     nickname = null,

          // holds the current room we are in
     currentRoom = null,

     // server information
     serverAddress = 'localhost',
     serverDisplayName = 'Server',
     serverDisplayColor = '#1c5380',

     // some templates we going to use in the chat,
     // like message row, client and room, this
     // templates will be rendered with jQuery.tmpl
     tmplt = {
      room: [
   '<li data-roomId="${room}">',
    '<span class="icon"></span> ${room}',
  client: [
   '<li data-clientId="${clientId}" class="cf">',
    '<div class="fl clientName">',
                                '<span class="icon"></span>',
    '<div class="fr composing"></div>',
  message: [
   '<li class="cf">',
    '<div class="fl sender">${sender}: </div>',
                                '<div class="fl text">${text}</div>', 
                                '<div class="fr time">${time}</div>',

 // bind DOM elements like button clicks and keydown
 function bindDOMEvents(){
  $('.chat-input input').on('keydown', function(e){
   var key = e.which || e.keyCode;
   if(key == 13) { handleMessage(); }

  $('.chat-submit button').on('click', function(){

  $('#nickname-popup .input input').on('keydown', function(e){
   var key = e.which || e.keyCode;
   if(key == 13) { handleNickname(); }

  $('#nickname-popup .begin').on('click', function(){
  $('#addroom-popup .input input').on('keydown', function(e){
   var key = e.which || e.keyCode;
   if(key == 13) { createRoom(); }

  $('#addroom-popup .create').on('click', function(){

  $('.big-button-green.start').on('click', function(){
   $('#nickname-popup .input input').val('');'#nickname-popup');
          $('#nickname-popup .input input').focus();

  $('.chat-rooms .title-button').on('click', function(){
   $('#addroom-popup .input input').val('');'#addroom-popup');
          $('#addroom-popup .input input').focus();

  $('.chat-rooms ul').on('scroll', function(){
   $('.chat-rooms ul li.selected').css('top', 

  $('.chat-messages').on('scroll', function(){
   var self = this;
    if($(self).scrollTop() + $(self).height() <
    } else {
   }, 50);

  $('.chat-rooms ul li').live('click', function(){
   var room = $(this).attr('data-roomId');
   if(room != currentRoom){
                                              { room: currentRoom });
    socket.emit('subscribe', { room: room });

 // bind event handlers
 // this events fired in the server
 function bindSocketEvents(){

  // when the connection is made, the server emiting
  // the 'connect' event
  socket.on('connect', function(){
   // firing back the connect event to the server
   // and sending the nickname for the connected client
   socket.emit('connect', { nickname: nickname });
  // after the server created a client for us, the ready event
  // is fired in the server with our clientId, now we can start 
  socket.on('ready', function(data){
   // hiding the 'connecting...' message
   $('.chat-shadow').animate({ 'opacity': 0 }, 200,
    $('.chat input').focus();
   // saving the clientId localy
   clientId = data.clientId;

  // after the initialize, the server sends a list of
  // all the active rooms
  socket.on('roomslist', function(data){
      for(var i = 0, len = data.rooms.length; i < len; i++){
   // in, their is always one default room
   // without a name (empty string), every socket is 
   // automaticaly joined to this room, however, we
   // don't want this room to be displayed in the
   // rooms list
   if(data.rooms[i] != ''){
    addRoom(data.rooms[i], false);

  // when someone sends a message, the sever push it to
  // our client through this event with a relevant data
  socket.on('chatmessage', function(data){
   var nickname = data.client.nickname;
   var message = data.message;
   //display the message in the chat window
   insertMessage(nickname, message, true, false, false);
  // when we subscribes to a room, the server sends a list
  // with the clients in this room
  socket.on('roomclients', function(data){
      // add the room name to the rooms list
      addRoom(, false);

      // set the current room
      // announce a welcome message
                        'Welcome to the room: `' + +
                        '`... enjoy!', true, false, true);
      $('.chat-clients ul').empty();
      // add the clients to the clients list
      addClient({ nickname: nickname, clientId: clientId },
                                   false, true);
      for(var i = 0, len = data.clients.length; i < len; i++){
        addClient(data.clients[i], false);

      // hide connecting to room message message
      $('.chat-shadow').animate({ 'opacity': 0 }, 200,
           $('.chat input').focus();
  // if someone creates a room the server updates us
  // about it
  socket.on('addroom', function(data){
   addRoom(, true);
  // if one of the room is empty from clients, the server,
  // destroys it and updates us
  socket.on('removeroom', function(data){
   removeRoom(, true);
  // with this event the server tells us when a client
  // is connected or disconnected to the current room
  socket.on('presence', function(data){
   if(data.state == 'online'){
    addClient(data.client, true);
   } else if(data.state == 'offline'){
    removeClient(data.client, true);

 // add a room to the rooms list, may add
 // a trailing '/' to the name so we are clearing it
 function addRoom(name, announce){
     // clear the trailing '/'
     name = name.replace('/','');

     // check if the room is not already in the list
     if($('.chat-rooms ul li[data-roomId="' + name + '"]').length == 0){
  $.tmpl(, { room: name })
                                      .appendTo('.chat-rooms ul');
  // if announce is true, show a message about this room
          insertMessage(serverDisplayName, 'The room `' + name + '`
                                   created...', true, false, true);

 // remove a room from the rooms list
 function removeRoom(name, announce){
  $('.chat-rooms ul li[data-roomId="' + name + '"]').remove();
  // if announce is true, show a message about this room
      insertMessage(serverDisplayName, 'The room `' + name +
                                        '` destroyed...', true, false, true);

 // add a client to the clients list
 function addClient(client, announce, isMe){
  var $html = $.tmpl(tmplt.client, client);
  // if this is our client, mark him with color

  // if announce is true, show a message about this client
   insertMessage(serverDisplayName, client.nickname +
                              ' has joined the room...', true, false, true);
  $html.appendTo('.chat-clients ul')

 // remove a client from the clients list
 function removeClient(client, announce){
  $('.chat-clients ul li[data-clientId="' +
                                       client.clientId + '"]').remove();
  // if announce is true, show a message about this room
   insertMessage(serverDisplayName, client.nickname +
                                ' has left the room...', true, false, true);

 // every client can create a new room, when creating one, the client
 // is unsubscribed from the current room and then subscribed to the
 // room he just created, if he trying to create a room with the same
 // name like another room, then the server will subscribe the user
 // to the existing room
 function createRoom(){
     var room = $('#addroom-popup .input input').val().trim();
     if(room && room.length <= ROOM_MAX_LENGTH &&
                              room != currentRoom){
  // show room creating message
                        .html('Creating room: ' + room + '...');
  $('.chat-shadow').animate({ 'opacity': 1 }, 200);
  // unsubscribe from the current room
  socket.emit('unsubscribe', { room: currentRoom });

  // create and subscribe to the new room
  socket.emit('subscribe', { room: room });
     } else {
  shake('#addroom-popup', '#addroom-popup .input input',
                          'shake', 'yellow');
  $('#addroom-popup .input input').val('');

 // sets the current room when the client
 // makes a subscription
 function setCurrentRoom(room){
  currentRoom = room;
  $('.chat-rooms ul li.selected').removeClass('selected');
  $('.chat-rooms ul li[data-roomId="' + room + '"]')

 // save the client nickname and start the chat by
 // calling the 'connect()' function
 function handleNickname(){
  var nick = $('#nickname-popup .input input').val().trim();
  if(nick && nick.length <= NICK_MAX_LENGTH){
      nickname = nick;
  } else {
                        '#nickname-popup .input input', 'shake', 'yellow');
      $('#nickname-popup .input input').val('');

 // handle the client messages
 function handleMessage(){
  var message = $('.chat-input input').val().trim();

      // send the message to the server with the room name
      socket.emit('chatmessage', { message: message,
                                                   room: currentRoom });
      // display the message in the chat window
      insertMessage(nickname, message, true, true);
      $('.chat-input input').val('');
  } else {
      shake('.chat', '.chat input', 'wobble', 'yellow');

 // insert a message to the chat window, this function can be
 // called with some flags
 function insertMessage(sender, message, showTime, isMe, isServer){
  var $html = $.tmpl(tmplt.message, {
   sender: sender,
   text: message,
   time: showTime ? getTime() : ''

  // if isMe is true, mark this message so we can
  // know that this is our message in the chat window

  // if isServer is true, mark this message as a server
  // message
                             .css('color', serverDisplayColor);
  $html.appendTo('.chat-messages ul');
                       scrollTop: $('.chat-messages ul').height() }, 100);

 // return a short time format for the messages
 function getTime(){
     var date = new Date();
     return (date.getHours() < 10 ? '0' + date.getHours().toString() :
                    date.getHours()) + ':' + (date.getMinutes() < 10 ? '0' +
                    date.getMinutes().toString() : date.getMinutes());

 // just for animation
 function shake(container, input, effect, bgColor){
   lockShakeAnimation = true;
    lockShakeAnimation = false;
   }, 1500);
 // after selecting a nickname we call this function
 // in order to init the connection with the server
 function connect(){
  // show connecting message
  $('.chat-shadow .content').html('Connecting...');
  // creating the connection and saving the socket
  socket = io.connect(serverAddress);
  // now that we have the socket we can bind events to it

 // on document ready, bind the DOM elements to events


There are some ways to send a message from the server to the client with, Let's explore some of them:

  • socket.emit - Fire event to the client side which associated to this socket.
    socket.emit('eventName', { data: 'tosend' });
  • socket.broadcast.emit - Like the socket.emit, firing an event to the client but instead of firing the event to the associated socket, the event will be fired in all other connected socket except from the broadcasting one.
    socket.broadcast.emit('eventName', { data: 'tosend'});
  •'roomName').emit - Sometimes we want to broadcast from one socket to all another socket in the same room rather than all the sockets in the server, in this way we can broadcast to all other sockets who joined a specific room'roomName').emit('eventName', {data: 'tosend'});
  • io.sockets.emit - In this way we can fire an event to all the clients that connected to the server.
    io.sockets.emit('eventName', { data: 'tosend'});
  • io.of('/namespaceName').emit - Like rooms, in we can create namespaces, if you want to emit an event to all the clients that connected to a namespace, this is the way.
    io.of('/namespaceName').emit('eventName', { data: 'tosend'});


  • Demo of this chat application
  • Download the source code from github

1 comment: