Collaborative Whiteboard Tutorial, Part One - Sun, Feb 15, 2015
This tutorial aims to demonstrate how to build a real-time communication channel between one server and multiple clients (i.e. browsers), simply by using JavaScript. This project uses Node.js for its server-side backend, and targets Google Chrome for the client side. However, the outcome also works well on other browsers, including Internet Explorer. We call this application QuickDraw.
The end result is supposed to be a web service that stores multiple whiteboards in different rooms, and each client can simply type in a room name and observe or modify the contents of the whiteboard. In case a room does not exist, it will be created automatically.
This tutorial is split into different steps. First, I will show how to create a simple communication channel between the server and clients, and afterwards we transmit user input to the server and receive drawing data from the server to the clients.
The Web Server
We use Node.js on the server side as a web server to send static HTML files as well as the client side JavaScript files, and also to handle the main logic of the program. The express library is used to power the web server and send the files in the client folder to the clients. All the code regarding the web server is stored in the server/web.js
folder. Here we do not plan to explain how to run a web server with Node.js, and will simply use this file to start a web server on port 8081:
var web = require("./server/web.js");
web.start(8081);
web.js automatically creates a Socket.io object which is later used to power the communications between the server and clients.
The Communication Protocol
The server is responsible for storing whiteboards (rooms) and their content, and also broadcasting their data to the clients upon request. Therefore it needs to hold a list of rooms, their names and their content.
var rooms = [];
var roomIds = {};
function getRoom(roomId)
{
if (!(roomId in roomIds))
{
roomIds[roomId] = rooms.length;
rooms.push([]);
}
return rooms[roomIds[roomId]];
}
function sendRoom(roomId)
{
web.io.emit("servermessage", {
"room": roomId,
"command": "send_room",
"data": getRoom(roomId)
});
}
The sendRoom
function receives a room name (according to the diagram above,) and sends the data associated with that room to the clients. This function can be called every time a client joins in order to receive a copy of what is currently on the whiteboard. We handle incoming messages on the server in the handleMessage function, so we can hook sendRoom
to a join
message:
function handleMessage(msg)
{
if (msg["command"] == "join") join(msg);
}
Now we need to handle the actual drawing task. Once a user clicks on their canvas, the client side sends the drawing data which contains information such as the position of touch, the brush color and the brush radius, in JSON format. The server side receives this data with a draw
command, and adds this information to the room data. After storing the information, it rebroadcasts the same data so that every client receives this drawing information:
function draw(msg)
{
var room = getRoom(msg["room"]);
room.push(msg["data"]);
web.io.emit("servermessage", {
"room": msg["room"],
"command": "draw",
"data": msg["data"]
});
}
The draw method needs to be hooked to the draw command handled by the handleMessage
function:
function handleMessage(msg)
{
if (msg["command"] == "join") join(msg);
if (msg["command"] == "draw") draw(msg);
}
Finally, the users should be able to clear a room. This is done by sending a clear
command to the server. The server erases all the data stored in that room, and tells all clients to do the same on their canvas:
function clear(msg)
{
var room = getRoom(msg["room"]);
while (room.length) room.pop();
web.io.emit("servermessage", {
"room": msg["room"],
"command": "clear"
});
}
Again, we need to connect this method to the handleMessage
function. To prevent crashes due to unknown commands, we surround our code with a try/catch block:
function handleMessage(msg)
{
try
{
//check if message is valid
if (!("room" in msg && "command" in msg))
return;
if (msg["command"] == "draw") draw(msg);
if (msg["command"] == "join") join(msg);
if (msg["command"] == "clear") clear(msg);
}
catch (err)
{
console.log(err);
}
}
And lastly, we need to tell Socket.io to call handleMessage
every time a message arrives from a client:
web.io.on('connection', function (socket) {
console.log((new Date()) + ' Connection established.');
socket.on("message", handleMessage);
});
The server side code of our collaborative whiteboard project is done. You can study the full code on QuickDraw’s GitHub repository. The next post will cover the client side code which deals with mouse clicks as input, and draws received data from the server!