Chapter 9

Going Real-Time with WebSockets

The main reason for using Node is real-time web apps, primarily because Node handles large amounts of I/O gracefully. But it doesn't matter how well your server handles I/O if messages can't be relayed to and from the client in real-time. That's where WebSockets come in—to create an open connection between the server and client, where messages can be streamed in both directions.

In this chapter, you learn why the WebSockets protocol was created and about the traditional approaches it replaces. You then dive right in and create a simple WebSockets app. You use Socket.IO both to create the socket server on Node and to connect to this server from the browser Next, you use Socket.IO to create a real-time chat room app. You find out how to relay messages to and from the server and how to communicate this information to the user. Then you add structure to this app with Backbone.js, adding in support for multiple usernames and timestamps in the chat room. Finally, you persist the data in the app with MongoDB.

This chapter ties together all of the concepts in Part III of this book: Node.js, the Express framework and MongoDB. By the end of the chapter, you'll be comfortable creating your own real-time apps in Node.

How WebSockets Work

To understand how WebSockets work, you first need to understand the conventional approach they were designed to replace. WebSockets are a solution to a long history of polling approaches with inadequate responses for real-time data.

Problems with Polling

Say that you're building the front end for a stock ticker web application. When the user hits the page, you first pull in the stock prices using an Ajax request. You then poll the server, say every five seconds, to see if a price change has occurred. However, rather than you having to check the server at regular intervals, wouldn't it be nicer if the server could let you know whenever a change occurs? That's essentially how WebSockets work: They create a two-way communication channel between the server and the client.

A Balancing Act

Without WebSockets, the polling approach forces developers to balance two problems:

• If you don't poll the server often enough, there can be a delay between when the change occurs and when that change is actually relayed to the user. For example, if a stock price changes before the five seconds are up, the user will have to wait for the next tic of the interval before seeing that change.

• On the other hand, if you poll the server too often, it produces unnecessary load for both the server and the client. If nothing has changed in the past five seconds, why slow down the browser with an extra Ajax request? Furthermore, why slow down the server to process the meaningless request?

The worst part is that these two problems feed off each other. Data isn't updating quickly enough, so you refresh more often, which causes unnecessary requests, so you refresh less often (and then you're back to where you started). As you can see, the issue can be tough to balance. Additionally, even if your server can handle the load, you're still going to run into HTTP latency issues. For a real-time solution, you could set the polling interval to something like 100 ms, but chances are it will take at least 200 ms for the complete round trip of the packets over HTTP.

Enter Long Polling

To tackle the problems with basic polling methods, long polling techniques (often referred as Comet applications) were created. One typical approach to long polling entails querying the server on a set interval, but unlike traditional polling approaches, the connection is kept open as the server sends a response only if there is new data. Long polling cuts down on unnecessary load but still doesn't address the fundamental latency issues with HTTP requests. Another major drawback to Comet applications is that they often violate the HTTP 1.1 spec, which states that a client should not maintain more than two simultaneous connections with the server (http://www.w3.org/Protocols/rfc2616/rfc2616-sec8.html#sec8.1.4).

Long polling relies on iframe and MIME hacks, which should be reason enough to avoid it.

The WebSockets Solution

The WebSockets protocol provides an elegant solution for the polling dilemma. WebSockets create an open connection between the server and client, so the server can send content to the browser without being solicited by the browser requesting it. This open connection allows content to stream freely in both directions, much faster than it can over traditional HTTP connections.

Browser Support

Although you're probably chomping at the bit to get started with WebSockets, keep in mind that it's still a relatively new technology. And as such, browser support can be an issue. Fortunately, at the time of this writing, WebSockets are fully supported by all desktop browsers. However, Microsoft IE9 and earlier don't support sockets. Mobile support is also spotty. Although WebSockets are fully supported in most mobile browsers, such as iOS Safari, they aren't supported in Android Browser or Opera Mini. See the support table in Figure 9-1.

Unfortunately, if you want to support older versions of IE and any version of Android Browser, you'll need to use a mixed approach for polling. But don't get discouraged, in the next section, you learn about a sockets API that provides very robust fallbacks for all the browsers you want to support.

9781118524404-fg0901.tif

From caniuse.com

Figure 9-1 WebSockets support table.

Getting Started with Socket.IO

Socket.IO is a JavaScript library for the server and the client that makes WebSocket communication a piece of cake. The API provides useful methods for sending and receiving data using WebSockets. Best of all, Socket.IO automatically provides fallbacks for older browsers, using Adobe, Flash, Socket, and various long-polling techniques. It automatically selects the best transport method at runtime, using the appropriate protocol on the client, while also providing the appropriate API on the server. Combining these fallbacks ensures that you will have the best transport possible on just about any browser (even all the way back to IE 5.5). For more information about Socket.IO's browser support, visit http://socket.io/#browser-support.

