How to create WebSocket Rooms (without Socket.io)

No Comments

Are you interested in implementing WebSocket rooms without using Socket.io? Then you are in the right place; in this post, we will discuss why it’s sometimes necessary to do your implementation, and we will see a step-by-step example of how to do it with a NodeJS backend!

What is a WebSocket and what is Socket.io

First, let’s discuss what a WebSocket is, and then let’s see what Socket.io has to do with all of that. WebSockets are an advanced technology that allows interactive sessions between a client and a server. For example, the client can send a message, and the server can send an appropriate message back. The special thing here is that the client does not have to request the answer because the server will send it based on events.

Socket.io is a library that allows for simple working with WebSockets in multiple programming languages, like JavaScript or Rust. Some benefits of working with a WebSocket are simpler message handling, rooms, and more.

When to use WebSocket without Socket.io

That all sounds great, but not every frontend allows for the usage of Socket.io. As long as you can use JavaScript or a JavaScript-based framework, it will work in most cases, but if you use something like Flutter that’s hard. Another problem occurs if your backend does not support Socket.io (i.e., Ruby).

Therefore, we will look at how to handle multiple clients and manage them through rooms without Socket.io. In the following example, we will look at it with an implementation in NodeJS. But this guide is supposed to give you an idea of how it can work with all sorts of backends that support WebSockets!

How to use a WebSocket in NodeJS

WebSocket Server

For this example, we will create a small project for WebSockets with the ws package. So let us first create the project with npm init -y and then install the package with the command npm install ws. Now we can use it inside of the project.

We need to create our first file (index.js) to use the package. Inside the file, we will first import ws and then use it to spin up a WebSocket server.

import { WebSocketServer } from 'ws';

const wss = new WebSocketServer({ port: 3000 });

wss.on('connection', function connection(ws) {
  ws.on('message', function message(data) {
    console.log('received: %s', data);
  });

  ws.send('something');
});

We will also add the nodemon package for faster development and create a profile for it in the package.json. First, install it with npm i --save-dev nodemon and then add the following line to the scripts object inside of the package.json:

"start": "nodemon index.js"

After that, we can start the basic server with npm run start.

WebSocket client

To test if the server works and does what we want, we will also create a small client. Therefore we will first create an index.html that will connect to the server.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Client</title>
</head>
<body>
    <p><b id="last-msg"></b></p>
    <script>
        const ws = new WebSocket("ws://localhost:3000");
        ws.onopen = function (event) {
            ws.send("Hello");
        }

        ws.onmessage = function (event) {
            document.getElementById("last-msg").innerText = event.data;
        }
    </script>
</body>
</html>

When you now launch the HTML (for example, with the LiveServer Extension), you can see that the website will show “something”, the message the server sends to every client after the connection.

Message handling

Lastly, before creating the rooms, we will decide on a message format and the messages that need to be exchanged between server and client. In this case, we will use the JSON format because we can build up all kinds of messages with the same structure.

{
	"type": "type"
	"params": {
		"param1": val
	}
}

For our example, we will need the following messages:

And we could build them up like this:

{ "type": "create", "params": {}}

{ "type": "join", 
	"params": {
		"code": "code"
	}
}

{ "type": "leave", "params": {}}

Create WebSocket Rooms with a Object (Dictionary)

Now that we decided on the messages and their format, we can start setting up the client handling. Therefore we will begin to edit the server. The first step is to recognize when someone joined and then listen to incoming messages. Then, inside the messages block, we can listen to the messages defined in the last section.

wss.on('connection', function connection(ws) {
  ws.on('message', function message(data) {
    const obj = JSON.parse(data);
    const type = obj.type;
    const params = obj.params;

    switch (type) {
      case "create":
        create(params);
        break;
      case "join":
        join(params);
        break;
      case "leave":
        leave(params);
        break;    
      default:
          console.warn(`Type: ${type} unknown`);
        break;
    }
  });
  
  function create(params) {}
  function join(params) {}
  function leave(params) {}
});

