Skip to content

Instantly share code, notes, and snippets.

@martinsik
Last active September 24, 2024 14:54
Show Gist options
  • Save martinsik/2031681 to your computer and use it in GitHub Desktop.
Save martinsik/2031681 to your computer and use it in GitHub Desktop.
Node.js chat frontend and server
$(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.text('Choose name:');
};
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);
} catch (e) {
console.log('This doesn\'t look like a valid JSON: ', message.data);
return;
}
// NOTE: if you're not sure about the JSON structure
// check the server source code above
if (json.type === 'color') { // first response from the server with user's 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
input.removeAttr('disabled'); // let the user write another message
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 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');
// 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 comminucate '
+ '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>');
}
});
// 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 = [ ];
/**
* 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
if (userName === false) { // first message sent by user is their name
// 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);
}
});
});
<!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>
@rayj00
Copy link

rayj00 commented Jun 21, 2017

Ok, now I cannot get beyond the "Choose Name" step. If I just let it sit at Choose Name (after entering a name) it eventually ends up with Error Unable to comminucate with the WebSocket server.

@Globik
Copy link

Globik commented Jul 31, 2017

@rayj00 install express.js, change the server code and don't sack any more. Also use better npm install ws. It's better than websocket.js.

@aatronics
Copy link

Thanks for such a nice code

@hotdang-ca
Copy link

hotdang-ca commented Sep 5, 2017

Just wanted to put this here, for those who don't really like jQuery:

Pure Javascript chat-frontend.js

Excellent article, helped me break away from websocket abstractions such as Socket.io or Pusher.

@rayj00
Copy link

rayj00 commented Jan 5, 2018

Is there a working demo of this chat, complete with all the fixes?

Ray

@rayj00
Copy link

rayj00 commented Jan 13, 2018

Working..... but chat messages are not deleted when all users disconnect? When a user comes back in, the old messages are there?
Ideas?

Ray

@bluerid
Copy link

bluerid commented Jan 23, 2018

WebSocket-Node module provides a WebSocketRouter that can be used to define routes. However, I wasn't able to find a way to pass path parameters using the WebSocketRouter. Do you happen to know how I can do that? Is that even possible? I don't intend to use express.
https://github.com/theturtle32/WebSocket-Node

@Planeur
Copy link

Planeur commented Mar 25, 2018

i think this works well.

connection.on('close', function(connection) {
// find the destroyed connection
for (var i = 0; i < clients.length; i ++) {

            if (clients[i].socket._readableState.destroyed==true) { //compare remote address to remove from the disconnecting client
                var remoteAdresse= clients[i].remoteAddress.slice(7);
                console.log((new Date()) + " Peer "+ remoteAdresse + " disconnected."); 
                clients.splice(i, 1);
            }
       }
        // push back user's color to be reused by another user
        colors.push(userColor);
    
})

@Planeur
Copy link

Planeur commented Mar 25, 2018

when the connection is destroyed connection.remoteAddress is undefined.

@jonathan-annett
Copy link

tweaked the files a bit to allow node to server the files automajically
server

client

chat-server.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');
    var fs = require('fs');
    
    /**
     * Global variables
     */
    // latest 100 messages
    var history = [ ];
    // list of currently connected clients (users)
    var clients = [ ];
    
    var http_files = {};
    [
        ["/jquery.min.js","application/javascript"],
        ["/frontend.js","application/javascript"],
        ["/frontend.html","text/html"]
    ].forEach(function(fn){
        http_files[fn[0]]={
            content : fs.readFileSync('.'+fn[0]).toString(),
            contentType : fn[1]
        };
    });
    
    http_files["/"]=http_files["/frontend.html"];
    http_files["/index.html"]=http_files["/frontend.html"];
    
    /**
     * 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) {
        // this doubles as a way to serve the fies, and a connection for websocket to use
        var file = http_files[request.url];
        if (file) {
            response.writeHeader(200,{"content-type" : file.contentType});
            response.write(file.content);
            return response.end();
        }
        response.writeHeader(404,{"content-type" : "text/plain"});
        response.write("not found");
        return response.end();
    
    });
    
    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
                if (userName === false) { // first message sent by user is their name
                    // 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.js (note same content - just renamed from chat-frontend.js, included here for convenience)

    $(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.text('Choose name:');
        };
    
        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);
            } catch (e) {
                console.log('This doesn\'t look like a valid JSON: ', message.data);
                return;
            }
    
            // NOTE: if you're not sure about the JSON structure
            // check the server source code above
            if (json.type === 'color') { // first response from the server with user's 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
                input.removeAttr('disabled'); // let the user write another message
                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 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');
    
                // 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 comminucate '
                                                     + '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>');
        }
    });

frontend.html

    <!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="jquery.min.js"></script>
            <script src="frontend.js"></script>
        </body>
    </html>

finally, do this in the same folder : wget https://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js to cache jquery

@rolandcoops
Copy link

Great tutorial, thanks for making it

@JensThanx
Copy link

Thanks for the tutorial Martin.

@jonathan-annett
Thanks for the new version. It works fine for me.
For those using Windows/Powershell, the final command would be something like:
wget https://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js -OutFile jquery.min.js.

@MarcoLutze
Copy link

great script, it works fine! Thank you! My question: how do i remove the connection on the server, f.e if you close the browser, or reloading the browser creates a new connection without closing the old connection.What am i doing wrong?

Copy link

ghost commented Jul 10, 2018

Can we use in different systems

@nickvargish
Copy link

nickvargish commented Oct 23, 2018

Hugely useful resource, thanks very much for posting it.

I think the method of removing clients is suspect. If you remove an element from an array, the index of the other (higher numbered) items will change.
l = [0, 1, 2, 3, 4];
console.log(l[3]) //3
l.splice(2, 1)
console.log(l[3]) //4
You could store the connections in a hash using some kind of unique (time based maybe) id.

@srivanigorthi
Copy link

should we call frontend.html ? Where is chat.html file?

@kwyni
Copy link

kwyni commented Nov 4, 2018

In the chat-server.js, the line
content : fs.readFileSync('.'+fn[0]).toString(),
should be replaced by this:
content : fs.readFileSync(__dirname + fn[0]).toString(),

for services to have a better chance to run (to prevent from file not found)

@pan-rzeznik
Copy link

should we call frontend.html ? Where is chat.html file?

frontend.html => chat.html. It's work.

@adrwh
Copy link

adrwh commented Feb 25, 2019

Hi @martinsik this is excellent. I have been trying to basically replicate this using pure http2. Can i please share my code with you and ask for your help?

Specifically i am stuck when the client enters text into the input field, then hits the submit button, i can handle that request in the server code, but i don't know how to stream it back to the client(s).

Copy link

ghost commented Feb 26, 2019

May I ask what this is for?

@fakruboss
Copy link

fakruboss commented Mar 24, 2019

Hi Martinsik,

I am able to perform chat when i use the url in the browser as "file:///D:/jsfiles/nodejs-chat-IMPORTANT/nodejs-chat/chat.html"

If i use the in the browser as 'http://127.0.0.1:1337/chat.html', i am not able to connect to the server. please let me know the way to access chat application by pointing browser as like 'http://127.0.0.1:1337/chat.html'

host the frontend.html using npm http-server, and then you can access the chat application using the ip provided by the http-server

steps:
npm install http-server

cd frontend.js (give the path where the file is located)

http-server

@Yayo98
Copy link

Yayo98 commented May 29, 2019

How Can I use it in another computer?

@msreddy09
Copy link

Use the following code to work with http://localhost:1337/frontend.html

var basePath = __dirname;
var http = require('http');
var fs = require('fs');
var path = require('path');
http.createServer(function(req, res) {
    var stream = fs.createReadStream(path.join(basePath, req.url));
    stream.on('error', function() {
        res.writeHead(404);
        res.end();
    });
    stream.pipe(res);
}) 

@sonvuhwg
Copy link

sonvuhwg commented Aug 7, 2019

install finalhandler and serve-static with the npm
npm install finalhandler serve-static

thanks i succeed

@sooley
Copy link

sooley commented Sep 27, 2019

Thank for this source

@mansiparashar
Copy link

Does the server write back to the chat? If so, then how?

@ThanhVNguyen
Copy link

to serve the frontend.html direct from the chat-server.js you need to do these:

install finalhandler and serve-static with the npm
npm install finalhandler serve-static

in the file chat-server.js replace line 39

// Not important for us. We're writing WebSocket server, not HTTP server

with

    var done = finalhandler(request, response);
    serve(request, response, done);

at line 21 add these lines

var finalhandler = require('finalhandler');
var serveStatic = require('serve-static');

var serve = serveStatic("./");

int the file frontend.html replace line 26
<script src="./frontend.js"></script>
with
<script src="./chat-frontend.js"></script>

finally call the chat page with

http://127.0.0.1:1337/frontend.html

thank you so much.

@aleleonian
Copy link

Very helpful.

Thank you.

@elliotmedia
Copy link

how you access a list of all "userNames" connected to the server to send to someone?

@hrieke
Copy link

hrieke commented Feb 10, 2022

License?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment