Saturday, 3 December 2016

Push messages to browser/clients with NodeJS WebSocket

Hi Folks, off late I have been trying to do a POC on pushing messages from server to all the connected clients and found that websocket can be used for it in conjunction with NodeJS. So I am putting it all over to this post.

Following are the prerequisites in order to run the setup:
  1. NodeJS Installation (We need npm to install packages) Download Link
  2. NPM Packages (websocket, http-server, finalhandler, serve-static).
NPM package installation commands:
  • npm install websocket
  • npm install http-server
  • npm install finalhandler
  • npm install serve-static
After successful installation of NodeJS and above mentioned npm packages we can start off writing the code. We need following three files hosted on a folder (All the npm package installation commands will be executed on this folder).
  1. frontend.html.
  2. frontend.js (NodeJS Frontend).
  3. backend.js (NodeJS Backend).
Following is the source code of above files:

frontend.html
<html>
<head>
<meta charset="utf-8">
<title>WebSockets - Simple chat</title>
<style>
#content { padding:5px; background:#ddd; border-radius:5px; overflow-y: scroll;
border:1px solid #CCC; margin-top:10px; height: 160px; }
#label { margin-top:20px; }
#primary-panel { margin:10px 10px 10px 10px; }
#input { border-radius:2px; border:1px solid #ccc;
margin:10px 10px 10px 10px; padding:5px; width:400px; }
#status { display:block; float:left; margin:15px -5px 10px 10px; }
</style>
<link rel="stylesheet" type="text/css" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css"/>
</head>
<body>
<h2 align="center"><u>NodeJS WebSocket Tutorial</u></h2>
<div id="primary-panel" class="panel panel-primary">
<div class="panel-heading">
<h3 class="panel-title">Chat</h3>
</div>
<span id="status">Connecting...</span>
<input type="text" id="input" disabled="disabled" /><br/>
</div>
</div>
<div id="primary-panel" class="panel panel-primary">
<div class="panel-heading">
<h3 class="panel-title">Content from server</h3>
</div>
<div id="server-content" class="panel-body">
</div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.1.1/jquery.min.js"></script>
<script src="./chat-frontend.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"></script>
</body>
</html>
view raw frontend.html hosted with ❤ by GitHub

frontend.js

