Collaborative Drawing App: Communicating with Sockets

Shawn
5 min readMay 30, 2022

--

In this section we set up communication between the frontend and backend using web sockets.

Backend Setup

For the first time in a while, we’re making changes to server/index.js. There are various web socket packages for Node. For our relatively simple use case we’ll just use ws. Import that near the top of the file:

const {WebSocketServer} = require('ws');

At the bottom of the file, let’s add some proof of concept code. This starts the web socket server and will show when a connection is made from the front end:

const wss = new WebSocketServer({ port: 8080 });
wss.on('connection', ws => {
console.log('A connection!');
ws.on('message', data => {
console.log('Data from frontend:', data.toString());
wss.clients.forEach(client => client.send(data));
});
});

Frontend Setup

We’ll wrap the default WebSocket object in a class, so our code will be more organized. Add this to script.js:

class Socket {
constructor() {
this.socket = new WebSocket('ws://localhost:8080');
this.socket.onopen = e => this.sendDraw();
this.onmessage = msg => this.handleMessage(msg);
}
sendDraw() {
this.socket.send('this works');
}

async handleMessage(msg) {
console.log(await msg.data.text());
}
}

Then over in index.html, add this:

const socket = new Socket();

This code starts up the socket connection and sends a test message to the server. It’s important you have the server running with the new code before refreshing the page in the browser for this to work. Upon refreshing the page, the frontend sends the socket message, which should look something like this in the console where you have the Node server running:

Then the backend broadcasts the test message back to every client currently connected. So if we have multiple copies of the frontend open in different tabs, each one will get the “this works” message when a new tab is opened. If everything is working correctly, you should see “this works” in the developer console for the browser you’re using.

Identifying Yourself

Now that we can send socket messages to and from the server, we can start the collaborative part of this project. The collaboration needs a couple setup pieces:

  • The concept of “rooms,” which represents the canvas multiple people are working on together
  • IDs for each person in the room

To do this we add the Room class on the frontend:

class Room {
constructor() {
this.getRoom();
this.id = Math.round(Math.random() * 10000).toString();
}
getRoom() {
const {pathname} = location;
if (pathname !== '/') {
this.room = pathname.split('/')[1];
} else {
this.room = Math.round(Math.random() * 10000).toString();
history.pushState({}, '', '/' + this.room);
}
}
}

The getName method does the heavy work here. It checks the URL for a room name. For example, in http://localhost:3000/foo , the room name is foo. You can make the room name be whatever you want, as long as it conforms to the rules for URL structure. If it doesn’t find a room name in the URL, it randomly generates one. In this case, a number between 0 and 10,000.

The constructor also generates a random ID for the current tab. It’s also a random number between 0 and 10,000.

Instantiate the Room object in the index.html file. Put it before all the other Javascript in the file:

const room = new Room();

Now when we reload http://localhost:3000 , we see a number is attached to the end of the URL. But there’s a problem. If we now refresh the page, or manually type in a room name, we get an error 404. This is a problem in our Node code. None of the route rules know how to handle this, so we need to add it. Add this under the function for serving script.js:

app.get('/:name?', (req, res) => {
res.sendFile(path.join(__dirname + '/../client/index.html'));
});

Then restart the Node server, and now it should work correctly. This rule looks for URLs that have http://localhost:3000 followed by alphanumeric characters.

Sending a Draw Event

We’ll finish this article by just getting code in place to send a draw event to all open tabs. What needs to happen is this: when the user clicks a point on the canvas, the Canvas object needs to send information about the click to the server, using relevant data from the Socket and Room objects. To make this happen, first we need to refactor the code a bit.

In the index.html file, pass the socket and room objects as arguments to the canvas object:

const canvas = new Canvas(socket, room);

And store them as properties in the Canvas class:

class Canvas {
constructor(socket, room) {
...
this.socket = socket;
this.room = room;

}
...
}

Over in Socket , we update the sendDraw method. We pass it the needed info for other clients to know who is sending the drawing, and how to draw it themselves:

sendDraw(room, mode, color, startPoint, endPoint) {
this.socket.send(JSON.stringify({
type: 'draw',
roomId: room.id,
roomName: room.room,
mode,
color,
startPoint,
endPoint
}));
}

This method is called in Canvas.handleDraw . Note however, it needs to be called in the else statement where this.pointMode is “end.” That way the socket message is only sent when the shape is drawn. It also needs to be called before the start and end points are set back to null. Otherwise, the other clients won’t know where to draw the shape.

handleDraw(e) {
...
if (...) {
...
} else {
...
this.socket.sendDraw(this.room, this.mode, this.activeColor, this.startPoint, this.endPoint);
this.startPoint = null;
this.endPoint = null;
}
}

Change the line saying

this.socket.onopen = e => this.sendDraw();

To be console.log(e); or delete it altogether if you don’t think it’d be useful.

Putting it Together

To test the changes so far, open two tabs both with the same URL, say http://localhost:3000/1234 . Open the developer console for both, and start drawing some shapes on each tab. What you should should see in the console on either tab, are JSON objects describing what was just drawn.

The view from one tab
The view from the other tab
The view from the server console

Here’s what the code looks like at this point:

In the next section, let’s see how to actually draw the shapes received from other tabs, and also clear the canvas on every tab.

Collaborative Drawing App Series

--

--