Node.js & WebSocket — Simple chat tutorial

Martin Sikora
May 9, 2017 · 12 min read

Chat features

HTML + CSS

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>WebSockets - Simple chat</title>
<style>
* { font-family:tahoma; font-size:12px; padding:0px;margin:0px;}
p { line-height:18px; }
div { width:500px; margin-left:auto; margin-right:auto;}
#content { padding:5px; background:#ddd; border-radius:5px;
overflow-y: scroll; border:1px solid #CCC;
margin-top:10px; height: 160px; }
#input { border-radius:2px; border:1px solid #ccc;
margin-top:10px; padding:5px; width:400px;
}
#status { width:88px;display:block;float:left;margin-top:15px; }
</style>
</head>
<body>
<div id="content"></div>
<div>
<span id="status">Connecting...</span>
<input type="text" id="input" disabled="disabled" />
</div>
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js">
</script>
<script src="./frontend.js"></script>
</body>
</html>

Communication client -> server, server -> client

Node.js server

npm install websocket

WebSocket server template

var WebSocketServer = require('websocket').server;
var http = require('http');

var server = http.createServer(function(request, response) {
// process HTTP request. Since we're writing just WebSockets
// server we don't have to implement anything.

});
server.listen(1337, function() { });

// create the server
wsServer = new WebSocketServer({
httpServer: server
});

// WebSocket server
wsServer.on('request', function(request) {
var connection = request.accept(null, request.origin);

// This is the most important callback for us, we'll handle
// all messages from users here.
connection.on('message', function(message) {
if (message.type === 'utf8') {
// process WebSocket message
}
});

connection.on('close', function(connection) {
// close user connection
});
});

WebSocket server full source code

"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 = [ ];
/**
* Helper function for escaping input strings
*/
function htmlEntities(str) {
return String(str)
.replace(/&/g, '&amp;').replace(/</g, '&lt;')
.replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}
// Array with some colors
var colors = [ 'red', 'green', 'blue', 'magenta', 'purple', 'plum', 'orange' ];
// ... in random order
colors.sort(function(a,b) { return Math.random() > 0.5; } );
/**
* HTTP server
*/
var server = http.createServer(function(request, response) {
// Not important for us. We're writing WebSocket server,
// not HTTP server
});
server.listen(webSocketsServerPort, function() {
console.log((new Date()) + " 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()) + ' 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()) + ' Connection accepted.'); // send back chat history
if (history.length > 0) {
connection.sendUTF(
JSON.stringify({ type: 'history', data: history} ));
}
// user sent some message
connection.on('message', function(message) {
if (message.type === 'utf8') { // accept only text
// first message sent by user is their name
if (userName === false) {
// remember user name
userName = htmlEntities(message.utf8Data);
// get random color and send it back to the user
userColor = colors.shift();
connection.sendUTF(
JSON.stringify({ type:'color', data: userColor }));
console.log((new Date()) + ' User is known as: ' + userName
+ ' with ' + userColor + ' color.');
} else { // log and broadcast the message
console.log((new Date()) + ' Received Message from '
+ userName + ': ' + message.utf8Data);

// we want to keep history of all sent messages
var obj = {
time: (new Date()).getTime(),
text: htmlEntities(message.utf8Data),
author: userName,
color: userColor
};
history.push(obj);
history = history.slice(-100);
// 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) {
if (userName !== false && userColor !== false) {
console.log((new Date()) + " Peer "
+ connection.remoteAddress + " disconnected.");
// remove user from the list of connected clients
clients.splice(index, 1);
// push back user's color to be reused by another user
colors.push(userColor);
}
});
});

Frontend application

$(function () {
// if user is running mozilla then use it's built-in WebSocket
window.WebSocket = window.WebSocket || window.MozWebSocket;

var connection = new WebSocket('ws://127.0.0.1:1337');

connection.onopen = function () {
// connection is opened and ready to use
};

connection.onerror = function (error) {
// an error occurred when sending/receiving data
};

connection.onmessage = function (message) {
// try to decode json (I assume that each message
// from server is json)

try {
var json = JSON.parse(message.data);
} catch (e) {
console.log('This doesn\'t look like a valid JSON: ',
message.data);
return;
}
// handle incoming message
};
});

Frontend full source code

$(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 WebSocket.'}
));
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.text('Choose name:');
};
connection.onerror = function (error) {
// just in there were some problems with connection...
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);
} catch (e) {
console.log('Invalid JSON: ', message.data);
return;
}
// NOTE: if you're not sure about the JSON structure
// check the server source code above
// first response from the server with user's color
if (json.type === 'color') {
myColor = json.data;
status.text(myName + ': ').css('color', myColor);
input.removeAttr('disabled').focus();
// from now user can start sending messages
} else if (json.type === 'history') { // entire message history
// insert every single message to the chat window
for (var i=0; i < json.data.length; i++) {
addMessage(json.data[i].author, json.data[i].text,
json.data[i].color, new Date(json.data[i].time));
}
} else if (json.type === 'message') { // it's a single message
// let the user write another message
input.removeAttr('disabled');
addMessage(json.data.author, json.data.text,
json.data.color, new Date(json.data.time));
} else {
console.log('Hmm..., I\'ve never seen JSON like this:', json);
}
};
/**
* Send message 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');
// we know that the first message sent from a user their name
if (myName === false) {
myName = msg;
}
}
});
/**
* 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 communicate with the WebSocket server.');
}
}, 3000);
/**
* Add message to the chat window
*/
function addMessage(author, message, color, dt) {
content.prepend('<p><span style="color:' + color + '">'
+ author + '</span> @ ' + (dt.getHours() < 10 ? '0'
+ dt.getHours() : dt.getHours()) + ':'
+ (dt.getMinutes() < 10
? '0' + dt.getMinutes() : dt.getMinutes())
+ ': ' + message + '</p>');
}
});

Running the server

node chat-server.js
Thu Oct 20 2011 09:15:44 GMT+0200 (CEST) Server is listening on port 1337
Thu Oct 20 2011 09:27:21 GMT+0200 (CEST) Connection from origin null.
Thu Oct 20 2011 09:27:21 GMT+0200 (CEST) Connection accepted. Currently 1 client.

So what’s next?

Pros and cons of node.js

What about socket.io?

What if I want some usage statistics?

/* ... */

/**
* HTTP server
*/
var server = http.createServer(function(request, response) {
console.log((new Date()) + ' HTTP server. URL'
+ request.url + ' requested.');

if (request.url === '/status') {
response.writeHead(200, {'Content-Type': 'application/json'});
var responseObject = {
currentClients: clients.length,
totalHistory: history.length
};
response.end(JSON.stringify(responseObject));
} else {
response.writeHead(404, {'Content-Type': 'text/plain'});
response.end('Sorry, unknown url');
}
});
server.listen(webSocketsServerPort, function() {
console.log((new Date())
+ " Server is listening on port " + webSocketsServerPort);
});

/* ... */
{"url":"/status","response":{"currentClients":2,"totalClients":4,"totalHistory":6}}

Download

Conclusion

Additional resources

Martin Sikora

Written by

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade