This is the 3rd post in a multi-part tutorial series on Socket.io. See Part 2 here.
In today’s portion of the tutorial, I’ll be explaining the server side (NodeJS) code in a bit more verbosity than the comments. A fair portion of this will simply be explaining the “why” of each section of code. Additionally, since we haven’t discussed the client side code, I’ll be enumerating on the missing links between the two where necessary.
If you haven’t downloaded the code already, please see
part 2. What follows is exactly what is in app.js with line numbers.
var version = 3;
var usernames = {};
Not the most exciting intro code, but you always need to initialize various variables. We only have one blank global variable, and that’s “usernames” for tracking all the connected usernames. In addition the server code has a simple integer “version” variable. When the client connects to this server, the server sends this version. If the client version is older (a lesser value), it automatically reloads itself. If you’ve ever used Spotify and seen the “There is a newer version of Spotify available” message, this is the same concept. The one caveat is that if you don’t update the client side code version first, it’ll reload infinitely.
var io = require('socket.io').listen(3000);
console.log("Server online, port 3000");
The official socket.io chat tutorial uses Express to serve the HTML page. This is great but it creates complexity for learning Socket specifically, so in our case we open and listen for socket only.
var chat = io
.of('/chat')
.on('connection', function (socket) {
//If we get a chat event (in the chat namespace)
socket.on('chat', function(msg){
//Get the senders socket IP address, for logging/debug
var address = socket.handshake.address;
var date = new Date();
//add the current timestamp to the msg object
msg.time = date.toISOString();
//send the msg back to all clients, log it
chat.emit('chat', msg);
console.log('Chat - '+ address +' - '+ msg.nick +' - '+ msg.time +' - '+ msg.message);
});
Starting on line 10 is the first (and arguably the most important) namespace of “chat”. A separate namespace is like a separate API endpoint. It isn’t required that you use them (you could do everything in the default namespace), but it does help separate what communication is going where. Our code is taking all messages to the “/chat” namespace, which come in the form of a key-value array, and simply appending the current time and sending the message to all users. The client side sends key-value pair data that includes the sending user’s nickname and the message. This section of code would be the best place to add some security and sanity checking for messages. For example, you could add checks that:
- Verify the sending username.
- Verify the message doesn’t contain malicious content.
- Build in rate limiting.
var color = io
.of('/color')
.on('connection', function (socket) {
socket.on('color', function(msg){
var address = socket.handshake.address;
//Sends the color back to all
color.emit('color', msg);
console.log('Color -- ', address, ' -- ', msg);
});
Starting on line 31 is the “color” namespace. It functions very similarly to how the chat namespace works: all the color messages that are sent to it, are sent back to the client. It serves simply as a demonstration of how one can use multiple namespaces for conveying different types of data.
var server = io
.of('/server')
.on('connection', function (socket) {
var address = socket.handshake.address;
// As soon as a new client connects, send them the server version
socket.emit('version', version);
// Handle the "joinnick" event, sent by the client on first connect
socket.on('joinnick', function(msg){
// Add their username to our list
usernames[msg] = msg;
// Assign their username to the socket
socket.username = msg;
// Send all users a welcome message
server.emit('server', "Welcome " + msg + " hailing from "+ address);
// Welcome just the user
socket.emit('server', "Welcome user " + socket.id);
console.log('Nick join: ' + address +' - '+ socket.id +' - '+ msg);
});
Starting on line 44 is the “server” namespace which is used for all the backend communication of the client and the server. This is where the fun “more IRC-like” functionality starts to come in. On line 50 is the version code, which was mentioned at the very top of our code. Lines 53 to 67 handle what to do when a client announces that it has joined (i.e. the client sends the specific ‘joinnick’ event), which is not the same as when a client actually firsts connects (that’s handled down on line 113). This small section of code registers the nickname to that specific socket, in case we wished to address the socket by username (rather than SocketIO’s default random ID). I also want to highlight the difference of line 62 server.emit and line 65 socket.emit — server.emit (like chat.emit and color.emit) sends a message on that namespace to all users, where as socket.emit sends a message back to just the initiating user. In the next two sections we’ll use/explain this more.
socket.on('nickchange', function(msg){
// msg is an unnamed array, break out the components
var bef = msg[0];
var aft = msg[1];
console.log('nickchange requested by ' + socket.id +' -- '+ bef +' -- '+ aft);
// A list of banned usernames, must be lowercase
var bannedNames = ["jod","server","god","shakataganai","snofox"];
// Check to make sure that:
// - The new username isn't used by someone else
// - The currently sent username matches what is on their socket (so you can't change someone else)
// - The new username isn't in the list of banned usernames (Above)
// - The new username is more than 3 characters
if(usernames[aft] != null || socket.username != bef || bannedNames.indexOf(aft.toLowerCase()) > -1 || aft.length < 3){
// Checks above failed, let that client know their request was denied
console.log('Rejecting nickchange ' + socket.id);
socket.emit('server', "Nick change rejected");
}else{
At line 70 we have of our sample IRC function of “nickchange” which allows the users to change their nickname. We extract the before (or current, for verification) & (proposed) after usernames, along with an array of banned usernames. The if statement logic is fully explained in the comments, but it isn’t all inclusive from a security perspective. If the check fails, the requesting user is notified (but no one else).
// Tell that client to change their username
socket.emit('nickchange',[bef,aft]);
// Tell all users that a nick was changed
server.emit('server', bef + " is now known as " + aft);
// Remove old username from array, add the new one
delete usernames[bef];
usernames[aft] = aft;
// Change the sockets assigned username to the new one
socket.username = aft;
console.log('Completed nickchange ' + socket.id);
}
If the request checks out, we tell the requesting user’s client to make the change. Then we alert all users that the change has been made. Additionally a very important piece of housekeeping is to to make sure our global tracker & socket are updated.
io.on('connection', function(socket){
var address = socket.handshake.address;
// On first connect, send back clients IP address - for debug
socket.emit('server', address);
// Log newly connected clients
console.log('Connected: ', address, socket.id);
socket.on('disconnect', function(){
// Client disconnected - Remove username from array & log
delete usernames[socket.username];
console.log('Disconnected: ', address, socket.id);
});
All that is left now is to handle the default namespace connection starting on line 113. This is where we first see the client connect. Most of this is for debugging, with the exception of line 125 where we delete the username from our global tracker. You’ll notice that we don’t add the user to our global username tracker here, because the client hasn’t informed us what the username is going to be yet (when it does, that’s handled at line 53).
That’s it, the entire 128 lines of app.js explained. It isn’t a lot of logic, it doesn’t do a ton, but it should get you started on the right path. If you wanted to expand upon this example you’re more than welcome to do so, it’s MIT licensed). Hopefully the basis I’ve provided will give you the quick entry to adding more features. Remember though, treat all socket.io input as untrusted, check it all before you do anything with it.
In Part 4, we’ll review the Client Code.