So I was just about to board my flight to Amsterdam when we were informed that the flight was to be delayed by three hours (guess what airline I was flying with..). I started thinking of things to do to kill the time, but had decided not to bring my laptop with me. Fortunately however I have my router port forwarded so I have ssh access to my Raspberry Pi (which I leave on 24/7 since it uses a whole 1.2 Watts) from anywhere I have an internet connection. So I pulled out my phone and fired up JuiceSSH and started thinking what I could do..
What Are WebSockets?
I had done some socket programming before (mainly writing a simple HTTP/HTTPS Proxy Server in Java - GitHub Repo) but I had heard the buzz word WebSockets being thrown around a lot lately so I decided to check it out. I had a rough idea of what to expect but a quick google gave me a nice succint description:
“WebSockets represent a long awaited evolution in client/server web technology. They allow a long-held single TCP socket connection to be established between the client and server which allows for bi-directional, full duplex, messages to be instantly distributed with little overhead resulting in a very low latency connection.” - from pusher.com
The first things that come to mind when reading that description are real-time applications such as video games and chat. Since I only had two hours (and was developing over SSH on my phone), I decided on the latter. Another quick google search provided a great blog post on setting up a simple chat server with WebSockets.
Getting the Required Tools
The first thing to do was to obtain a WebSocket library for node. There are a bunch of WebSocket implementations and im sure most are great, but the I went for was websocket-node. The first thing to do was to make a directory to hold my project. As I knew there would be server side and client side code, I made two seperate directories - public for client side code and server for server side code.
A quick note here - as we need to serve up the client side code (through a browser in my case), we need some extra functionality to do this for us. As I am a web developer I already had the LAMP stack set up on my machines at home (this is a great article on how to set this up if you haven’t already). The LAMP stack is an extremely common and easy way to serve up static websites and is most likely what was used if you have ever paid for web hosting. Another great option if you would like to stick with Node is Express. I probably would have went with this had I not already had a way to serve web pages set up.
I set up a new virtual host for my WebSocket chat server (see here) and proceeded to create the directories to hold my chat server.
|
|
The -p flag for mkdir is a handy one which will create any directories that do not exist along the way, saving you from typing multiple mkdir commands.
Next I created a package.json file in my server directory (as this will be written in node, but the client side code will not).
|
|
Follow along in the initialization process to create your package.json file (although what you actually type has little importance). Finally install websocket-node using the following:
|
|
Creating the WebSocket Chat Server
Server Side Code
Finally it was time to write some code. I decided to work on the server side code first. The first step is to set up a standard Node HTTP server.
Next we need to specify a free port on which the server can listen. I picked 3000 as I had already portforwarded that port to my Raspberry Pi on my router. Portforwarding allows your router to forward incoming packets at the specified port, to a specified machine on your local network. It looks something like this:
http://<Router’s IP>:3000 —> Router —> Server on Raspberry Pi Listening on port 3000
Since I had port forwarded port 3000 to point to my Raspberry Pi, the router then forwards those packets onto my Raspberry Pi allowing my server to be connected to from outside of my local network.
So telling our HTTP server to listen on port 3000:
Next we can create a WebSocket Server and connect it up to our HTTP server by passing the HTTP server to the constructor. This WebSocket
is what gives us our long held client-server connection, as opposed to standard HTTP which requires a fresh HTTP request everytime we wish to update.
|
|
Next we need some way of keeping track of our clients and the messages that have been sent. As I was just doing this for fun I didn’t implement anything fancy for storing the messages (no not even file IO - but I was doing this from my phone after all…). I ended up just storing the messages in an array which obviously isn’t a good idea, but it works in the short term.
So we need three variables, one to use as clients IDs, a Map to hold our clients which can be indexed by the ID and an array of sent messages.
|
|
Next we define the callback to executed when a client attempts to connect to the server. Upon receiving a request, we have the option to accept or reject the client. All are welcome on my chat server so I just accepted any incoming connections. I assign the new client the next ID and increment the ID so it’s ready for the next client, add the client to our clients Map and log out a small message to the terminal so we can see when clients have joined. Finally we will send all the old messages to the newly connected client. Note that the sendUTF method obviously only works with strings, but since were using JSON, a simple JSON.stringify()
takes care of that and the data can be recovered using JSON.parse()
on the other side.
|
|
Finally we need to define a couple more callbacks for when a client sends a message (or rather, when the server receives a message) and when the client disconnects. WebSockets make this really simple with the on('message', callback)
and on('close', callback)
respectively. Upon (the server) receiving a message, we want to save the message and broadcast that message to all connected clients. Ideally, we wouldn’t send this back to client who sent it and just update the sender’s client side messages, but as this was just a quick implementation, I went with the simpler route of just having the message broadcast to and picked up by all clients (including the sender).
Finally when a client disconnects, we free up the resources they were using and remove them from our clients Map so that we don’t try to send any more messages to that connection. This is why we used a Map, so that the client with the appropriate ID can be deleted.
|
|
The last thing to do on the server side code is to define our sendToAllClients(message)
function. Since we have a Map containing all of our clients and a function sendUTF(message)
to send a message to a client, this is really simple.
|
|
Client Side Code
Now that the server is all set up its time to create the web page for the chat room. I wrote this using basic HTML, CSS, Bootstrap and jQuery. First head back to the root of our project, cd into our public folder and make two files - index.html and script.js.
|
|
I started out with the markup to create the interface. It consists of two <form>
s (one for signing up for the chat room and one for sending messages) and a <ul>
to which we will programatically add <li>
s as we receive messages.
|
|
Finally I added some styling, grabbed CDNs for Bootstrap and jQuery and included our script.js file.
The last thing we have to do is to connect up our client side code to our server and set up some logic in jQuery to handle the client joining the chat room and sending messages. Once the client submits the join form, we will save their inputted credentials and connect to the server. This is really easy with WebSockets, simply create a new WebSocket
instance that points to our server.
|
|
We need to define a couple of callbacks in order to get the functionality we want. onopen
allows us to define a callback function that will be executed when we successfully connect to the server. I wanted to announce when a new client joined the chatroom, so in the body of the onopen
callback I created a new message object and sent that off to the server. I also enabled the <textarea>
field which is used for inputing the user’s message and the submit <button>
which is used for sending the message. Until this point they were both disabled as I didn’t want users attempting to send messages prior to connecting to the server.
We also need an event listener to handle receiving messages from the server. Once messages are received from the server, we want to create <li>
s to hold the new messages. For clarity we will define this function later, so the callback just needs to call this function for each new message that is received (or array of messages in the case of the server sending the array of past messages when the client connects).
Next we need a listener for sending messages to the server. This callback simply creates a message obejct, sends it on to the server and and clears the message <textarea>
in preperation for the next message.
Finally we define the addMessage(message)
function. We make use of jQuery’s append(markup)
method in order to append a list item to the unordered list which holds our messages. Theres some extra bits and pieces in the markup to display the sender’s name, avatar and the timestamp.
// Add listener for sending messages
$("#send").submit(function( event ) {
event.preventDefault();
// Package up the message object
var message = {
name: name,
avatar: avatar,
message: $("#message").val(),
date: new Date()
}
// Clear the message input field once the message has been sent
$("#message").val('');
// Send the message object (serialized as a string)
ws.send(JSON.stringify(message));
});
// Some markup for the messages
function addMessage(message) {
$("#chatlog").append('<li id="message-li">'
+ '<div><img id="avatar-pic" src="'
+ " " + message.avatar + '">'
+ "<h4>" + message.name + "</h4>"
+ '<p id="date-string">' + new Date(message.date).toLocaleTimeString() + "</p></div>"
+ message.message
+ "<hr>"
+ "</li>"
);
}
Conclusion
And with that we’re all set. Run the server code with node server/index.js
from the root of our project directory and browse to your IP address (or localhost if you’re developing locally) and give it a go!
The full source code is available here on GitHub. Thanks for reading.