In this chapter, I'm assuming that you're using WebSockets with the Express framework and Underscore templates, as I explain in Chapter 7.

Socket.IO on the Server

In this subsection you combine Express with Socket.IO to create your first socket server. You learn how to track the number of connections to your socket, and emit messages to the client. First, install Socket.IO using NPM:

npm install socket.io

Next, make sure Express is installed for your app, and open the app.js file generated by Express. Scroll down to the bottom and replace the http.createServer() call with the following:

var server = http.createServer(app),

io = require('socket.io').listen(server);

server.listen(app.get('port'), function(){

  console.log("Express server listening on port " + app.get('port'));

});

This snippet modifies the HTTP server call so that it can be used to create the socket server.

Next, add the following lines:

var activeClients = 0;

// on connection

io.sockets.on('connection', function(socket) {

  activeClients++;

  io.sockets.emit('message', {clients: activeClients});

  

  // on disconnect

  socket.on('disconnect', function(data) {

    activeClients--;

    io.sockets.emit('message', {clients: activeClients});

  });

});

This is a very simple script that tracks the number of users connected to the socket server. First, an event is set up with io.sockets.on(‘connection') to track when a new user connects. When that event fires, it increments the number of activeClients and then emits a message using the socket connection. Likewise, another event is set up for when this particular user disconnects from the socket. A similar callback is fired, this time reducing the number of activeClients and emitting that data via the socket.

Socket.IO on the Client

You have now set up some very basic events to relay data about how many users are connected to the socket. But you still need to connect to the socket on the client-side and handle that data. This subsection teaches you how to set up a socket listener on the client-side to update the view in real-time.

Before handling the client-side of the socket connection, you need to create some markup for the page to set up the quick view. First, open./routes/index.js and make sure you have a route set up for the root of your app:

exports.index = function(req, res){

  res.render('index', { title: 'Socket.IO' });

};

Then open./views/index.html and add the following Underscore template:

<p>

Connected clients: <span id="client-count">0</span>

</p>

Finally, open ./views/layout.html and add the necessary script tags right before the closing </body> tag:

<!DOCTYPE html>

<html>

<head>

  <title><%=title %></title>

  <link rel="stylesheet" href="/stylesheets/style.css" />

</head>

<body>

<%=body %>

<script src="/socket.io/socket.io.js"></script>

<script src="/javascripts/main.js"></script>

</body>

</html>

Now the view references two scripts: the Socket.IO core and a main.js file you can use for your client-side code. There's no need to add the socket.io.js file to your server, because the Socket.IO module handles that for you.

Make sure to set up Express with Underscore templates as described in Chapter 7. Otherwise, simply convert the templates used in this chapter to the default Jade templates.

Now you're ready to connect to the socket from the browser. Create a file called ./public/javascripts/main.js with the following code:

// connect to the socket on port 3000

var socket = new io.connect(null, {port: 3000});

// when data is received, update the count on the page

socket.on('message', function(data) {

    document.getElementById('client-count').innerHTML = data.clients;

});

Here the script first connects to the WebSocket using http://localhost:3000, since that is where the Node server is located. Once a user connects, it will fire the io.socket.on(‘connection') event on the server-side. Then a callback fires whenever data is received from the socket. The data is used to update the count on the page with the appropriate number of connected users.

Now that the WebSocket functionality for the server and client is created, you're ready to test the app. Fire up your Express server and open http://localhost:3000; you should see Connected lients: 1, as shown in Figure 9-2.

9781118524404-fg0902.eps

Figure 9-2 One connection is being registered over the WebSocket.

Now open up the same page in another tab, and you should see the count increase to 2. Because they are connected using WebSockets, the change will be reflected in real time on both pages. Next, close one of the pages, and you will see the count jump back down to 1. Congratulations, you've built your first WebSockets app!

Building a Real-Time Chat Room

You've gotten your feet wet with WebSockets, but so far the connection-counting app isn't all that impressive. Fortunately, it won't be too hard to take it to the next level. In this section, I explain how you can expand the app to build a chat room that people can use in real time.

Creating the Chat Room View

First, you need to create a new view for the chat room. Modify ./views/index.html as follows:

<div id="chatroom"></div>

<form method="post" class="chatbox">

<input type="text" name="message" />

<input type="submit" value="Send" />

</form>

<p class="connected">

Connected users: <span id="client-count">0</span>

</p>

Here you can see a <div id=”chatroom”></div>, which is used to display the messages, as well as a form to submit a new message to the chat.

Next, add some styling to ./public/stylesheets/style.less:

#chatroom {

  background: #DDD;

  width: 800px;

  height: 300px;

  margin-bottom: 10px;

  overflow-y: scroll;

  

  p {

    padding: 0 15px;

  }

}