$(function() {
"use strict";
// for better performance - to avoid searching in DOM
var content = $('#content');
var input = $('#input');
var status = $('#status');
// my color assigned by the server
var myColor = false;
// my name sent to the server
var myName = false;
// if user is running mozilla then use it's built-in WebSocket
window.WebSocket = window.WebSocket || window.MozWebSocket;
// if browser doesn't support WebSocket, just show some notification and exit
if (!window.WebSocket) {
content.html($('<p>', {
text: 'Sorry, but your browser doesn\'t ' +
'support WebSockets.'
}));
input.hide();
$('span').hide();
return;
}
// open connection
var connection = new WebSocket('ws://127.0.0.1:1337');
connection.onopen = function() {
// first we want users to enter their names
input.removeAttr('disabled');
status.html('<b>Message to send:</b>');
};
connection.onerror = function(error) {
// just in there were some problems with conenction...
content.html($('<p>', {
text: 'Sorry, but there\'s some problem with your ' +
'connection or the server is down.'
}));
};
// most important part - incoming messages
connection.onmessage = function(message) {
// try to parse JSON message. Because we know that the server always returns
// JSON this should work without any problem but we should make sure that
// the massage is not chunked or otherwise damaged.
try {
var json = JSON.parse(message.data);
prepareMessage(json);
input.removeAttr('disabled').focus();
} catch (e) {
console.log('This doesn\'t look like a valid JSON: ', message.data);
return;
}
};
/**
* Send mesage when user presses Enter key
*/
input.keydown(function(e) {
if (e.keyCode === 13) {
var msg = $(this).val();
if (!msg) {
return;
}
// send the message as an ordinary text
connection.send(msg);
$(this).val('');
// disable the input field to make the user wait until server
// sends back response
input.attr('disabled', 'disabled');
}
});
/**
* This method is optional. If the server wasn't able to respond to the
* in 3 seconds then show some error message to notify the user that
* something is wrong.
*/
setInterval(function() {
if (connection.readyState !== 1) {
status.text('Error');
input.attr('disabled', 'disabled').val('Unable to comminucate ' +
'with the WebSocket server.');
}
}, 3000);
/**
* This methods appends the message coming from server as part of JSON to 'content' <div> in the HTML file.
*/
function prepareMessage(message) {
content.append('<br/>' + message.data.text);
}
});
view raw frontend.js hosted with ❤ by GitHub
backend.js
// http://ejohn.org/blog/ecmascript-5-strict-mode-json-and-more/
"use strict";
// Optional. You will see this name in eg. 'ps' or 'top' command
process.title = 'node-chat';
// Port where we'll run the websocket server
var webSocketsServerPort = 1337;
// websocket and http servers
var webSocketServer = require('websocket').server;
var http = require('http');
/**
* Global variables
*/
// latest 100 messages
var history = [];
// list of currently connected clients (users)
var clients = [];
var finalhandler = require('finalhandler');
var serveStatic = require('serve-static');
var serve = serveStatic("./");
/**
* Helper function for escaping input strings
*/
function htmlEntities(str) {
return String(str).replace(/&/g, '&').replace(/</g, '<')
.replace(/>/g, '>').replace(/"/g, '"');
}
/**
* HTTP server
*/
var server = http.createServer(function (request, response) {
var done = finalhandler(request, response);
serve(request, response, done);
});
server.listen(webSocketsServerPort, function () {
console.log('[' + (new Date().toLocaleString()) + '] -- Server is listening on port ' + webSocketsServerPort);
});
/**
* WebSocket server
*/
var wsServer = new webSocketServer({
// WebSocket server is tied to a HTTP server. WebSocket request is just
// an enhanced HTTP request. For more info http://tools.ietf.org/html/rfc6455#page-6
httpServer: server
});
// This callback function is called every time someone
// tries to connect to the WebSocket server
wsServer.on('request', function (request) {
console.log('[' + (new Date().toLocaleString()) + '] -- Connection from origin ' + request.origin + '.');
// accept connection - you should check 'request.origin' to make sure that
// client is connecting from your website
// (http://en.wikipedia.org/wiki/Same_origin_policy)
var connection = request.accept(null, request.origin);
// we need to know client index to remove them on 'close' event
var index = clients.push(connection) - 1;
var userName = false;
var userColor = false;
console.log('[' + (new Date().toLocaleString()) + '] -- Connection accepted.');
// user sent some message
connection.on('message', function (message) {
if (message.type === 'utf8') { // accept only text
// log and broadcast the message
var msg = '[' + (new Date().toLocaleString()) + '] -- Received Message from Client-' + index + ': ' + message.utf8Data;
console.log(msg);
// we want to keep history of all sent messages
var obj = {
text: htmlEntities(msg)
};
// broadcast message to all connected clients
var json = JSON.stringify({
type: 'message',
data: obj
});
for (var i = 0; i < clients.length; i++) {
clients[i].sendUTF(json);
}
}
});
// user disconnected
connection.on('close', function (connection) {
console.log((new Date()) + " Peer " +
connection.remoteAddress + " disconnected.");
// remove user from the list of connected clients
clients.splice(index, 1);
});
});
setInterval(function () {
var msg = '[' + (new Date().toLocaleString()) + '] -- Active Clients: ' + clients.length;
var obj = {
text: htmlEntities(msg)
};
var json = JSON.stringify({
type: 'message',
data: obj
});
for (var i = 0; i < clients.length; i++) {
clients[i].sendUTF(json);
}
}, 3000);
view raw backend.js hosted with ❤ by GitHub


Following is the directory structure of project: