A Chat Application Using Socket.IO

Share this article

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. Application Architecture

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: Chat Protocol

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) – Send message over the socket.
  • socket.broadcast.send(message, args) – Broadcasts message 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.

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 GuneylerEmre Guneyler
View Author

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.

chatNode-JS-ToolsSocket.IO
Share this article
Read Next
Get the freshest news and resources for developers, designers and digital creators in your inbox each week