.chatbox {

  input[type=text] {

    font-size: 14px;

    width: 700px;

    padding: 5px;

    float: left;

    margin-right: 10px;

  }

  

  input[type=submit] {

    -webkit-appearance: none;

    font-size: 14px;

    border: 1px solid rgba(0,0,0,0.3);

    background-color: tomato;

    color: papayawhip;

    text-shadow: 1px 1px 1px rgba(0,0,0,0.8);

    padding: 5px;

    width: 76px;

  }

}

.connected {

  font-size: 12px;

  color: gray;

}

This adds basic LESS styles.

I'm assuming you're already comfortable with LESS, but if not, check out Appendix A, or simply convert the styles to regular CSS.

The scripting becomes a little more intense from this point, so download jQuery to your ./public/javascripts/ directory. Then include it in ./views/layout.html:

<!DOCTYPE html>

<html>

<head>

  <title><%=title %></title>

  <link rel="stylesheet" href="/stylesheets/style.css" />

</head>

<body>

<%=body %>

<script src="/javascripts/jquery-1.8.3.min.js"></script>

<script src="/socket.io/socket.io.js"></script>

<script src="/javascripts/main.js"></script>

</body>

</html>

Now, if you reload http://localhost:3000, the chat room view appears, as shown in Figure 9-3.

9781118524404-fg0903.eps

Figure 9-3 The chatroom markup and styling have been rendered in the view.

Submitting a Message to the Server

Now that the chat room view is set up, it's time to build the functionality. Starting with the client-side open ./public/javascripts/main.js. The first step is to pass along messages that are submitted into the form:

var socket,

    Chat = {};

Chat.init = function(setup) {

  // connect to the socket

  socket = new io.connect(setup.host);

  // when data is received, update the user count

  socket.on('message', function(data) {

    setup.dom.count.text(data.clients);

  });

  

  // bind submit for chat box

  setup.dom.form.submit(Chat.submit);

};

// submit a new chat to the server

Chat.submit = function(e) {

  e.preventDefault();

    

  // get the text of the input and empty it

  var $message = $(e.target.message),

  text = $message.val();

  

  $message.val(''),

  

  // send the message over the socket

  socket.emit('newchat', {text: text});

};

$(function() {

  // initialize the chat app

  Chat.init({

    host: 'http://localhost:3000',

    dom: {

      count: $('#client-count'),

      form: $('.chatbox')

    }

  });

});

Here, a Chat.init() function is established, which accepts the socket host as well as reference to various DOM elements. The init function first connects to the socket server, then binds a handler, Chat.submit(), to the submit event for the chat form. Chat.submit() starts by preventing the form from being submitted over HTTP and then pulls the text of the message and empties the text input. Next, it sends that text to the socket, by emitting a new event called newchat.

Handling the Message on the Server

Next, set up the server to handle the newchat message. Modify app.js as follows:

var activeClients = 0;

// on connection

io.sockets.on('connection', function(socket) {

  activeClients++;

  io.sockets.emit('message', {clients: activeClients});

  

  // on disconnect

  socket.on('disconnect', function(data) {

    activeClients--;

    io.sockets.emit('message', {clients: activeClients});

  });

  

  // new chat received

  socket.on('newchat', function(data) {

    io.sockets.emit('chat', data);

  });

});

The changes on the server-side are pretty simple, as you can see from the bold section in the code. First, a handler is registered for the newchat event. That handler simply emits the data from that event back over the socket so that all the connected clients can register the new message.

Displaying the New Message on the Clients

Now, when a user submits a new message, it is passed over the socket to the server, which then passes it back over the socket to all the connected clients. You want to catch this new event and then display the message on the client-side, back in main.js:

var socket,

    Chat = {};

Chat.init = function(setup) {

  // connect to the socket

  socket = new io.connect(setup.host);

  // when data is received, update the user count

  socket.on('message', function(data) {

    setup.dom.count.text(data.clients);

  });

  

  // bind submit for chat box

  setup.dom.form.submit(Chat.submit);

  

  // handle new chats

  Chat.$chatroom = setup.dom.room;

  socket.on('chat', Chat.printChat);

};

// submit a new chat to the server

Chat.submit = function(e) {

  e.preventDefault();

  

  // get the text of the input and empty it

  var $message = $(e.target.message),

  text = $message.val();

  

  $message.val(''),

  

  // send the message over the socket

  socket.emit('newchat', {text: text});

};

// print a new chat to the chat room

Chat.printChat = function(data) {

  var $newChat = $('<p>' + data.text + '</p>'),

  

  $newChat.appendTo(Chat.$chatroom);

  

  // scroll to the bottom

  Chat.$chatroom.animate({ scrollTop: Chat.$chatroom.height() }, 100);

};

$(function() {

  // initialize the chat app

  Chat.init({

    host: 'http://localhost:3000',

    dom: {

      count: $('#client-count'),

      form: $('.chatbox'),

      room: $('#chatroom')

    }

  });

});

