Building a multiroom chat application - Node fundamentals - Node.js in Action (2014)

Node.js in Action (2014)

Part 1. Node fundamentals

Chapter 2. Building a multiroom chat application

This chapter covers

· A first look at various Node components

· A sample real-time application using Node

· Server and client-side interaction

In chapter 1, you learned how asynchronous development using Node differs from conventional synchronous development. In this chapter, we’ll take a practical look at Node by creating a small event-driven chat application. Don’t worry if the details in this chapter seem over your head; our intent is to demystify Node development and give you a preview of what you’ll be able to do when you’ve completed the book.

This chapter assumes you have experience with web application development, have a basic understanding of HTTP, and are familiar with jQuery. As you move through the chapter, you’ll

· Tour the application to see how it will work

· Review technology requirements and perform the initial application setup

· Serve the application’s HTML, CSS, and client-side JavaScript

· Handle chat-related messaging using Socket.IO

· Use client-side JavaScript for the application’s UI

Let’s start with an application overview—you’ll see what the application will look like and how it’ll behave when it’s completed.

2.1. Application overview

The application you’ll build in this chapter allows users to chat online with each other by entering messages into a simple form, as shown in figure 2.1. A message, once entered, is sent to all other users in the same chat room.

Figure 2.1. Entering a message into the chat application

When starting the application, a user is automatically assigned a guest name, but they can change it by entering a command, as shown in figure 2.2. Chat commands are prefaced with a slash (/).

Figure 2.2. Changing one’s chat name

Similarly, a user can enter a command to create a new chat room (or join it if it already exists), as shown in figure 2.3. When joining or creating a room, the new room name will be shown in the horizontal bar at the top of the chat application. The room will also be included in the list of available rooms to the right of the chat message area.

Figure 2.3. Changing rooms

After the user changes to a new room, the system will confirm the change, as shown in figure 2.4.

Figure 2.4. The results of changing to a new room

While the functionality of this application is deliberately bare-bones, it showcases important components and fundamental techniques needed to create a real-time web application. The application shows how Node can simultaneously serve conventional HTTP data (like static files) and real-time data (chat messages). It also shows how Node applications are organized and how dependencies are managed.

Let’s now look at the technologies needed to implement this application.

2.2. Application requirements and initial setup

The chat application you’ll create needs to do the following:

· Serve static files (such as HTML, CSS, and client-side JavaScript)

· Handle chat-related messaging on the server

· Handle chat-related messaging in the user’s web browser

To serve static files, you’ll use Node’s built-in http module. But when serving files via HTTP, it’s usually not enough to just send the contents of a file; you also should include the type of file being sent. This is done by setting the Content-Type HTTP header with the proper MIME type for the file. To look up these MIME types, you’ll use a third-party module called mime.

MIME types

MIME types are discussed in detail in the Wikipedia article: http://en.wikipedia.org/wiki/MIME.

