top of page

#13 Implementing socket.io rooms

  • Aaron
  • Dec 4, 2020
  • 5 min read

Updated: Dec 5, 2020

At long last, I think I'm finally getting the hang of implementing socket.io rooms. Because I found the online documentation lacking, I'll try to share with you what I've learned.


ree


Quick things to note:

  • I am using socket.io v3.0.3

  • Get a list all created rooms socket.adapter.rooms or Object.keys(io.sockets.adapter.rooms) It used to be io.sockets.manager for older socket.io versions => but that does not work anymore (use socket.adapter.rooms now)

  • Socket.io rooms are destroyed when they are empty, and when a user joins a room that does not exist yet, they will create a new room

  • Socket.io will automatically create a default room for each connection. This can be handy for direct private messaging, but if you, like me, need a more dynamic implementation of rooms (users can create rooms themselves or see all of the available game rooms they can join), means you can’t depend on the socket.io list of all created rooms alone. You will need to create a room object and manage it yourself (handle leaves, joins, sync it with socket.io own rooms implementation, …). Something I only figured out after quite a bit of struggling. I really wish the documentation of socket.io would’ve addressed this. Either in documentation or a getting starter project. So I would’ve known from the get-go that I was not supposed to try and manage it with socket.io’s implementation alone.

  • You can add your own attributes to the socket instance (socket.youCustomAttribute = attributeValue)

  • The emit cheat sheet is your one and only friend https://socket.io/docs/v3/emit-cheatsheet/


My rooms requirements for this project:

  • Login (with a unique name)

  • Go to the second page / render new components

  • Send data (to server, from server to everyone but sender...

  • Show currently connected participants. In general, in rooms, ...

  • Create a room

    • Put a check on user created rooms, don’t let them create rooms like crazy.

    • Uniquely identifiable

    • Capable of holding data (users, gameState)

  • View rooms (view the available rooms in real-time)

  • Join room (Join an available room)

  • Destroy empty rooms

  • capable of handling game logic and closed off from other rooms

And you've already read through the basics of socket.io's rooms documentation.


Socket.io rooms are destroyed when they are empty, useful right? It could've been, for my use case scenario where you want to create "game rooms" exclusively, you get hindered by the fact that socket.io also creates a default room for each user (this was meant to facilitate private messaging, so you could just send a message to that user's private room). So if we would just show a list with all rooms to our users, they would see all those private default rooms too. That's why we need to create and manage our own room object and handle the leaves, joins, creation, ... and sync it with socket.io's own implementation of rooms.


I did not include some parts of the code, to keep the focus more on the core, and keep it simpler. If you want to see the full code: check my Github.


Login

I wanted a quick way to log the users in, just making sure that the username they choose was unique and that there aren't too many weird characters in there.

You'll probably want to have a form somewhere in your front end where you want to put a login event emitter on:

When someone fills out the name form, you'll want to emit that event to the server.

socket.emit("name", `enteredName`);

On the server, you'll want to listen for that event:

//init connected clients, so that you can keep track of them
const clients = {};