In the bold sections, this script defines a handler, Chat.printChat, for the chat event. That event simply creates a new <p> with the text and appends that to the chat room <div>. It then scrolls to the bottom of that <div> so that the newest message will always be displayed.

Now fire up http://localhost:3000 in a couple different tabs. When you send a message from any one of them, you will see it displayed on each, as shown in Figure 9-4.

9781118524404-fg0904.eps

Figure 9-4 The chat app is now working, with messages being relayed to all connected users.

Adding Structure with Backbone.js

So far, the basic chat room app is working, but there's still a lot to add to it. However, before getting too deep into additional functionality, it's a good idea to work in some Backbone.

Adding Scripts to Layout.html

First, put the latest versions of Backbone.js and Underscore.js in the ./public/javascripts directory. Then include the scripts in layout.html:

<!DOCTYPE html>

<html>

<head>

  <title><%=title %></title>

  <link rel="stylesheet" href="/stylesheets/style.css" />

</head>

<body>

<%=body %>

<script src="/javascripts/jquery-1.8.3.min.js"></script>

<script src="/javascripts/underscore.min.js"></script>

<script src="/javascripts/backbone.min.js"></script>

<script src="/socket.io/socket.io.js"></script>

<script src="/javascripts/main.js"></script>

</body>

</html>

Models and Collections

When building with Backbone, always start with the models and collections. So add these to main.js:

// Models & Collections

Chat.Message = Backbone.Model.extend({

  defaults: {

    text: ''

  }

});

Chat.Messages = Backbone.Collection.extend({

  model: Chat.Message

});

As you can see, the models are really basic for this app.

Views

Although the models are simple, the views are a little more complex. You may recall that in Chapter 3 you used Backbone to create nested views. A similar approach is covered in this chapter, because you need a main view for the chat room, which will be filled with child views for each message.

I move fairly quickly in this section, but the script almost exactly follows the example of nested views in Chapter 3. Feel free to flip back to Chapter 3 if you need a refresher for anything that's going on here.

Start with the child view, which is pretty basic. First, it builds a wrapper <p> for each message. Next, it defines a render() function that pulls in the text of the message and then appends that to the parent view:

Chat.MessageView = Backbone.View.extend({

  tagName: 'p',

  

  render: function() {

    // add the message text

    this.$el.text(this.model.get('text'));

    

    // append the new message to the parent view

    this.parentView.$el.append(this.$el);

    

    return this;

  }

});

Next, create the parent view:

Chat.MessagesView = Backbone.View.extend({

  el: '#chatroom',

  

  initialize: function() {

    // bind "this" context to the render function

    _.bindAll( this, 'render' );

    

    // register event handlers on the collection

    this.collection.on('change', this.render);

    this.collection.on('add', this.render);

    this.collection.on('remove', this.render);

    

    // render the initial state

    this.render();

  },

  

  render: function() {

    // empty out the wrapper

    this.$el.empty();

    // loop through the messages in the collection

    this.collection.each(function(message) {

      var messageView = new Chat.MessageView({

        model: message

      });

      

      // save a reference to this view within the child view

      messageView.parentView = this;

      

      // render it

      messageView.render();

    }, this);

    

    // scroll to the bottom

    this.$el.animate({ scrollTop: this.$el.height() }, 100);

    

    return this;

  }

});

There's a lot going on in this view:

1. First, it binds itself to the <div id=”chatroom”> in the markup.

2. Then the initialize() function binds the render() function to any changes in the collection. That way, when a message is added, deleted, or modified, it will be reflected in the view.

3. Next, the render() function starts by emptying out the #chatroom wrapper. Then it loops through each of the items in the collection, using the model from each to create a new instance of the child view. Finally, it scrolls to the bottom of the wrapper.

Attaching Backbone to the App

Now the models, collections, and views are set up in Backbone, but they still need to be connected to the app:

Chat.init = function(setup) {

  // connect to the socket

  socket = new io.connect(setup.host);

  // when data is received, update the user count

  socket.on('message', function(data) {

    setup.dom.count.text(data.clients);

  });

  // initialize the collection & views

  Chat.messages = new Chat.Messages();

  

  Chat.messagesView = new Chat.MessagesView({

    collection: Chat.messages

  });

  // bind submit for chat box

  setup.dom.form.submit(Chat.submit);

  

  // handle new chats

  Chat.$chatroom = setup.dom.room;

  socket.on('chat', Chat.addMessage);

};

// add a new message to the chat room

Chat.addMessage = function(data) {

  Chat.messages.add(data);

};

A couple of changes were made here:

• The Chat.init() function creates new instances of both the collection and parent view.

• The Chat.printChat() function is replaced with Chat.addMessage(). The new function simply adds the new message to the collection. It doesn't have to handle the rendering, because that is done automatically with Backbone.

Now, if you reload the page, you see it working as before, except now the page is built with better structure.