To handle chat-related messaging, you could poll the server with Ajax. But to make this application as responsive as possible, you’ll avoid using traditional Ajax as a means to send messages. Ajax uses HTTP as a transport mechanism, and HTTP wasn’t designed for real-time communication. When a message is sent using HTTP, a new TCP/IP connection must be used. Opening and closing connections takes time, and the size of the data transfer is larger because HTTP headers are sent on every request. Instead of employing a solution reliant on HTTP, this application will prefer WebSocket (http://en.wikipedia.org/wiki/WebSocket), which was designed as a bidirectional lightweight communications protocol to support real-time communication.

Since only HTML5-compliant browsers, for the most part, support WebSocket, the application will leverage the popular Socket.IO library (http://socket.io/), which provides a number of fallbacks, including the use of Flash, should using WebSocket not be possible. Socket.IO handles fallback functionality transparently, requiring no additional code or configuration. Socket.IO is covered more deeply in chapter 13.

Before we plunge in and actually do the preliminary work of setting up the application’s file structure and dependencies, let’s talk more about how Node lets you simultaneously handle HTTP and WebSocket—one of the reasons why it’s such a good choice for real-time applications.

2.2.1. Serving HTTP and WebSocket

Although this application will avoid the use of Ajax for sending and receiving chat messages, it will still use HTTP to deliver the HTML, CSS, and client-side JavaScript needed to set things up in the user’s browser.

Node can easily handle simultaneously serving HTTP and WebSocket using a single TCP/IP port, as figure 2.5 depicts. Node comes with a module that provides HTTP serving functionality. There are a number of third-party Node modules, such as Express, that build upon Node’s built-in functionality to make web serving even easier. We’ll go into depth about how to use Express to build web applications in chapter 8. In this chapter’s application, however, we’ll stick to the basics.

Figure 2.5. Handling HTTP and WebSocket within a single application

Now that you have a rough idea of the core technologies the application will use, let’s start fleshing it out.

Need to install Node?

If you haven’t already installed Node, please head to appendix A now for instructions for doing so.

2.2.2. Creating the application file structure

To start constructing the tutorial application, create a project directory for it. The main application file will go directly in this directory. You’ll need to add a lib subdirectory, within which some server-side logic will be placed. You’ll need to create a public subdirectory where client-side files will be placed. Within the public subdirectory, create a javascripts subdirectory and a stylesheets directory.

Your directory structure should now look like figure 2.6. Note that while we’ve chosen to organize the files in this particular way in this chapter, Node doesn’t require you to maintain any particular directory structure; application files can be organized in any way that makes sense to you.

Figure 2.6. The skeletal project directory for the chat application

Now that you’ve established a directory structure, you’ll want to specify the application’s dependencies.

An application dependency, in this context, is a module that needs to be installed to provide functionality needed by the application. Let’s say, for example, that you were creating an application that needed to access data stored using a MySQL database. Node doesn’t come with a built-in module that allows access to MySQL, so you’d have to install a third-party module, and this would be considered a dependency.

2.2.3. Specifying dependencies

Although you can create Node applications without formally specifying dependencies, it’s a good habit to take the time to specify them. That way, if you want others to use your application, or you plan on running it in more than one place, it becomes more straightforward to set up.

Application dependencies are specified using a package.json file. This file is always placed in an application’s root directory. A package.json file consists of a JSON expression that follows the CommonJS package descriptor standard (http://wiki.commonjs.org/wiki/Packages/1.0) and describes your application. In a package.json file you can specify many things, but the most important are the name of your application, the version, a description of what the application does, and the application’s dependencies.

Listing 2.1 shows a package descriptor file that describes the functionality and dependencies of the tutorial application. Save this file as package.json in the root directory of the tutorial application.

Listing 2.1. A package descriptor file

If the content of this file seems a bit confusing, don’t worry...you’ll learn more about package.json files in the next chapter and, in depth, in chapter 14.

2.2.4. Installing dependencies

With a package.json file defined, installing your application’s dependencies becomes trivial. The Node Package Manager (npm; https://github.com/isaacs/npm) is a utility that comes bundled with Node. It offers a great deal of functionality, allowing you to easily install third-party Node modules and globally publish any Node modules you yourself create. Another thing it can do is read dependencies from package.json files and install each of them with a single command.

Enter the following command in the root of your tutorial directory:

npm install

If you look in the tutorial directory now, there should be a newly created node_modules directory, as shown in figure 2.7. This directory contains your application’s dependencies.

Figure 2.7. When npm is used to install dependencies, a node_modules directory is created.

With the directory structure established and dependencies installed, you’re ready to start fleshing out the application logic.

2.3. Serving the application’s HTML, CSS, and client-side JavaScript

As outlined earlier, the chat application needs to be capable of doing three basic things:

· Serving static files to the user’s web browser

· Handling chat-related messaging on the server

· Handling chat-related messaging in the user’s web browser

Application logic will be handled by a number of files, some run on the server and some run on the client, as shown in figure 2.8. The JavaScript files run on the client need to be served as static assets, rather than being executed by Node.

Figure 2.8. In this chat application, there’s both client-side and server-side JavaScript logic.

In this section, we’ll tackle the first of those requirements: we’ll define the logic needed to serve static files. We’ll then add the static HTML and CSS files themselves.

2.3.1. Creating a basic static file server

To create a static file server, we’ll leverage some of Node’s built-in functionality as well as the third-party mime add-on for determining a file MIME type.

To start the main application file, create a file named server.js in the root of your project directory and put variable declarations from listing 2.2 in it. These declarations will give you access to Node’s HTTP-related functionality, the ability to interact with the filesystem, functionality related to file paths, and the ability to determine a file’s MIME type. The cache variable will be used to cache file data.

Listing 2.2. Variable declarations

Sending File Data and Error Responses

Next you need to add three helper functions used for serving static HTTP files. The first will handle the sending of 404 errors when a file is requested that doesn’t exist. Add the following helper function to server.js:

function send404(response) {

response.writeHead(404, {'Content-Type': 'text/plain'});

response.write('Error 404: resource not found.');

response.end();

}

The second helper function serves file data. The function first writes the appropriate HTTP headers and then sends the contents of the file. Add the following code to server.js:

function sendFile(response, filePath, fileContents) {

response.writeHead(

200,

{"content-type": mime.lookup(path.basename(filePath))}

);

response.end(fileContents);

}

Accessing memory storage (RAM) is faster than accessing the filesystem. Because of this, it’s common for Node applications to cache frequently used data in memory. Our chat application will cache static files to memory, only reading them from disk the first time they’re accessed. The next helper determines whether or not a file is cached and, if so, serves it. If a file isn’t cached, it’s read from disk and served. If the file doesn’t exist, an HTTP 404 error is returned as a response. Add this helper function to server.js.

Listing 2.3. Serving static files

Creating the HTTP Server

For the HTTP server, an anonymous function is provided as an argument to create-Server, acting as a callback that defines how each HTTP request should be handled. The callback function accepts two arguments: request and response. When the callback executes, the HTTP server will populate these arguments with objects that, respectively, allow you to work out the details of the request and send back a response. You’ll learn about Node’s http module in detail in chapter 4.

Add the logic in the following listing to server.js to create the HTTP server.

Listing 2.4. Logic to create an HTTP server

Starting the HTTP server

You’ve created the HTTP server in the code, but you haven’t added the logic needed to start it. Add the following lines, which start the server, requesting that it listen on TCP/IP port 3000. Port 3000 is an arbitrary choice; any unused port above 1024 would work (a port under 1024 might also work if you’re running Windows or, if in Linux or OS X, you start your application using a privileged user such as “root”).

server.listen(3000, function() {

console.log("Server listening on port 3000.");

});

If you’d like to see what the application can do at this point, you can start the server by entering the following into your command-line prompt:

node server.js

With the server running, visiting http://127.0.0.1:3000 in your web browser will result in the triggering of the 404 error helper, and the “Error 404: resource not found” message will be displayed. Although you’ve added the static file–handling logic, you haven’t added the static files themselves. A point to remember is that a running server can be stopped by using Ctrl-C on the command line.

Next, let’s move on to adding the static files necessary to get the chat application more functional.

2.3.2. Adding the HTML and CSS files

The first static file you’ll add is the base HTML. Create a file in the public directory named index.html and place the HTML in listing 2.5 in it. The HTML will include a CSS file, set up some HTML div elements in which application content will be displayed, and load a number of client-side JavaScript files. The JavaScript files provide client-side Socket.IO functionality, jQuery (for easy DOM manipulation), and a couple of application-specific files providing chat functionality.

Listing 2.5. The HTML for the chat application

The next file you need to add defines the application’s CSS styling. In the public/stylesheets directory, create a file named style.css and put the following CSS code in it.

Listing 2.6. Application CSS

With the HTML and CSS roughed out, run the application and take a look using your web browser. The application should look like figure 2.9.

Figure 2.9. The application in progress

The application isn’t yet functional, but static files are being served and the basic visual layout is established. With that taken care of, let’s move on to defining the server-side chat message dispatching.

2.4. Handling chat-related messaging using Socket.IO

Of the three things we said the app had to do, we’ve already covered the first one, serving static files, and now we’ll tackle the second—handling communication between the browser and server. Modern browsers are capable of using WebSocket to handle communication between the browser and the server. (See the Socket.IO browser support page for details on supported browsers: http://socket.io/#browser-support.)

Socket.IO provides a layer of abstraction over WebSocket and other transports for both Node and client-side JavaScript. Socket.IO will fall back transparently to other WebSocket alternatives if WebSocket isn’t implemented in a web browser while keeping the same API. In this section, we’ll

· Briefly introduce you to Socket.IO and define the Socket.IO functionality you’ll need on the server side

· Add code that sets up a Socket.IO server

· Add code to handle various chat application events

Socket.IO, out of the box, provides virtual channels, so instead of broadcasting every message to every connected user, you can broadcast only to those who have subscribed to a specific channel. This functionality makes implementing chat rooms in your application quite simple, as you’ll see later.

Socket.IO is also a good example of the usefulness of event emitters. Event emitters are, in essence, a handy design pattern for organizing asynchronous logic. You’ll see some event emitter code at work in this chapter, but we’ll go into more detail in the next chapter.

Event emitters

An event emitter is associated with a conceptual resource of some kind and can send and receive messages to and from the resource. The resource could be a connection to a remote server or something more abstract, like a game character. The Johnny-Five project (https://github.com/rwldrn/johnny-five), in fact, leverages Node for robotics applications, using event emitters to control Arduino microcontrollers.

First, we’ll start the server functionality and establish the connection logic. Then we’ll define the functionality you need on the server side.

2.4.1. Setting up the Socket.IO server

To begin, append the following two lines to server.js. The first line loads functionality from a custom Node module that supplies logic to handle Socket.IO-based server-side chat functionality. We’ll define that module next. The next line starts the Socket.IO server functionality, providing it with an already defined HTTP server so it can share the same TCP/IP port:

var chatServer = require('./lib/chat_server');

chatServer.listen(server);

You now need to create a new file, chat_server.js, inside the lib directory. Start this file by adding the following variable declarations. These declarations allow the use of Socket.IO and initialize a number of variables that define chat state:

var socketio = require('socket.io');

var io;

var guestNumber = 1;

var nickNames = {};

var namesUsed = [];

var currentRoom = {};

Establishing connection logic

Next, add the logic in listing 2.7 to define the chat server function listen. This function is invoked in server.js. It starts the Socket.IO server, limits the verbosity of Socket.IO’s logging to the console, and establishes how each incoming connection should be handled.

The connection-handling logic, you’ll notice, calls a number of helper functions that you can now add to chat_server.js.

Listing 2.7. Starting up a Socket.IO server

With the connection handling established, you now need to add the individual helper functions that will handle the application’s needs.

2.4.2. Handling application scenarios and events

The chat application needs to handle the following types of scenarios and events:

· Guest name assignment

· Room-change requests

· Name-change requests

· Sending chat messages

· Room creation

· User disconnection

To handle these you’ll add a number of helper functions.

Assigning guest names

The first helper function you need to add is assignGuestName, which handles the naming of new users. When a user first connects to the chat server, the user is placed in a chat room named Lobby, and assignGuestName is called to assign them a name to distinguish them from other users.

Each guest name is essentially the word Guest followed by a number that increments each time a new user connects. The guest name is stored in the nickNames variable for reference, associated with the internal socket ID. The guest name is also added to namesUsed, a variable in which names that are being used are stored. Add the code in the following listing to lib/chat_server.js to implement this functionality.

Listing 2.8. Assigning a guest name

Joining rooms

The second helper function you’ll need to add to chat_server.js is joinRoom. This function, shown in listing 2.9, handles logic related to a user joining a chat room.

Having a user join a Socket.IO room is simple, requiring only a call to the join method of a socket object. The application then communicates related details to the user and other users in the same room. The application lets the user know what other users are in the room and lets these other users know that the user is now present.

Listing 2.9. Logic related to joining a room

Handling name-change requests

If every user just kept their guest name, it would be hard to remember who’s who. For this reason, the chat application allows the user to request a name change. As figure 2.10 shows, a name change involves the user’s web browser making a request via Socket.IO and then receiving a response indicating success or failure.

Figure 2.10. A name-change request and response

Add the code in the following listing to lib/chat_server.js to define a function that handles requests by users to change their names. From the application’s perspective, the users aren’t allowed to change their names to anything beginning with Guest or to use a name that’s already in use.

Listing 2.10. Logic to handle name-request attempts

Sending chat messages

Now that user nicknames are taken care of, you need to add a function that defines how a chat message sent from a user is handled. Figure 2.11 shows the basic process: the user emits an event indicating the room where the message is to be sent and the message text. The server then relays the message to all other users in the same room.

Figure 2.11. Sending a chat message

Add the following code to lib/chat_server.js. Socket.IO’s broadcast function is used to relay the message:

function handleMessageBroadcasting(socket) {

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

socket.broadcast.to(message.room).emit('message', {

text: nickNames[socket.id] + ': ' + message.text

});

});

}

Creating rooms

Next, you need to add functionality that allows a user to join an existing room or, if it doesn’t yet exist, to create it. Figure 2.12 shows the interaction between the user and the server.

Figure 2.12. Changing to a different chat room

Add the following code to lib/chat_server.js to enable room changing. Note the use of Socket.IO’s leave method:

function handleRoomJoining(socket) {

socket.on('join', function(room) {

socket.leave(currentRoom[socket.id]);

joinRoom(socket, room.newRoom);

});

}

Handling user disconnections

Finally, you need to add the following logic to lib/chat_server.js to remove a user’s nickname from nickNames and namesUsed when the user leaves the chat application:

function handleClientDisconnection(socket) {

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

var nameIndex = namesUsed.indexOf(nickNames[socket.id]);

delete namesUsed[nameIndex];

delete nickNames[socket.id];

});

}

With the server-side components fully defined, you’re now ready to further develop the client-side logic.

2.5. Using client-side JavaScript for the application’s user interface

Now that you’ve added server-side Socket.IO logic to dispatch messages sent from the browser, it’s time to add the client-side JavaScript needed to communicate with the server. Client-side JavaScript is needed to handle the following functionality:

· Sending a user’s messages and name/room change requests to the server

· Displaying other users’ messages and the list of available rooms

Let’s start with the first piece of functionality.

2.5.1. Relaying messages and name/room changes to the server

The first bit of client-side JavaScript you’ll add is a JavaScript prototype object that will process chat commands, send messages, and request room and nickname changes.

In the public/javascripts directory, create a file named chat.js and put the following code in it. This code starts JavaScript’s equivalent of a “class” that takes a single argument, a Socket.IO socket, when instantiated:

var Chat = function(socket) {

this.socket = socket;

};

Next, add the following function to send chat messages:

Chat.prototype.sendMessage = function(room, text) {

var message = {

room: room,

text: text

};

this.socket.emit('message', message);

};

Add the following function to change rooms:

Chat.prototype.changeRoom = function(room) {

this.socket.emit('join', {

newRoom: room

});

};

Finally, add the function defined in the following listing for processing a chat command. Two chat commands are recognized: join for joining or creating a room and nick for changing one’s nickname.

Listing 2.11. Processing chat commands

2.5.2. Showing messages and available rooms in the user interface

Now it’s time to start adding logic that interacts directly with the browser-based user interface using jQuery. The first functionality you’ll add will be to display text data.

In web applications there are, from a security perspective, two types of text data. There’s trusted text data, which consists of text supplied by the application, and there’s untrusted text data, which is text created by or derived from text created by users of the application. Text data from users is considered untrusted because malicious users may intentionally submit text data that includes JavaScript logic in <script> tags. This text data, if displayed unaltered to other users, could cause nasty things to happen, such as redirecting users to another web page. This method of hijacking a web application is called a cross-site scripting (XSS) attack.

The chat application will use two helper functions to display text data. One function will display untrusted text data, and the other function will display trusted text data.

The function divEscapedContentElement will display untrusted text. It will sanitize text by transforming special characters into HTML entities, as shown in figure 2.13, so the browser knows to display them as entered rather than attempting to interpret them as part of an HTML tag.

Figure 2.13. Escaping untrusted content

The function divSystemContentElement will display trusted content created by the system rather than by other users.

In the public/javascripts directory, add a file named chat_ui.js and put the following two helper functions in it:

function divEscapedContentElement(message) {

return $('<div></div>').text(message);

}

function divSystemContentElement(message) {

return $('<div></div>').html('<i>' + message + '</i>');

}

The next function you’ll append to chat_ui.js is for processing user input; it’s detailed in the following listing. If user input begins with the slash (/) character, it’s treated as a chat command. If not, it’s sent to the server as a chat message to be broadcast to other users, and it’s added to the chat room text of the room the user’s currently in.

Listing 2.12. Processing raw user input

Now that you’ve got some helper functions defined, you need to add the logic in the following listing, which is meant to execute when the web page has fully loaded in the user’s browser. This code handles client-side initiation of Socket.IO event handling.

Listing 2.13. Client-side application initialization logic

To finish the application off, add the final CSS styling code in the following listing to the public/stylesheets/style.css file.

Listing 2.14. Final additions to style.css

#room-list {

float: right;

width: 100px;

height: 300px;

overflow: auto;

}

#room-list div {

border-bottom: 1px solid #eee;

}

#room-list div:hover {

background-color: #ddd;

}

#send-message {

width: 700px;

margin-bottom: 1em;

margin-right: 1em;

}

#help {

font: 10px "Lucida Grande", Helvetica, Arial, sans-serif;

}

With the final code added, try running the application (using node server.js). Your results should look like figure 2.14.

Figure 2.14. The completed chat application

2.6. Summary

You’ve now completed a small real-time web application using Node.js!

You should have a sense of how the application is constructed and what the code is like. If aspects of this example application are still unclear, don’t worry: in the following chapters we’ll go into depth on the techniques and technologies used in this example.

Before you delve into the specifics of Node development, however, you’ll want to learn how to deal with the unique challenges of asynchronous development. The next chapter will teach you essential techniques and tricks that will save you a lot of time and frustration.