A Chat Application Using Socket.IO
In this article, we are going to build a simple chat application using Socket.IO and Backbone.js. Socket.IO is aimed at developers who want to start developing highly interactive, real-time web applications, such as chat systems or multiplayer games. Backbone.js will add structucture to our client-side code and make it easy to manage and decouple concerns in our application.
Readers should be familiar with Node.js and Express. Familiarity with Backbone is a plus, as well as Underscore.js, which is used for basic templating.
Introduction
A diagram illustrating the structure of our client side code is shown below. In the middle is a controller, which acts as a bridge between the socket client and view. The controller gets updates from the socket client, and changes the model. Updates are reflected in the view using Backbone bindings.
Client Side
We’ll begin by looking at the client side code. All chat interactions are handled in HomeView
. Let’s start by defining HomeModel
in /public/js/models/main.js
.
var HomeModel = Backbone.Model.extend({
defaults: {
// Backbone collection for users
onlineUsers: new UserCollection(),
// Backbone collection for user chats, initialized with a predefined chat model
userChats: new ChatCollection([
new ChatModel({sender: '', message: 'Chat Server v.1'})
])
},
// method for adding a new user to onlineUsers collection
addUser: function(username) {
this.get('onlineUsers').add(new UserModel({name: username}));
},
// method for removing a user from onlineUsers collection
removeUser: function(username) {
var onlineUsers = this.get('onlineUsers');
var u = onlineUsers.find(function(item) {
return item.get('name') == username;
});
if (u) {
onlineUsers.remove(u);
}
},
// method for adding new chat to userChats collection
addChat: function(chat) {
this.get('userChats').add(new ChatModel({sender: chat.sender, message: chat.message}));
},
});
We use Backbone collections to listen for changes on the collection. The updates on the collections are reflected automatically by our view.
Next, we define our home template inside /public/index.html
.
<script type="text/template" id="home-template">
<div class="row">
<div class="col-md-10">
<div class="panel panel-default">
<div class="panel-heading">Lobby</div>
<div class="panel-body">
<div class="nano">
<div class="content">
<div class="list-group" id="chatList"></div>
</div>
</div>
<form>
<input class="form-control" type="text" id="chatInput"></input>
</form>
</div>
</div>
</div>
<div class="col-md-2">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">Online Users <span class="badge pull-right" id="userCount"></span></h3>
</div>
<div class="panel-body">
<div class="nano">
<div class="content">
<div class="list-group" id="userList"></div>
</div>
</div>
</div>
</div>
</div>
</div>
</script>
The HomeView
is located in /public/js/views/main.js
. The file is relatively long, so it is left to the reader to explore.
Chat Client
Next, let’s define our Socket.IO chat client. It communicates with the server by sending messages to the server and listening for notifications. These notifications trigger events on the event bus to communicate with the controller. The following code is found in /public/js/socketclient.js
.
var ChatClient = function(options) {
// redefine this to avoid conflicts
var self = this;
// app event bus
self.vent = options.vent;
// server hostname replace with your server's hostname eg: http://localhost
self.hostname = 'http://chatfree.herokuapp.com';
// connects to the server
self.connect = function() {
// connect to the host
self.socket = io.connect(self.hostname);
// set responseListeners on the socket
self.setResponseListeners(self.socket);
}
// send login message
self.login = function(name) {
self.socket.emit('login', name);
}
// send chat message
self.chat = function(chat) {
self.socket.emit('chat', chat);
}
self.setResponseListeners = function(socket) {
// handle messages from the server
socket.on('welcome', function(data) {
// request server info
socket.emit('onlineUsers');
self.vent.trigger('loginDone', data);
});
socket.on('loginNameExists', function(data) {
self.vent.trigger('loginNameExists', data);
});
socket.on('loginNameBad', function(data) {
self.vent.trigger('loginNameBad', data);
});
socket.on('onlineUsers', function(data) {
console.log(data);
self.vent.trigger('usersInfo', data);
});
socket.on('userJoined', function(data) {
self.vent.trigger('userJoined', data);
});
socket.on('userLeft', function(data) {
self.vent.trigger('userLeft', data);
});
socket.on('chat', function(data) {
self.vent.trigger('chatReceived', data);
});
}
}
Socket.IO really makes it easy to send/receive messages between the client and server. Here, we use two methods:
socket.emit(message, [callback])
– Used to send messages to the server.socket.on(message, callback)
– Used to receive messages from the server.callback
is invoked on reception.
Below is a diagram showing what our chat protocol looks like:
Main Controller
For the final part on client side, we have our controller, orchestrating between views, models, and the socket client. Place this in /public/js/main.js
var MainController = function() {
var self = this;
// Event Bus for socket client
self.appEventBus = _.extend({}, Backbone.Events);
// Event Bus for Backbone Views
self.viewEventBus = _.extend({}, Backbone.Events);
// initialize function
self.init = function() {
// create a chat client and connect
self.chatClient = new ChatClient({vent: self.appEventBus});
self.chatClient.connect();
// create our views, place login view inside container first.
self.loginModel = new LoginModel();
self.containerModel = new ContainerModel({
viewState: new LoginView({
vent: self.viewEventBus,
model: self.loginModel
})
});
self.containerView = new ContainerView({model: self.containerModel});
self.containerView.render();
};
// View Event Bus Message Handlers
self.viewEventBus.on('login', function(name) {
// socketio login
self.chatClient.login(name);
});
self.viewEventBus.on('chat', function(chat) {
// socketio chat
self.chatClient.chat(chat);
});
// Socket Client Event Bus Message Handlers
// triggered when login success
self.appEventBus.on('loginDone', function() {
self.homeModel = new HomeModel();
self.homeView = new HomeView({vent: self.viewEventBus, model: self.homeModel});
// set viewstate to homeview
self.containerModel.set('viewState', self.homeView);
});
// triggered when login error due to bad name
self.appEventBus.on('loginNameBad', function(name) {
self.loginModel.set('error', 'Invalid Name');
});
// triggered when login error due to already existing name
self.appEventBus.on('loginNameExists', function(name) {
self.loginModel.set('error', 'Name already exists');
});
// triggered when client requests users info
// responds with an array of online users.
self.appEventBus.on('usersInfo', function(data) {
var onlineUsers = self.homeModel.get('onlineUsers');
var users = _.map(data, function(item) {
return new UserModel({name: item});
});
onlineUsers.reset(users);
});
// triggered when a client joins the server
self.appEventBus.on('userJoined', function(username) {
self.homeModel.addUser(username);
self.homeModel.addChat({sender: '', message: username + ' joined room.'});
});
// triggered when a client leaves the server
self.appEventBus.on('userLeft', function(username) {
self.homeModel.removeUser(username);
self.homeModel.addChat({sender: '', message: username + ' left room.'});
});
// triggered when chat receieved
self.appEventBus.on('chatReceived', function(chat) {
self.homeModel.addChat(chat);
});
}
To bootstrap everything, we simply create a MainController
and call it’s init
method, inside /public/js/main.js
:
$(document).ready(function() {
var mainController = new MainController();
mainController.init();
});
That’s it for the client side. If you encounter any errors, Chrome has excellent debugging tools. Use it’s network tab to see if messages are really exchanged.
Server Side
Next, we’ll turn to the server side which is implemented in Node.js, Express, and Socket.IO. Place this code, which implements the Express server component, in /scripts/web.js
:
// requirements
var express = require('express');
var http = require('http');
var socketio = require('socket.io');
var path = require('path');
// routes
var routes = require('../routes/index.js');
var app = express();
// routes middleware
app.use(app.router);
// serve public folder
app.use(express.static(path.join(__dirname, '../public')));
// serve index.html for every path
app.use(routes.index);
// this is how you use socket io with express
var server = http.createServer(app);
var io = socketio.listen(server);
var port = process.env.PORT || 8080;
server.listen(port, function() {
console.log(' - listening on ' + port+ ' ' + __dirname);
});
// require our chatserver
var ChatServer = require('./chatserver');
// initialize a new chat server.
new ChatServer({io: io}).init();
Chat Server
The last part of our application is the chat server. This is responsible for keeping a list of online users, and broadcasting chat messages. The first event that our server will receive on a new client connection is aptly named connection
. connection
events handlers, pass along the socket
that was just established. The socket
handles the following events:
socket.on(message, callback)
–callback
is called when a new message is received.message
can be any type of data, depending on what was sent.socket.on('disconnect', callback)
–callback
is called when the socket disconnects.socket.emit(message, args)
– Sendmessage
over the socket.socket.broadcast.send(message, args)
– Broadcastsmessage
to all sockets except the sender.
Now we’ve seen how to handle sockets, let’s define a user model inside /scripts/chatserver.js
:
// User Model
var User = function(args) {
var self = this;
// Socket field
self.socket = args.socket;
// username field
self.user = args.user;
}
Finally our chat server goes in /scripts/chatserver.js
:
var Server = function(options) {
var self = this;
self.io = options.io;
// users array
self.users = [];
// initialize function
self.init = function() {
// Fired upon a connection
self.io.on('connection', function(socket) {
self.handleConnection(socket);
});
}
// socket handler for an incoming socket
self.handleConnection = function(socket) {
// wait for a login message
socket.on('login', function(username) {
var nameBad = !username || username.length < 3 || username.length > 10;
// check for badname
if (nameBad) {
socket.emit('loginNameBad', username);
return;
}
var nameExists = _.some(self.users, function(item) {
return item.user == username;
});
// check for already existing name
if (nameExists) {
socket.emit('loginNameExists', username);
} else {
// create a new user model
var newUser = new User({ user: username, socket: socket });
// push to users array
self.users.push(newUser);
// set response listeners for the new user
self.setResponseListeners(newUser);
// send welcome message to user
socket.emit('welcome');
// send user joined message to all users
self.io.sockets.emit('userJoined', newUser.user);
}
});
}
// method to set response listeners
self.setResponseListeners = function(user) {
// triggered when a socket disconnects
user.socket.on('disconnect', function() {
// remove the user and send user left message to all sockets
self.users.splice(self.users.indexOf(user), 1);
self.io.sockets.emit('userLeft', user.user);
});
// triggered when socket requests online users
user.socket.on('onlineUsers', function() {
var users = _.map(self.users, function(item) {
return item.user;
});
user.socket.emit('onlineUsers', users);
});
// triggered when socket send a chat message
user.socket.on('chat', function(chat) {
if (chat) {
self.io.sockets.emit('chat', { sender: user.user, message: chat });
}
});
}
}
Conclusion
We’ve seen how to build a simple chat application using Backbone and Socket.IO. There are many Socket.IO features we haven’t covered in this article, such as rooms and namespaces. Socket.IO makes it really easy to exchange messages between clients and a server. We’ve used Node.js for the server side, though Socket.IO has libraries for many other platforms. For more information and resources, check out the Socket.IO wiki.
You can find the source code for this article on GitHub. A live demo is also available on Heroku.