Putting all the code together:

var socket,

    Chat = {};

// Models & Collections

Chat.Message = Backbone.Model.extend({

  defaults: {

    text: ''

  }

});

Chat.Messages = Backbone.Collection.extend({

  model: Chat.Message

});

// Views

Chat.MessageView = Backbone.View.extend({

  tagName: 'p',

  

  render: function() {

    // add the message text

    this.$el.text(this.model.get('text'));

    

    // append the new message to the parent view

    this.parentView.$el.append(this.$el);

    

    return this;

  }

});

Chat.MessagesView = Backbone.View.extend({

  el: '#chatroom',

  

  initialize: function() {

    // bind "this" context to the render function

    _.bindAll(this, 'render'),

    

    // add various events for the collection

    this.collection.on('change', this.render);

    this.collection.on('add', this.render);

    this.collection.on('remove', this.render);

    

    // render the initial state

    this.render();

  },

  

  render: function() {

    // empty out the wrapper

    this.$el.empty();

    // loop through the messages in the collection

    this.collection.each(function(message) {

      var messageView = new Chat.MessageView({

        model: message

      });

      

      // save a reference to this view within the child view

      messageView.parentView = this;

      

      // render it

      messageView.render();

    }, this);

    

    // scroll to the bottom

    this.$el.animate({ scrollTop: this.$el.height() }, 100);

    

    return this;

  }

});

// init function

Chat.init = function(setup) {

  // connect to the socket

  socket = new io.connect(setup.host);

  // when data is received, update the user count

  socket.on('message', function(data) {

    setup.dom.count.text(data.clients);

  });

  

  // initialize the collection & views

  Chat.messages = new Chat.Messages();

  

  Chat.messagesView = new Chat.MessagesView({

    collection: Chat.messages

  });

  // bind submit for chat box

  setup.dom.form.submit(Chat.submit);

  

  // handle new chats

  Chat.$chatroom = setup.dom.room;

  socket.on('chat', Chat.addMessage);

};

// submit a new chat to the server

Chat.submit = function(e) {

  e.preventDefault();

  

  // get the text of the input and empty it

  var $message = $(e.target.message),

  text = $message.val();

  

  $message.val(''),

  

  // send the message over the socket

  socket.emit('newchat', {text: text});

};

  

// add a new message to the chat room

Chat.addMessage = function(data) {

  Chat.messages.add(data);

};

$(function() {

  // initialize the chat app

  Chat.init({

    host: 'http://localhost:3000',

    dom: {

      count: $('#client-count'),

      form: $('.chatbox'),

      room: $('#chatroom')

    }

  });

});

Adding Users

Now that the chat room is built with more structure, it's time to add some more features. First, as it currently stands, the chat room isn't all that useful for communication, because users can't tell who is saying what. First, create a prompt for the user to enter his or her name:

Chat.init = function(setup) {

  // connect to the socket

  socket = new io.connect(setup.host);

  // when data is received, update the user count

  socket.on('message', function(data) {

    setup.dom.count.text(data.clients);

  });

  

  // get username

  Chat.username = Chat.getUsername();

  

  // initialize the collection & views

  Chat.messages = new Chat.Messages();

  

  Chat.messagesView = new Chat.MessagesView({

    collection: Chat.messages

  });

  // bind submit for chat box

  setup.dom.form.submit(Chat.submit);

  

  // handle new chats

  Chat.$chatroom = setup.dom.room;

  socket.on('chat', Chat.addMessage);

};

// get the user's name

Chat.getUsername = function() {

  return prompt("What's your name?", '') || 'Anonymous';  

};

Here the Chat.getUsername() function uses a basic DOM prompt to fetch the user's name, which you can see in Figure 9-5.

9781118524404-fg0905.eps

Figure 9-5 This prompt pulls in the user's name. The prompt is pretty basic, so feel free to replace it with a modal dialog or another implementation.

Chat.getUsername()returns what the user entered, or ‘Anonymous' in the event that the user cancels out of the prompt.

Next, modify the Chat.submit() function to pass the user's name along via the WebSocket:

// submit a new chat to the server

Chat.submit = function(e) {

  e.preventDefault();

  

  // get the text of the input and empty it

  var $message = $(e.target.message),

  text = $message.val();

  

  $message.val(''),

  

  // send the message over the socket

  socket.emit('newchat', {

    name: Chat.username,

    text: text

  });

};

Because the server simply relays whatever is sent to the socket back to all the users, you don't have to do anything on the server-side. The last step is to modify the message view to display the username:

Chat.MessageView = Backbone.View.extend({

  tagName: 'p',

  

  template: _.template('<strong><%=name %>:</strong> <%=text %>'),

  

  render: function() {

    // add the message html

    this.$el.html(this.template(this.model.toJSON()));

    

    // append the new message to the parent view

    this.parentView.$el.append(this.$el);

    

    return this;

  }

});

