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 inHomeView
. 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.
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 namedconnection
. 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.
/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.Frequently Asked Questions (FAQs) about Chat Application using Socket.io
How can I ensure the security of my chat application using Socket.io?
Security is a crucial aspect of any application, especially when it involves real-time communication. Socket.io does not inherently provide security measures, so it’s up to you to implement them. You can use HTTPS to secure the data transmission between the client and the server. Additionally, you can use JSON Web Tokens (JWT) for user authentication. JWT ensures that the person sending messages is the authenticated user. Remember to validate and sanitize all incoming data to prevent cross-site scripting (XSS) and injection attacks.
Can I use Socket.io with other backend technologies apart from Node.js?
Yes, you can. While Socket.io is primarily designed to work with Node.js, it can also be used with other backend technologies. However, this might require additional libraries or tools to ensure compatibility. For instance, if you’re using Python, you can use Flask-SocketIO. If you’re using PHP, you can use Elephant.io or Ratchet. Remember that using Socket.io with other backend technologies might not provide the same performance or features as with Node.js.
How can I scale my Socket.io chat application for a large number of users?
Scaling a Socket.io application can be challenging due to its real-time nature. However, there are several strategies you can use. One is to use the Redis adapter for Socket.io, which allows you to create multiple instances of your application and share messages between them. Another strategy is to use a load balancer to distribute the load among multiple servers. You can also use clustering in Node.js to take advantage of multi-core systems.
How can I store chat history in my Socket.io chat application?
Storing chat history can be done by integrating a database with your Socket.io application. You can use any type of database, but a NoSQL database like MongoDB is often a good choice for chat applications due to its flexibility and scalability. Whenever a chat message is sent, you can save it to the database with information like the sender, the recipient, the message content, and the timestamp. Then, you can retrieve this chat history whenever it’s needed.
Can I send private messages in a Socket.io chat application?
Yes, you can. Socket.io provides a feature called ‘rooms’ that you can use to implement private messaging. When a user wants to send a private message, you can create a unique room for the two users, and only they will receive the messages sent in this room. This requires keeping track of which users are in which rooms, which can be done using an object or a database.
How can I handle disconnections in my Socket.io chat application?
Socket.io automatically handles reconnections, but you can also listen for the ‘disconnect’ event to handle it manually. This can be useful to notify other users that someone has disconnected, or to clean up any resources associated with the disconnected user. Remember that the ‘disconnect’ event is fired when the user closes their browser, loses their internet connection, or if any other error occurs.
Can I send images or other files in my Socket.io chat application?
Yes, you can, but Socket.io itself does not support file transfers. You’ll need to use another method to send files. One common method is to convert the file into a Base64 string and send it as a regular message, then convert it back into a file on the client side. However, this method is not suitable for large files. For large files, you might want to use a separate HTTP server or a service like AWS S3.
How can I test my Socket.io chat application?
Testing a Socket.io application can be done using various tools and libraries. For unit testing, you can use a library like Mocha or Jest. For end-to-end testing, you can use a tool like Cypress or Puppeteer. You can also use a library like socket.io-client to simulate clients in your tests. Remember to test all aspects of your application, including connection, disconnection, message sending, and error handling.
Can I use Socket.io for other real-time applications apart from chat?
Absolutely. While chat is a common use case for Socket.io, it can be used for any type of real-time application. This includes games, live updates, collaborative editing, real-time analytics, and more. The principles are the same: you send messages between the client and the server in real-time, and handle these messages appropriately.
How can I handle errors in my Socket.io chat application?
Error handling is an important part of any application. In Socket.io, you can listen for the ‘error’ event to handle errors. This event is fired when an error occurs with a socket. The error object is passed to the event handler, which you can use to determine the cause of the error and how to handle it. You can also use try-catch blocks in your code to handle any other errors.
Emre Guneyler is a CS student in Turkey. He is passionate about web and game development. Besides programming he enjoys chess, poker, and Call of Duty 4.