In any Real Time chatting application showing a number of online users play very important role, which needs to be updated when a new user comes online or an online user goes offline. So this article explains how we can create such a list of online users.
Here we will use Nodejs for server-side, AngularJs for client-side and Socket.io for real-time updates. Also, in this application, we will create a login and registration functionality.Here we will use MongoDB in order to store user’s information.
Explanation :
- Okay, now let’s understand the working of this application, including the login, registration and fetching the list of online users along with the real-time updates for the list.
- First Users will log into the application or if a user is new to our application then he/she will have to register in our application.
- When the user completes the registration process, then we will redirect the user to the home page.
- On the home page first thing first, we will check the user is logged in or not. If yes then we will fetch the online users or else redirected user will be logged out.
- Now suppose any user is logged in our application and on some other machine any new user logs into the application than the rest of the logged in user will receive the socket event.
- That socket will carry the information of the newly logged in user. The figure shown below explains the working of this module.
Also, read Typing notification in chatting application using nodejs and socket.io
Assumptions:
- Here I am assuming A reader of this post is intermediate in Nodejs, So I won’t explain the login and registration functionalities.
- Also, all the CSS file you will find in the zip file.
Below you can see the directory structure for our server. In the utils folder, we have our config files, helper file which contains all the method and files to handle routes.
1. Directory structure and application configuration:
Directory structure:
+--- node_modules | +--- utils | |+-- config.js |+-- db.js |+-- helper.js |+-- routes.js |+-- scoket.js +-- server.js +-- package.json
Alright, now it’s time to get our hands dirty, let’s start from package.json file.Below is my package.json file or alternatively you can create your own by using npm init
command.
package.json:
{ "name": "showing-online-users-using-nodejs-socket-io", "version": "2.0.0", "description": "Showing online users using Nodejs and Socket.io", "main": "server.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": [ "Showing", "online", "users", "using", "Nodejs", "and", "Socket.io" ], "author": "Shashank Tiwari", "license": "MIT", "dependencies": { "body-parser": "^1.17.2", "ejs": "^2.5.6", "express": "^4.15.3", "mongodb": "^2.2.28", "socket.io": "^2.0.2" } }
2. Creating a server:
Now let’s create our server.js file and write down the below code. In the below code, we will initialize the routes for the application and socket events. Also, we will add the application configuration for this current application setup.
As shown below, we will first setup configutation for the application and we will initiate the application’s routing and socket events.
server.js:
/* * @author Shashank Tiwari * Showing online users using Nodejs and Socket.io */ 'use strict'; const express = require("express"); const http = require('http'); const socketio = require('socket.io'); const bodyParser = require('body-parser'); const config = require('./utils/config'); const socketEvents = require('./utils/socket'); const routes = require('./utils/routes'); class Server{ constructor(){ this.port = process.env.PORT || 8080; this.host = `localhost`; this.app = express(); this.http = http.Server(this.app); this.socket = socketio(this.http); } appConfig(){ this.app.use( bodyParser.json() ); new config(this.app); } /* Including app Routes starts*/ includeRoutes(){ new routes(this.app).routesConfig(); new socketEvents(this.socket).socketConfig(); } /* Including app Routes ends*/ appExecute(){ this.appConfig(); this.includeRoutes(); this.http.listen(this.port, this.host, () => { console.log(`Listening on http://${this.host}:${this.port}`); }); } } const app = new Server(); app.appExecute();
3. Creating Login and Registration
Here, I am not going to talk about the login and registration. You will find the code related to that in the source code of this application. The routes.js is fully utilized in the login and registration.
4. Implementing socket Event
Noe let’s create socket.js inside utils folder and write down the below into it.In this file, we will write the socket events which will push the real-time updates to the client.
socket.js:
/* * Showing online users using Nodejs and Socket.io * @author Shashank Tiwari */ 'use strict'; const helper = require('./helper'); class Socket{ constructor(socket){ this.io = socket; } socketEvents(){ this.io.on('connection', (socket) => { /** * get the user's Chat list */ socket.on('chat-list', (data) => { let chatListResponse = {}; if (data.userId == '') { chatListResponse.error = true; chatListResponse.message = `User does not exits.`; this.io.emit('chat-list-response',chatListResponse); }else{ helper.getUserInfo( data.userId,(err, UserInfoResponse)=>{ delete UserInfoResponse.password; delete UserInfoResponse.timestamp; helper.getChatList(data.userId, (err, response)=>{ this.io.to(socket.id).emit('chat-list-response',{ error : false , singleUser : false , chatList : response === null ? null : response.users }); if (response !== null) { let chatListIds = response.socketIds; chatListIds.forEach( (Ids)=>{ this.io.to(Ids.socketId).emit('chat-list-response',{ error : false , singleUser : true , chatList : UserInfoResponse }); }); } }); }); } }); /** * Logout the user */ socket.on('logout',(data)=>{ const userId = data.userId; helper.logout(userId , (error, result)=>{ this.io.to(socket.id).emit('logout-response',{ error : false }); socket.disconnect(); }); }); /** * sending the disconnected user to all socket users. */ socket.on('disconnect',()=>{ setTimeout(()=>{ helper.isUserLoggedOut(socket.id,(response)=>{ if (response.loggedOut) { socket.broadcast.emit('chat-list-response',{ error : false , userDisconnected : true , socketId : socket.id }); } }); },1000); }); }); } socketConfig(){ this.io.use(function(socket, next) { let userID = socket.request._query['userId']; let userSocketId = socket.id; const data = { id : userID, value : { $set :{ socketId : userSocketId, online : 'Y' } } } helper.addSocketId( data ,(error,response)=>{ next(); }); }); this.socketEvents(); } } module.exports = Socket;
Explanation:
In the above socket class, I have created two methods listed below,
socketEvents().
socketConfig()
.
Let’s start with socketConfig()
method, In this method, if you notice we are storing the socket id and updating the online status of the user respectively who connects to chat application.
Here I am sending the user id of the user from the angular application, in order to update the socket id and online status.
InsocketEvents()
method, we are listeningchat-list
,logout
and disconnect
socket events on the server side.
- On
chat-list
event:I am emitting thechat-list-response
from the server for client-side. - On
logout
event: I am emitting thechat-list-response
from the server for client-side by logging out the user.
Here we covered our server side application setup and created the REST API. Also, integrated the socket.io on the server side and created the events required for our application.
5. Creating helper file to run the MongoDB Queries.
Now let’s create a helper.js file inside utils directory and write the down below code. In the below code snippet, I have written each method’s name, description, and parameters required to that respective method.
helper.js:
/* * Showing online users using Nodejs and Socket.io * @author Shashank Tiwari */ 'use strict'; class Helper{ constructor(){ this.Mongodb = require("./db"); } /* * Name of the Method : userNameCheck * Description : To check if the username is available or not. * Parameter : *1) data query object for MongDB *2) callback function * Return : callback */userNameCheck(data,callback){ this.Mongodb.onConnect( (db,ObjectID) => { db.collection('users').find(data).count( (err, result) => { db.close(); callback(result); }); }); } /* * Name of the Method : login * Description : login the user. * Parameter : *1) data query object for MongDB *2) callback function * Return : callback */login(data,callback){ console.log(data); this.Mongodb.onConnect( (db,ObjectID) => { db.collection('users').findAndModify( data ,[], {$set: {'online': 'Y'}},{},(err, result) => { db.close(); callback(err,result.value); }); }); } /* * Name of the Method : registerUser * Description : register the User * Parameter : *1) data query object for MongDB *2) callback function * Return : callback */registerUser(data,callback){ this.Mongodb.onConnect( (db,ObjectID) => { db.collection('users').insertOne(data, (err, result) =>{ db.close(); callback(err,result); }); }); } /* * Name of the Method : userSessionCheck * Description : to check if user is online or not. * Parameter : *1) data query object for MongDB *2) callback function * Return : callback */userSessionCheck(data,callback){ this.Mongodb.onConnect( (db,ObjectID) => { db.collection('users').findOne( { _id : ObjectID(data.userId) , online : 'Y'}, (err, result) => { db.close(); callback(err,result); }); }); } /* * Name of the Method : getUserInfo * Description : to get information of single user. * Parameter : *1) userId of the user *2) callback function * Return : callback */getUserInfo(userId,callback){ this.Mongodb.onConnect( (db,ObjectID) => { db.collection('users').findOne( { _id : ObjectID(userId)}, (err, result) => { db.close(); callback(err,result); }); }); } /* * Name of the Method : addSocketId * Description : Updates the socket id of single user. * Parameter : *1) userId of the user *2) callback function * Return : callback */addSocketId(data,callback){ this.Mongodb.onConnect( (db,ObjectID) => { db.collection('users').update( { _id : ObjectID(data.id)}, data.value ,(err, result) => { db.close(); callback(err,result.result); }); }); } /* * Name of the Method : logout * Description : To logout the loggedin user. * Parameter : *1) userID *2) callback function * Return : callback */logout(userID,callback){ const data = { $set :{ online : 'N' } }; this.Mongodb.onConnect( (db,ObjectID) => { db.collection('users').update( {_id : ObjectID(userID)}, data ,(err, result) => { db.close(); callback(err,result.result); }); }); } /* * Name of the Method : getChatList * Description : To get the list of online user. * Parameter : *1) userId (socket id) of the user *2) callback function * Return : callback */getChatList(userId,callback){ this.Mongodb.onConnect((db,ObjectID) => { console.log(userId); db.collection('users').find({_id:{ $ne:ObjectID(userId) } },{ username:1,online: 1,socketId:1}).toArray((error, result) => { db.collection('users').find({ _id:{ $ne:ObjectID(userId) } },{socketId: 1}).toArray((error, queryResult) => { db.close(); callback(error,{ users : result, socketIds : queryResult }); }); }); }); } isUserLoggedOut(userSocketId,callback){ this.Mongodb.onConnect( (db,ObjectID) => { db.collection('users').findAndModify( { socketId: userSocketId} ,[], {$set: {'online': 'N'}},{},(error, result) => { db.close(); if (error) { callback({loggedOut:true}); }else{ if (result===null) { callback({loggedOut:true}); }else{ if (result.online === 'Y') { callback({loggedOut:false}); }else{ callback({loggedOut:true}); } } } }); }); } } module.exports = new Helper();
6. Creating an Angular application.
Now let’s start the implementation of the front end. First we will start with login and registration, In order to do this let’s create index.html inside the views folder and write down below code. Below markup wrote for login and registration, here I am using UI bootstrap for AngularJs.
index.html:
<!-- Showing online users using Nodejs and Socket.io @author Shashank Tiwari --> <html ng-app="app" ng-controller="app"> <head> <title>Sending message to specific client</title> <link rel="stylesheet" href="css/bootstrap.min.css"> <link rel="stylesheet" href="css/style.css"> </head> <body> <div class="container"> <div class="row"> <div class="col-md-12 app-heading"> <h1 class="text-center">Showing online users using Nodejs and Socket.io</h1> </div> <div class="col-md-offset-3 col-md-6"> <div class="self-container"> <uib-tabset active="active"> <uib-tab index="0" heading="Login"> <div class="tab-container"> <div class="form-group"> <label>Username:</label> <input type="text" name="username" class="form-control" ng-model='form.username'/> </div> <div class="form-group"> <label>Password:</label> <input type="text" name="password" class="form-control" ng-model='form.password'/> </div> <div class="form-group"> <button class="btn btn-primary" ng-click="login()">Login</button> </div> </div> </uib-tab> <uib-tab index="1" heading="Registration"> <div class="tab-container"> <div class="form-group"> <label>Username:</label> <input type="text" name="username" class="form-control" ng-model='form.isUsernameSuggest' ng-keydown='usernameCheck()'/> <div ng-hide='UI.isUsernameAvailable'> <br/> <div class="alert alert-danger">This username is not available</div> </div> </div> <div class="form-group"> <label>Password:</label> <input type="text" name="password" ng-model='form.registerPassword' class="form-control"/> </div> <div class="form-group"> <button class="btn btn-primary" ng-click="register()">Register</button> </div> </div> </uib-tab> </uib-tabset> </div> </div> </div> </div> </body> <script src="/socket.io/socket.io.js"></script> <script src="js/angular.min.js"></script> <script src="js/ui-bootstrap-tpls-2.5.0.min.js"></script> <script src="js/service.js"></script> <script src="js/script.js"></script> </html>
Our login and Registration markup is completed, now let’s start the implementation of the home page. Create the home.html inside the views folder and write down the below code.
home.html:
<!-- Showing online users using Nodejs and Socket.io @author Shashank Tiwari --> <html ng-app="app" ng-controller="home"> <head> <title>Sending message to specific client</title> <link rel="stylesheet" href="../css/bootstrap.min.css"> <link rel="stylesheet" href="../css/style.css"> </head> <body> <div class="app-header"> <h2> Hello {{UI.username}}</h2> <div class="logout" ng-click='logout()'>Logout</div> </div> <div class="container"> <div class="row users-list"> <div ng-repeat='result in UI.results' ng-class='getClassName(result.online)' class="list col-md-12"> <h3>{{result.username}}</h3> <p>User Status: {{getOnlineStatus(result.online)}}</p> </div> </div> </div> <div class="app-footer"> <a href="https://codershood.info/2015/12/10/showing-online-users-using-nodejs-socket-io/" target="_blank">Read full Tutorial with explanation</a> </div> </body> <script src="/socket.io/socket.io.js"></script> <script src="../js/angular.min.js"></script> <script src="../js/service.js"></script> <script src="../js/ui-bootstrap-tpls-2.5.0.min.js"></script> <script src="../js/script.js"></script> </html>
Explanation:
The above markup is short and sweet, here first you will see the logout option then the ng-repeat for to iterate the list of online/offline users and at the end all the scripts that we have used.
Now let’s write down the AngularJs code, where we will create our AngularJs services and controllers. So let’s start from controllers, Create a script.js inside the public_data/js/
folder. In this file, we will create code login, registration, and home’s code.So basically we will have two controllers here one is for index.html and second on is for home.html. Write down the below code into the script.js file.
script.js:
/* * Showing online users using Nodejs and Socket.io * @author Shashank Tiwari */ 'use strict'; const app = angular.module('app',['ui.bootstrap']); app.factory('socket', function ($rootScope) { let socket = null; return { connect: function(userId){ socket = io.connect({query: `userId=${userId}`}); }, on: function (eventName, callback) { socket.on(eventName, function () { var args = arguments; $rootScope.$apply(function () { callback.apply(socket, args); }); }); }, emit: function (eventName, data, callback) { socket.emit(eventName, data, function () { var args = arguments; $rootScope.$apply(function () { if (callback) { callback.apply(socket, args); } }); }) } }; }); app.service('appService', function (socket,$http) { return new AppService($http); }); app.controller('app', function ($scope,$timeout,socket,appService) { $scope.UI = { isUsernameAvailable : true, typingTimer : null }; $scope.form = { username : null, password : null, isUsernameSuggest : null, registerPassword:null }; var typingTimer = null; $scope.usernameCheck = function(){ $timeout.cancel($scope.UI.typingTimer); $scope.UI.typingTimer = $timeout(()=>{ if ($scope.form.isUsernameSuggest !== undefined || $scope.form.isUsernameSuggest !=='') { appService.usernameCheck( { username:$scope.form.isUsernameSuggest, }, (response)=>{ if(!response.error) { $scope.UI.isUsernameAvailable = true; }else{ $scope.UI.isUsernameAvailable = false; } }); } },800); } $scope.login = function (){ if ($scope.form.username === undefined || $scope.form.username ==='') { alert('Invalid username.'); }else if($scope.form.password === undefined || $scope.form.password ==='') { alert('Invalid password.'); }else{ appService.login({ username:$scope.form.username, password:$scope.form.password }, (response)=>{ if (response.error) { alert(response.message); }else{ window.location.href = '/home/'+response.userId; } }); } } $scope.register = function (){ if ($scope.form.isUsernameSuggest === undefined || $scope.form.isUsernameSuggest ==='') { alert('Invalid username.'); }else if($scope.form.registerPassword === undefined || $scope.form.registerPassword ==='') { alert('Invalid password.'); }else{ appService.register({ username:$scope.form.isUsernameSuggest, password:$scope.form.registerPassword }, (response)=>{ if (response.error) { alert(response.message); }else{ window.location.href = '/home/'+response.userId; } }); } } }); app.controller('home', function ($scope,$timeout,socket,appService) { $scope.UI = { username : null, typingTimer : null, results : null }; appService.userSessionCheck((response)=>{ if (response.error) { window.location.href='/'; }else{ $scope.UI.username = response.username; let userId = appService.getUserid(); socket.connect(userId); socket.emit('chat-list',{userId:userId}); socket.on('chat-list-response',(response)=>{ if (response.singleUser) { let chatListUsers = $scope.UI.results.filter( ( obj ) =>{ if(obj._id === response.chatList['_id']){ return false; }else{ return true; } }); $scope.UI.results = chatListUsers; $scope.UI.results.push(response.chatList); }else if(response.userDisconnected){ if ($scope.UI.results.length > 1) { let chatListUsers = $scope.UI.results.filter( ( obj ) =>{ if(obj.socketId === response.socketId){ obj.online = 'N'; } return true; }); $scope.UI.results =chatListUsers; } }else{ $scope.UI.results = response.chatList; } }); } }); $scope.logout = function(){ socket.emit('logout',{userId:appService.getUserid()}); socket.on('logout-response',(response)=>{ if (!response.error) { window.location.href='/'; } }); } $scope.getClassName = function(isOnline){ if (isOnline==='N') { return 'user-offline'; }else{ return 'user-online'; } } $scope.getOnlineStatus = function(isOnline){ if (isOnline==='N') { return 'Offline'; }else{ return 'Online'; } } });
Explanation:
In the above code,
First, we have created a socket factory method, where I am connecting socket server with the client by passing the userId of the user. Here I have created a separate class named as appService and I am using that class to create angular js service. We will take a look appService class after some time.
But before that let’s understand the both controllers. Here app controller si used for index.html, in other words, we are using the app controller for login and registration.I think the app controller is very easy to understand so I won’t talk much about it.
In the home controller, we have logic behind showing the online and offline user, So let’s break the code and understand it.
Here, we are capturing the userId of the user from the Url by using the appServiceclass. If in any case userId found null or blank then on the next line we will redirect the user to login page.
Or else in the ideal situation, we will check the session by passing the userId of the respective user to userSessionCheck()
which is defined inside the appService
. If everything goes fine then we will connect the user to the socket.
Let’s get the list of online users, to do that we will use socket. The code to get the list of users goes inside the userSessionCheck()
method for an obvious reason.
we are updating the list of the user, by using the socket on and emit event. For example, If a new user logs in into the system or an existing user goes offline in both the cases we will update the list of online users.
Now, here I am emitting the complete list of users to the user who just logged into the system.
And, the for the rest of users, those who are already online will get the users information who went offline or just logged in.
This makes sense, right? Because sending the complete list of online users to each and every user is not good.
Now if you see the above code there is else if()
code block, http for the below task.
- If the response from the socket event contains the singleUser property which means the new socket connection is established.
- If the response contains the userDisconnected property then it means the user went and offline remove the user from the chat list.
- And at the end, In the else condition we will update the list of users in the else block.
Now the last part is remaining and i.e. is appService class. In this class, I have defined the method to create an HTTP request for login and registration and some other important for out application. So create a service.js class inside the public_data/js
folder and write down the below code.
service.js:
/* * Showing online users using Nodejs and Socket.io * @author Shashank Tiwari */ class AppService{ constructor($http){ this.http = $http; } httpCall(params,callback){ if (params.url === undefined || params.url === '') { alert('Invalid Server call'); return; }; this.http({ url:params.url, method: 'POST', data:params.data }).then( (response) =>{ callback(response.data); }, (response) =>{ callback(response.data); }); } login(data,callback){ if (data.username === undefined || data.username ===''){ callback({ error:true, message:'Invalid username.' }); }else if(data.password === undefined || data.password ===''){ callback({ error:true, message:'Invalid password.' }); }else{ const params = { url : '/login', data : { username:data.username, password:data.password } }; this.httpCall(params, (response)=>{ callback(response); }); } } usernameCheck(data,callback){ if (data.username === undefined || data.username ===''){ callback({ error:true, message:'Invalid username.' }); }else{ const params = { url : '/usernameCheck', data : { username:data.username } }; this.httpCall(params, (response)=>{ callback(response); }); } } register(data,callback){ if (data.username === undefined || data.username ===''){ callback({ error:true, message:'Invalid username.' }); }else if(data.password === undefined || data.password ===''){ callback({ error:true, message:'Invalid password.' }); }else{ const params = { url : '/registerUser', data : { username:data.username, password:data.password } }; this.httpCall(params, (response)=>{ callback(response); }); } } getUserid(){ const urlData = (window.location.pathname).split('/'); if (urlData.length < 3 ) { window.location.herf = '/'; }else{ return urlData[urlData.length-1];; } } userSessionCheck(callback){ let userId = this.getUserid(); const params = { url : '/userSessionCheck', data : { userId:userId } }; this.httpCall(params, (response)=>{ callback(response); }); } }
7. Final thoughts:
So for this article that’s it.If you find this article useful consider sharing this on your social media and let the other people know about it and if you feel like giving me any suggestions please comment in below comment box.
Yes, I am working on the issue,
What if the process will be terminated unexpectedly? After restart process, old users will be yet marked as online.
I will add this fix ASAP and if you already have an idea about this issue, please comment below it will help other readers too.
What if the process will be terminated unexpectedly? After restart process old users will be yet marked as online.
Nice catch, I’ll update this code.
Is there any progress in this issue? What if the application is running within the node cluster, i.e. 2 or more processes and one of them died?
how to run the app i m getting error when trying to login..
in server.js line no 80. says can’t use the undefined property length
hi
Please update this module “Showing online and offline users using nodejs and Socket.io in Angular 2”..Thanks in Advance
Sure, I’ll consider your suggestion.
What if the process will be terminated unexpectedly?
Simply update status to ‘N’ for all users (with ‘Y’ status) in DB when the process starts.
What is config.js
bro can you add this to github