Before we can fill the function itself, we need to create an object with rooms as the keys and the different clients as values. Additionally, we will set the maximum number of clients per room inside another constant variable.

const maxClients = 2;
let rooms = {};

In addition to that, we will create a function that sends general information about the server’s status. We can use it to inform the client about the current status:

function generalInformation(ws) {
  let obj;
  if (ws["room"] === undefined)
  obj = {
    "type": "info",
    "params": {
      "room": ws["room"],
      "no-clients": rooms[ws["room"]].length,
    }
  }
  else
  obj = {
    "type": "info",
    "params": {
      "room": "no room",
    }
  }

  ws.send(JSON.stringify(obj));
}

The idea is that we create a new room inside of the rooms object when calling the create function. The room key is a random letter combination. In the following snippet, you will see the updated create function and helper method that creates the room key.

function create(params) {
    const room = genKey(5);
    rooms[room] = [ws];
    ws["room"] = room;

    generalInformation(ws);
}

function genKey(length) {
    let result = '';
    const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
    for (let i = 0; i < length; i++) {
        result += characters.charAt(
          Math.floor(Math.random() * characters.length));
    }
    return result;
}

Another thing you may notice is the line ws["room"] = room. We use it to assign the room code to the client, to have an easier time later leaving the room again. Additionally, we send an update on the current status to the client to inform him if the action was successful (this is optional, thus was not listed before and will be included in all of the following actions).

Next up is the join room. Before we can join a room, some requirements have to be fulfilled. First, the room must exist; second, the maximum client number should not be reached yet.

function join(params) {
  const room = params.code;
  if (!Object.keys(rooms).includes(room)) {
    console.warn(`Room ${room} does not exist!`);
    return;
  }

  if (rooms[room].length >= maxClients) {
    console.warn(`Room ${room} is full!`);
    return;
  }

  rooms[room].push(ws);
  ws["room"] = room;

	generalInformation(ws);
}

And lastly, we will add the option to leave a room again (and close it when it is empty). Therefore, we have to remove the client from the room array and set the client’s room to be empty.

function leave(params) {
  const room = ws.room;
	rooms[room] = rooms[room].filter(so => so !== ws);
  ws["room"] = undefined;

  if (rooms[room].length == 0)
    close(room);
}

function close(room) {
  rooms = rooms.filter(key => key !== room);
}

Now that we implemented our room systems, we have to update the test client to verify that everything is working as expected. Therefore we will edit the HTML as follows:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Client</title>
</head>
<body>
    <button onclick="create()">Create</button>
    <input type="text" name="room-code" id="room-code">
    <button onclick="join()">Join</button>
    <button onclick="leave()">Leave</button>
    <p><b id="last-msg"></b></p>
    <script>
        const ws = new WebSocket("ws://localhost:3000");

        ws.onopen = function (event) {}

        ws.onmessage = function (event) {
            console.log(event.data);
            document.getElementById("last-msg").innerText = event.data;
        }

        function create() { ws.send('{ "type": "create" }'); }

        function join() {
            const code = document.getElementById("room-code").value;
            const obj = { "type": "join" , "params": { "code": code }}
            ws.send(JSON.stringify(obj));
        }

        function leave() { ws.send('{ "type": "leave" }'); }
    </script>
</body>
</html>

We now have working rooms and can use these to handle room-based messages. For example, we can broadcast messages to one room by looping over the array entries and sending a message to them like that:

rooms[room].forEach(cl => cl.send(...));

Other things work pretty much the same.

Conclusion

In this post, we learned how to create a WebSocket and handle the rooms in them. To do that, we created an object (in other languages dictionary) with the room as the key and a client array as the value. With that, we can send room-based messages and let clients join and leave existing rooms.

Some ideas for a small training project would be a WebSocket Tic Tac Toe, like I did in this post (just without socket.io), or a simple messaging application like WhatsApp.

I hope you learned something new. In case you enjoyed the post, consider leaving a comment and subscribing to my newsletter!

Discussion (0)

Add Comment

Your email address will not be published.