As you can see here, the view is now using an Underscore template to render the message. You'll need all the data in the model, so the model is converted to JSON and passed into the template when it compiles. Now fire up two different clients. As shown in Figure 9-6, the names are now displaying in the chat room.

Consider saving the username in local storage so that the user doesn't have to reenter it. I omitted that from this example so that you can test the app in multiple tabs with different usernames.

9781118524404-fg0906.eps

Figure 9-6 The chatroom has a much better UX now that names are being displayed.

Adding a Timestamp

It's also a good idea to add a timestamp to the messages so that users can see when each message is sent. First, install the Moment.js module, which makes formatting timestamps easy:

npm install moment

With Moment.js, you can create a timestamp for the chat messages, such as:

moment().format('h:mm'),

This method returns a formatted time, such as 14:10 for 2:10 PM. Although you could include this on the client-side, doing so isn't the best idea. Different chat room users may be in different time zones or have incorrect times set on their machines. If you include this function on the client-side, it will pull much less dependable values. So include this function on the server-side and add the timestamp to the message when it's received over the socket:

var moment = require('moment'),

    activeClients = 0;

// on connection

io.sockets.on('connection', function(socket) {

  activeClients++;

  io.sockets.emit('message', {clients: activeClients});

  

  // on disconnect

  socket.on('disconnect', function(data) {

    activeClients--;

    io.sockets.emit('message', {clients: activeClients});

  });

  

  // new chat received

  socket.on('newchat', function(data) {

    data.timestamp = moment().format('h:mm'),

    io.sockets.emit('chat', data);

  });

});

Finally, you just have to add the timestamp to the message template in main.js:

Chat.MessageView = Backbone.View.extend({

  tagName: 'p',

  

  template: _.template('<strong><%=name %> [<%=timestamp %>]:</strong> <%=text %>'),

  

  render: function() {

    // add the message html

    this.$el.html(this.template(this.model.toJSON()));

    

    // append the new message to the parent view

    this.parentView.$el.append(this.$el);

    

    return this;

  }

});

Now the timestamps are displaying in the chat room, as you can see in Figure 9-7.

9781118524404-fg0907.eps

Figure 9-7 The messages in the chatroom now display a timestamp.

Persistence with MongoDB

As the app currently stands, messages are displayed in a real-time stream that is sent to all connected users. But it would be nice to save the messages to a persistence layer so that previous messages can be displayed when a user enters the chat room.

Connecting to MongoDB

First, make sure MongoDB is installed and running. Then install the Native MongoDB Driver module:

npm install mongodb

Then add a require() statement in the top of your Express app.js file:

var express = require('express')

  , routes = require('./routes')

  , user = require('./routes/user')

  , http = require('http')

  , path = require('path')

  , mongodb = require('mongodb'),

Next, connect to the MongoDB server and create a collection for messages on your database:

// connect to mongodb

var db = new mongodb.Db('mydb', new mongodb.Server('localhost', 27017,

{auto_reconnect: true}), {w: 1});

db.open( function(err, conn) {

  db.collection('chatroomMessages', function(err, collection) {

    // init the chatroom

    chatroomInit(collection);

  });

});

The chatroomInit() function is just a callback to make sure the socket functionality doesn't initialize before you're connected to MongoDB. So fold the chat room code into the callback:

var chatroomInit = function(messageCollection) {

  var moment = require('moment'),

      activeClients = 0;

  // on connection

  io.sockets.on('connection', function(socket) {

    activeClients++;

    io.sockets.emit('message', {clients: activeClients});

    // on disconnect

    socket.on('disconnect', function(data) {

      activeClients--;

      io.sockets.emit('message', {clients: activeClients});

    });

    // new chat received

    socket.on('newchat', function(data) {

      data.timestamp = moment().format('h:mm'),

      io.sockets.emit('chat', data);

    });

  });

};

Saving Messages on MongoDB

Next, save any new chats that are received to the database by modifying the newchat callback:

// new chat received

socket.on('newchat', function(data) {

  data.timestamp = moment().format('h:mm'),

  io.sockets.emit('chat', data);

  // save the new message to mongodb

  messageCollection.insert(data, function(err, result) {

    console.log(result);

  });

});

Here the script uses the messageCollection that was passed into the chatroomInit() callback to insert the new data into the database. If you restart your Node server and submit a new message, you see the database insertion logged in the console, as shown in this output:

[ { name: 'Anonymous',

    text: 'Test message',

    timestamp: '16:01',

    _id: 50e2274ded94e40000000001 } ]

Loading Messages from MongoDB