//link name
 socket.on("name", name => {
 //validate the name
 if (name) {
  // Put your name validation here (no unwated characters, ...)
 } else {
    return;
 }

 // check if name is not already in use
 let nameInUse = false;
 for (const socketId in clients) {
    if (clients.hasOwnProperty(socketId)) {
       const otherClient = clients[socketId];
       if (otherClient.name === name) {
          nameInUse = true;
       }
    }
 }
    
 if (nameInUse) {
    socket.emit("name-error", "Name is already in use");
    return;
 }

 //Pass name to clients object and show users
 clients[socket.id].name = name;
 
 // You can also add an username attribute to the socket connection instance, so you can easily access the username
 socket.username = name;
 
 // Pass info to the client that creation was successful
 socket.emit("name", clients[socket.id]);

Create a room

Once the user is logged in, they should be able to create a room, the server code could look a bit like this:

 //init your own room object outside io.on("connection"), so we can manage the rooms for ourselves
 const rooms = {};

/*--- SOCKET.IO ---*/

 //create a room
 socket.on("createRoom", () => {
 //stop creation of extra rooms when user is already in one, or if user is not logged in (we will create .connectedRoom later, once we have actually created that room)
 if (socket.connectedRoom || !socket.username) {
  return;
 }

 //get a randomRoomName (the GetRandom.randomRoomName is a javascript function I created that gives me a random unique roomName. It takes the rooms object with all the current rooms to make sure it has no duplicate value)
 roomName = GetRandom.randomRoomName(rooms);

 //if the roomName does not exist, socket.join will create a new room
 socket.join(roomName);

 //socket.rooms will contain multiple rooms because each socket automatically joins a room identified by its id as a default room
 console.log(socket.rooms); //[default room, room we created]

 //add it to our own room object and pass an empty default users object
 rooms[roomName] = { name: roomName, users: {} };

 //add creator of the room to the room by default
 rooms[roomName].users[socket.id] = socket.username;

 //create a rooms property to keep track of user rooms
 socket.connectedRoom = rooms[roomName];
 clients[socket.id].connectedRoom = rooms[roomName];

 //emit an event that we created a room to the creator
 //this should redirect the user to the created room in front-end
 socket.emit("createdRoom", roomName);

 //because a new room was created, send an event with updated rooms
 io.emit("allRooms", rooms);
 });

Join a room

Side-note: I originally had a "remove all rooms"-function that I ran before I'd let the user join another room. Because socket.io also creates a default room for each user (this was meant to make private messaging easier), it would also remove that default room, and therefore the entire server would crash. So please be careful when handling rooms.

//join a room
 socket.on("joinRoom", roomName => {
 //check if room already exsists and if user is logged in
 if (roomName in rooms && socket.username) {
    //check if user is already connected to another room
    if (socket.connectedRoom && socket.connectedRoom.name !==          roomName) {
    console.log(`already connected to another room: ${
    Object.values(socket.connectedRoom)[0]
    }`);

    //leave previous connected room with socket.io
    socket.leave(socket.connectedRoom.name);

    //delete from our own rooms object
    delete rooms[socket.connectedRoom.name].users[socket.id];

    //if the previous connected room is empty, delete the room
   if (Object.keys(rooms[socket.connectedRoom.name].users).length                  === 0) {
   delete rooms[socket.connectedRoom.name];
   //because a room was deleted, send an updated rooms event
   io.emit("allRooms", rooms);
   }

    //delete our connectedRoom attribute
    delete socket.connectedRoom;

    //delete from our clients object
    delete clients[socket.id].connectedRoom;
    io.emit("allRooms", rooms);
 }

 //join a new room
 socket.join(roomName);
 console.log(`${socket.username} joined ${roomName}`);

 //add joined user to our rooms object
 rooms[roomName].users[socket.id] = socket.username; //{ room1: { name: 'room1', users: { id: 'username' } } }
 io.to(roomName).emit(
 "roomEntered",
 `${socket.username} has joined ${roomName}`);

 //set our connectedRoom to the room we just joined
 socket.connectedRoom = rooms[roomName];
 clients[socket.id].connectedRoom = rooms[roomName];

 socket.emit("joinedRoom", roomName);
    } else {
 return socket.emit("err", `Can't find room ${roomName}`);
    }
 io.emit("allRooms", rooms);
  });
  


Leaving rooms on disconnect

//remove id if client disconnected
 socket.on("disconnect", () => {
 //remove rooms that client was in (with my own custom getUserRooms   function)
 getUserRooms(socket).forEach(roomName => {
  //delete user from rooms object
  delete rooms[roomName].users[socket.id];
  //delete room if there are 0 users
  if (Object.keys(rooms[roomName].users).length === 0) {
   delete rooms[roomName];
   //because a room was deleted, send an updated rooms event
   io.emit("allRooms", rooms);
  }
 });
 //delete user from clients
 delete clients[socket.id];
 calcConnectedClients();
});

These code snippets should help to get you started! ✌️

Comments


  • iconmonstr-github-1-240

©2020

bottom of page