/ Programming

Building a custom realtime chat module for Drupal 7 (part 2)

If you haven't already read part one, you should start there.

Step 1: Creating a basic Node.js server

As preparation for part 1 of this tutorial series, you should have installed Node.js on your server, along with its helper application npm. If you haven't done so, do that now as we're going to make use of npm now.

Our Node.js server is going to leverage the Socket.io module, which does not come with the stock Node.js installation. For our purposes here, we're going to install it locally within the module directory for our example_chat module.

In a terminal on your server, change into the example_chat module directory and issue the following command:

sudo npm install socket.io

Great, now we've got Socket.io installed and available for our chat server!
If anything went wrong at this step, there should be plenty of information around the net to get you sorted out.

Creating a very basic Node.js server only takes a few lines of code.

Create a new file in your example_chat module directory called chat_server.js with the following contents:

var http = require('http').createServer(handler)
 
http.listen(8080);
console.log('Chatserver listening on port 8080');
 
function handler(req, res) {
  res.writeHead(200);
  res.end('Example Chat Server');
}

Excellent! We've now got a very simple server that listens for connections on port 8080.

Now lets test it. In your terminal type node chat_server.js. You should see a message indicating that the chatserver is listening on port 8080.
In your web browser of choice, navigate to http://example.com:8080 (replacing example.com with your webserver's url of course).
You should see a single line of text in your browser that says "Example Chat Server"

Great, it works!

But, it's not very useful... yet.
Lets make some changes to our code so that it will be useful for our chat server, since right now it's just a simple web server that can only output one line of text. That's not useful.

Open up that chat_server.js file again and change it's contents to the following:

var http = require('http').createServer(handler)
  , io   = require('socket.io').listen(http).set('log level', 1);
 
http.listen(8080);
console.log('Chatserver listening on port 8080');
 
var nicknames = {};
 
function handler(req, res) {
  res.writeHead(200);
  res.end();
}
 
io.sockets.on('connection', function (socket) {
  socket.on('user message', function (msg) {
    socket.broadcast.emit('user message', socket.nickname, msg);
  });
 
  socket.on('nickname', function (nick, fn) {
    var i = 1;
    var orignick = nick;
    while (nicknames[nick]) {
      nick = orignick+i;
      i++;
    }
    fn(nick);
    nicknames[nick] = socket.nickname = nick;
    socket.broadcast.emit('announcement', nick + ' connected');
    io.sockets.emit('nicknames', nicknames);
  });
 
  socket.on('disconnect', function () {
    if (!socket.nickname) return;
 
    delete nicknames[socket.nickname];
    socket.broadcast.emit('announcement', socket.nickname + ' disconnected');
    socket.broadcast.emit('nicknames', nicknames);
  });
 
});

Whew, that's quite a bit more.. let's break it down piece by piece.

The first 12 lines are basically the same as our initial 9 line example, but we've also added the socket.io module and told it to listen to our webserver. We've also added an empty object variable to store nicknames. When users join the chat their name will be added to that list so that we can always see who is actively in our little chat room.

How about the io.sockets.on() business? Well, that's the heart of our chat server.

It consists of three events; user message, nickname, disconnect. We're going to add some additional code to our template in a little while that will cause these events to fire.

But, I'm sure you're curious, so here's what they do:

user message: When this event fires, the chat server takes the incoming message (the msg variable) and kicks it back to all connected users including the nickname of the user who sent the message (socket.nickname)

nickname: This will be the first event fired when a user joins that chat room. Upon joining their Drupal username will be sent to this event handler function, which includes some logic to ensure that if the name is already in the nicknames list, a numeral will be appended until a unique username is obtained, then that unique name is added to the list and assigned to that user for the duration of their chat session. This works similar to how nickname collisions work on irc. If I log into an irc server and have my nick set to willvincent, but me, or someone else is already logged in with that name, my irc client will automatically issue an alternate version until an unused version is found. What this gives us is the ability to open the chat room in multiple browser windows and still be able to use it in each window.