Now that messages are being saved in MongoDB, you just need to load those documents when a new user connects to the socket. In the callback for io.sockets.on(‘connection'), add the following code:

// get the last 10 messages from mongodb

messageCollection.find({}, {sort:[['_id', 'desc']], limit: 10}).toArray(function(err, results) {

  // loop through results in reverse order

  var i = results.length;

  while(i--) {

    // send each over the single socket

    socket.emit('chat', results[i]);

  }

});

This script uses the messageCollection from MongoDB to get in the last ten messages. To do so, it sorts by _id in descending order, with a limit of ten documents. However, there are a couple important things to note:

1. Because the documents are requested in descending order, the script has to loop through them in reverse using the while(i--) loop. That way, it can get the latest ten entries, but still display them in the correct order.

2. The script then emits the data over the socket, almost exactly as when it receives a new message to the chatroom. However, instead of using the public socket, io.sockets.emit(), it uses the private socket, socket.emit().

That's crucially important, because you want to relay the saved messages only to the newly connected user. If you were to transmit these messages over the main public socket, all connected users would get a new set of recent messages every time a new user connected.

The script emits the same chat event over the socket, so there's no need to modify anything on the client-side. The script in main.js already handles displaying data that is sent over this socket. However, this approach can create a bit of a bottleneck, since it will emit as many as ten individual messages over the socket. Feel free to modify the client-side script to accept an array of messages if you'd like to optimize this portion a bit.

Closing the Connection

It's a good idea to close the database connection if there are no users actively using the chat room. So add a close call to the disconnect handler:

// on disconnect

socket.on('disconnect', function(data) {

  activeClients--;

  io.sockets.emit('message', {clients: activeClients});

  // if no active users close db connection

  if ( !activeClients ) db.close();

});

Now, when a user disconnects, the script will check the number of connected uses. If there are no activeClients, it closes the connection to MongoDB to save resources. But don't worry; when a new user connects and queries the database, the connection reopens automatically.

Wrapping Up

The chat room script is now complete, but I want to walk through the code one last time. Putting all the client-side code together from main.js:

var socket,

    Chat = {};

// Models & Collections

Chat.Message = Backbone.Model.extend({

  defaults: {

    text: ''

  }

});

Chat.Messages = Backbone.Collection.extend({

  model: Chat.Message

});

// Views

Chat.MessageView = Backbone.View.extend({

  tagName: 'p',

  

  template: _.template('<strong><%=name %> [<%=timestamp %>]:</strong> <%=text %>'),

  

  render: function() {

    // add the message html

    this.$el.html(this.template(this.model.toJSON()));

    

    // append the new message to the parent view

    this.parentView.$el.append(this.$el);

    

    return this;

  }

});

Chat.MessagesView = Backbone.View.extend({

  el: '#chatroom',

  

  initialize: function() {

    // bind "this" context to the render function

    _.bindAll(this, 'render'),

    

    // add various events for the collection

    this.collection.on('change', this.render);

    this.collection.on('add', this.render);

    this.collection.on('remove', this.render);

    

    // render the initial state

    this.render();

  },

  

  render: function() {

    // empty out the wrapper

    this.$el.empty();

    // loop through the messages in the collection

    this.collection.each(function(message) {

      var messageView = new Chat.MessageView({

        model: message

      });

      

      // save a reference to this view within the child view

      messageView.parentView = this;

      

      // render it

      messageView.render();

    }, this);

    

    // scroll to the bottom

    this.$el.animate({ scrollTop: this.$el.height() }, 100);

    

    return this;

  }

});

// init function

Chat.init = function(setup) {

  // connect to the socket

  socket = new io.connect(setup.host);

  // when data is received, update the user count

  socket.on('message', function(data) {

    setup.dom.count.text(data.clients);

  });

  // get username

  Chat.username = Chat.getUsername();

  

  // initialize the collection & views

  Chat.messages = new Chat.Messages();

  

  Chat.messagesView = new Chat.MessagesView({

    collection: Chat.messages

  });

  // bind submit for chat box

  setup.dom.form.submit(Chat.submit);

  

  // handle new chats

  Chat.$chatroom = setup.dom.room;

  socket.on('chat', Chat.addMessage);

};

// submit a new chat to the server

Chat.submit = function(e) {

  e.preventDefault();

  

  // get the text of the input and empty it

  var $message = $(e.target.message),

  text = $message.val();

  

  $message.val(''),

  

  // send the message over the socket

  socket.emit('newchat', {

    name: Chat.username,

    text: text

  });

};

  

// add a new message to the chat room

Chat.addMessage = function(data) {

  Chat.messages.add(data);

};

// get the user's name

Chat.getUsername = function() {

  return prompt("What's your name?", '') || 'Anonymous';

};

$(function() {

  // initialize the chat app

  Chat.init({

    host: 'http://localhost:3000',

    dom: {

      count: $('#client-count'),

      form: $('.chatbox'),

      room: $('#chatroom')

    }

  });

});

To recap what's going on in this script:

1. It starts by creating models and collections for the messages in Backbone.

2. The script then creates views for each message as well as a parent view to display the list of messages. These are bound to changes in the collection so that any data changes automatically render on the page.

3. Chat.init() starts by connecting to the socket, then calls Chat.getUsername(), which allows the user to enter a name into a simple DOM prompt().

4. Then Chat.init() initializes the collection and views in Backbone.

5. Next, the Chat.submit() function handles submission of the chat message form, transmitting the text and username over the WebSocket.

6. The last part of Chat.init() handles new chats that are received over the WebSocket. These are added to the Chat.messages collection, which in turn renders the changes in the view.

7. Finally, everything is started in the main $(function) init, which starts the socket connection and triggers Chat.init().

To recap the server side, here are the relevant parts of app.js:

var server = http.createServer(app),

io = require('socket.io').listen(server);

server.listen(app.get('port'), function(){

  console.log("Express server listening on port " + app.get('port'));

});

// connect to mongodb

var db = new mongodb.Db('mydb', new mongodb.Server('localhost', 27017, {auto_reconnect: true}), {w: 1});

db.open( function(err, conn) {

  db.collection('chatroomMessages', function(err, collection) {

    // init the chatroom

    chatroomInit(collection);

  });

});

var chatroomInit = function(messageCollection) {

  var moment = require('moment'),

      activeClients = 0;

  // on connection

  io.sockets.on('connection', function(socket) {

    activeClients++;

    io.sockets.emit('message', {clients: activeClients});

    // on disconnect

    socket.on('disconnect', function(data) {

      activeClients--;

      io.sockets.emit('message', {clients: activeClients});

    

      // if no active users close db connection

      if ( !activeClients ) db.close();

    });

    

    // pull in the last 10 messages from mongodb

    messageCollection.find({}, {sort:[['_id', 'desc']], limit:

10}).toArray(function(err, results) {

      // loop through results in reverse order

      var i = results.length;

      while(i--) {

        // send each over the single socket

        socket.emit('chat', results[i]);

      }

    });

    

    // new chat received

    socket.on('newchat', function(data) {

      data.timestamp = moment().format('h:mm'),

      io.sockets.emit('chat', data);

    

      // save the new message to mongodb

      messageCollection.insert(data, function(err, result) {

        console.log(result);

      });

    });

  });

};

Now to go over the main points:

1. The server-side code starts by creating the HTTP and socket server.

2. It then connects to a collection on MongoDB, firing the chatroomInit() callback once it connects.

3. In chatroomInit(), it connects to the socket server, emitting a connection message over the socket and applying a handler to the disconnect event.

4. Then it gets the last ten messages from the MongoDB persistence layer, relaying each to the socket using a private connection for the user that just connected.

5. Finally, it sets up a handler for new messages that are sent from the client. It adds a timestamp to each of these and then relays it to all the users over the socket; then it saves the message to a MongoDB document.

Summary

In this chapter, you built a fast, real-time app using Node and WebSockets. You started by learning about traditional polling techniques and why WebSockets were created to replace them. Then you read about the WebSockets API Socket.IO. You used Socket.IO to create your first sockets server on Node and also to communicate with that server from the client-side.

Then you expanded the simple sockets app to create a real-time chat room application. You relayed data to and from the sockets server and displayed that information on the front end. Next, you hooked the chat room app into Backbone, to add a structure for the data, and used that structure to render changes in the browser. You then added a few features to the app, and finally you added a persistence layer using MongoDB.

This chapter wraps up all the server-side JavaScript concepts you learned in Part III. At this point, you should be comfortable enough with Node to create your own server-side applications. You know how to streamline Node development using the Express framework, persist data from Node using MongoDB, and finally how to connect to the Node server using WebSockets.

In the coming chapters, you'll continue to take your JavaScript skills to the next level, creating mobile apps, drawing in HTML5 canvas, and launching your app.

Additional Resources

Documentation

Socket.IO Wiki: https://github.com/learnboost/Socket.IO/wiki Moment.js Docs: http://momentjs.com/docs/

Tutorials

Getting Your Feet Wet with Node.js and Socket.IO: http://thecoffman.com/2011/02/21/getting-your-feet-wet-with-node.js-and-Socket.IO

Node.js & WebSocket. Simple Chat Tutorial: http://martinsikora.com/nodejs-and-websocket-simple-chat-tutorial

Nodechat.js. Using Node.js, Backbone.js, Socket.IO and Redis to Make a Real-Time Chat App: http://fzysqr.com/2011/02/28/nodechat-js-using-node-js-backbone-js-socket-io-and-redis-to-make-a-real-time-chat-app/

General WebSocket Info

Introducing WebSockets: Bringing Sockets to the Web: http://www.html5rocks.com/en/tutorials/websockets/basics

WebSockets Everywhere with Socket.IO: http://howtonode.org/websockets-socketio

WebSockets (MDN): https://developer.mozilla.org/en-US/docs/WebSockets

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset