Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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(...)?

like image 573
Firas Dib Avatar asked Jul 09 '15 12:07

Firas Dib


People also ask

Is Socket.IO full duplex?

info. WebSocket is a communication protocol which provides a full-duplex and low-latency channel between the server and the browser. More information can be found here.

How do I leave a room in socket IO client?

Joining and leaving​ In that case, a union is performed: every socket that is at least in one of the rooms will get the event once (even if the socket is in two or more rooms). In that case, every socket in the room excluding the sender will get the event. To leave a channel you call leave in the same fashion as join .

How many rooms can Socket.IO have?

socket.io rooms are a lightweight data structure. They are simply an array of connections that are associated with that room. You can have as many as you want (within normal memory usage limits). There is no heavyweight thing that makes a room expensive in terms of resources.


2 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.

like image 114
Archit Baweja Avatar answered Oct 05 '22 23:10

Archit Baweja


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')
});
like image 25
Sebastian Nette Avatar answered Oct 06 '22 00:10

Sebastian Nette