The nickname event handler also fires off an announcement event (which is handled on the client side) announcing the arrival of the new user, and sends out the updated nicknames list to all connected users so the nickname list gets updated in their browser.

disconnect: This event fires automatically when a user is disconnected from the chat server either by closing their browser window, or navigating away from the page. Like the nickname handler, this also fires off an annoucement event announcing the deparation of that user, and sends out the updated nicknames list with that user's name removed from the list.

Step 2: Adding JavaScript to the template

So, we could start our chat_server again, but since it's only going to react to specific events sent through the socket.io sub system, if we open a connection to it in a browser we're not going to see anything. So lets put the missing pieces together so that we can actually try out our chat system.

Some of this code is going to be a little ugly, but since we need to specify a particular port to connect to, rather than trying to have the following javascript be an external file it's easier and works better to just put it into our template.

Open your example_chat.tpl.php file and add the following line to the beginning of the file:

<script src="http://<?php print $_SERVER['SERVER_ADDR'] ?>:8080/socket.io/socket.io.js"></script>

What this does is tells the browser to request the socket.io.js file from our node.js server. Socket.io automatically handles the route for us, so when that request comes in it happily responds and serves up that static file. This file is what lets socket.io do it's magic. It handles determining the transport mechanism to use, and makes all the bi-directional communication with our server work.

Ok, now we need to implement the client-side javascript to handle and call events on our chat server. Immediately following the previously mentioned script tag, add the following to your example_chat.tpl.php file:

<script>
  (function($){
    var myNick = 'me';
    var socket = io.connect('http://<?php print $_SERVER['SERVER_ADDR'] ?>:8080');
 
    socket.on('connect', function () {
      $('#chat').addClass('connected');
    });
 
    socket.on('announcement', function (msg) {
      $('#lines').append($('<p>').append($('<em>').text(msg)));
    });
 
    socket.on('nicknames', function (nicknames) {
    $('#nicknames').empty().append($('<span>Online: </span>'));
      for (var i in nicknames) {
        $('#nicknames').append($('<b>').text(nicknames[i]));
      }
    });
 
    socket.on('user message', message);
    socket.on('reconnect', function () {
      $('#lines').remove();
      message('System', 'Reconnected to the server');
    });
 
    socket.on('reconnecting', function () {
      message('System', 'Attempting to re-connect to the server');
    });
 
    socket.on('error', function (e) {
      message('System', e ? e : 'A unknown error occurred');
    });
 
    function message (from, msg) {
      $('#lines').append($('<p>').append($('<b>').text(from), msg));
    }
 
    $(document).ready(function() {
      $('input#message').focus(function() {
        if ($(this).val() == 'Type your chat messages here...') {
          $(this).val('');
        }
      });
 
      socket.emit('nickname', '<?php print $username ?>', function (nick) {
        if (nick != 'me') {
          myNick = nick;
          return $('#chat').addClass('nickname-set');
        }
      });
 
      $('#send-message').submit(function () {
        message(myNick, $('#message').val());
        socket.emit('user message', $('#message').val());
        clear();
        $('#lines').get(0).scrollTop = 10000000;
        return false;
      });
 
      function clear () {
        $('#message').val('').focus();
      };
 
    });
  })(jQuery);
</script>

Alright! Now our chat will work.. but it's not going to look very nice.

Step 3: Tying up the loose ends

Rather than walk through the css, I've attached the css I'm using, place that file into your example_chat module's directory. Then open your example_chat.module file, and update our page callback function to include the css, as follows:

/**
 * Page callback for /chat menu item.
 */
function example_chat_page() {
  global $user;
  drupal_add_css(drupal_get_path('module', 'example_chat') . '/example_chat.css', 'file');
  return theme('example_chat', array('username' => $user->name));
}

Now, fire up your chat server: node chat_server.js
Enable the module on your Drupal 7 site
Ensure your user role has the access example chat permission
Then point your browser at http://example.com/chat

To really get the full effect you'll want to do this in multiple browser windows.

Building a custom realtime chat module for Drupal 7 (part 2)
Share this