Home » Nodejs » socket.io broadcast only to users who are in room A and B

socket.io broadcast only to users who are in room A and B

Posted by: admin November 30, 2017 Leave a comment

Questions:

Is it possible to make socket.io broadcast to all users of a namespace who are in both room A and room B but not those who are just in room A or room B?

If not, how would I go about implementing this myself? Is there a way to retrieve all users in a namespace who are in a given room?

I am working with socket.io 1.0 in node

Edit:
If there is no native method, how would I go about to create my own syntax such as:
socket.broadcast.in('room1').in('room2').emit(...)?

Answers:

You can look up all the users of a room using (ref How to update socket object for all clients in room? (socket.io) )

var clients = io.sockets.adapter.room["Room Name"]

So given two arrays for your 2 rooms’ roster list, you can compute the intersection using something like the answer here (ref: Simplest code for array intersection in javascript)

And finally you can take that list of users in both rooms and emit events using (ref: How to update socket object for all clients in room? (socket.io) )

//this is the socket of each client in the room.
var clientSocket = io.sockets.connected[clientId];

//you can do whatever you need with this
clientSocket.emit('new event', "Updates");

The alternate ofcourse is to have hidden rooms, where you maintain a combination of all rooms, and add users to those rooms behind the scenes, and then you are able to just simply emit to those hidden rooms. But that suffers from an exponential growth problem.

Questions:
Answers:

There is no in-built way to do this. So first let’s look up how the broadcast works:

https://github.com/Automattic/socket.io/blob/master/lib/namespace.js 206…221-224…230

this.adapter.broadcast(packet, {
    rooms: this.rooms,
    flags: this.flags
});

Now we know every broadcast creates a bunch of temp objects, indexOf lookups, arguments slices… And then calls the broadcast method of the adapter. Lets take a look at that one:

https://github.com/Automattic/socket.io-adapter/blob/master/index.js 111-151

Now we are creating even more temp objects and loop through all clients in the rooms or all clients if no room was selected. The loop happens in the encode callback. That method can be found here:

https://github.com/socketio/socket.io-parser/blob/master/index.js

But what if we are not sending our packets via broadcast but to each client separately after looping through the rooms and finding clients that exist both in room A and room B?

socket.emit is defined here: https://github.com/Automattic/socket.io/blob/master/lib/socket.js

Which brings us to the packetmethod of the client.js:
https://github.com/Automattic/socket.io/blob/master/lib/client.js

Each directly emitted packet will be separately encoded, which again, is expensive. Because we are sending the exact same packet to all users.

To answer your question:

Either change the socket.io adapter class and modify the broadcast method, add your own methods to the prototype or roll your own adapter by inheriting from the adapter class). (var io = require('socket.io')(server, { adapter: yourCustomAdapter });)

Or overwrite the joinand leave methods of the socket.js. Which is rather convenient considering that those methods are not called very often and you don’t have the hassle of editing through multiple files.

Socket.prototype.join = (function() {
    // the original join method
    var oldJoin = Socket.prototype.join;

    return function(room, fn) {

        // join the room as usual
        oldJoin.call(this, room, fn);

        // if we join A and are alreadymember of B, we can join C
        if(room === "A" && ~this.rooms.indexOf("B")) {
            this.join("C");
        } else if(room === "B" && ~this.rooms.indexOf("A")) {
             this.join("C");
        }  
    };
})();

Socket.prototype.leave = (function() {
    // the original leave method
    var oldLeave = Socket.prototype.leave;

    return function(room, fn) {

        // leave the room as usual
        oldLeave.call(this, room, fn);

        if(room === "A" || room === "B") {
             this.leave("C");
        }  
    };
})();

And then broadcast to C if you want to broadcast to all users in A and B.
This is just an example code, you could further improve this by not hard coding the roomnames but using an array or object instead to loop over possible room combinations.

As custom Adapter to make socket.broadcast.in("A").in("B").emit()work:

var Adapter = require('socket.io-adapter');

module.exports = CustomAdapter;

function CustomAdapter(nsp) {
  Adapter.call(this, nsp);
};

CustomAdapter.prototype = Object.create(Adapter.prototype);
CustomAdapter.prototype.constructor = CustomAdapter;

CustomAdapter.prototype.broadcast = function(packet, opts){
  var rooms = opts.rooms || [];
  var except = opts.except || [];
  var flags = opts.flags || {};
  var packetOpts = {
    preEncoded: true,
    volatile: flags.volatile,
    compress: flags.compress
  };
  var ids = {};
  var self = this;
  var socket;

  packet.nsp = this.nsp.name;
  this.encoder.encode(packet, function(encodedPackets) {
    if (rooms.length) {
      for (var i = 0; i < rooms.length; i++) {
        var room = self.rooms[rooms[i]];
        if (!room) continue;
        for (var id in room) {
          if (room.hasOwnProperty(id)) {
            if (~except.indexOf(id)) continue;
            socket = self.nsp.connected[id];
            if (socket) {
              ids[id] = ids[id] || 0;
              if(++ids[id] === rooms.length){
                socket.packet(encodedPackets, packetOpts);
              }
            }
          }
        }
      }
    } else {
      for (var id in self.sids) {
        if (self.sids.hasOwnProperty(id)) {
          if (~except.indexOf(id)) continue;
          socket = self.nsp.connected[id];
          if (socket) socket.packet(encodedPackets, packetOpts);
        }
      }
    }
  });
};

And in your app file:

var io = require('socket.io')(server, {
  adapter: require('./CustomAdapter')
});

Questions:
Answers:
io.sockets.adapter.room["Room A"].forEach(function(user_a){
  io.sockets.adapter.room["Room B"].forEach(function(user_b){
    if(user_a.id == user_b.id){
      user_a.emit('your event', { your: 'data' });
    }
  });
});