What should I be using? Socket.io rooms or Redis pub-sub?

29,002

Solution 1

Redis pub/sub is great in case all clients have direct access to redis. If you have multiple node servers, one can push a message to the others.

But if you also have clients in the browser, you need something else to push data from a server to a client, and in this case, socket.io is great.

Now, if you use socket.io with the Redis store, socket.io will use Redis pub/sub under the hood to propagate messages between servers, and servers will propagate messages to clients.

So using socket.io rooms with socket.io configured with the Redis store is probably the simplest for you.

Solution 2

I ended up writing a node plugin to allow for many pub-sub clients but only require 2 redis connections instead of a new one on every single socketio connection, it should work in general, figured someone else may find use for it.

This code assumed you have socket.io running and setup, basically in this example any number of socket.io clients can connect and it will always still only use 2 redis connections, but all clients can subscribe to their own channels. In this example, all clients get a message 'sweet message!' after 10 seconds.

Example with socket.io (utilizing redis pub-sub):

var
    RPubSubFactory = require('rpss.js');

var 
    redOne = redis.createClient(port, host),
    redTwo = redis.createClient(port, host);

var pSCFactory = new RPubSubFactory(redOne);

io.sockets.on('connection', function(socket){
    var cps = pSCFactory.createClient();
    cps.onMessage(function(channel, message){
        socket.emit('message', message);
    });
    io.sockets.on('disconnect', function(socket){
        // Dont actually need to unsub, because end() will cleanup all subs, 
        // but if you need to sometime during the connection lifetime, you can.
        cps.unsubscribe('cool_channel');
        cps.end();
    });
    cps.subscribe('cool_channel')
});

setTimeout(function(){
    redTwo.publish('cool_channel', 'sweet message!');
},10000);

Actual plugin code:

var RPubSubFactory = function(){

    var 
        len,indx,tarr;
    var
        dbcom = false,
        rPubSubIdCounter = 1,
        clientLookup = {},
        globalSubscriptions = {};

    // public
    this.createClient = function()
    {
        return new RPubSupClient();
    }

    // private
    var constructor = function(tdbcom)
    {
        dbcom = tdbcom;
        dbcom.on("message", incommingMessage);
    }
    var incommingMessage = function(rawchannel, strMessage)
    {
        len = globalSubscriptions[rawchannel].length;
        for(var i=0;i<len;i++){
            //console.log(globalSubscriptions[rawchannel][i]+' incomming on channel '+rawchannel);
            clientLookup[globalSubscriptions[rawchannel][i]]._incommingMessage(rawchannel, strMessage);
        }
    }

    // class
    var RPubSupClient = function()
    {
        var 
            id = -1,
            localSubscriptions = [];

        this.id = -1;
        this._incommingMessage = function(){};

        this.subscribe = function(channel)
        {
            //console.log('client '+id+' subscribing to '+channel);
            if(!(channel in globalSubscriptions)){
                globalSubscriptions[channel] = [id];
                dbcom.subscribe(channel);
            }
            else if(globalSubscriptions[channel].indexOf(id) == -1){
                globalSubscriptions[channel].push(id);
            }
            if(localSubscriptions.indexOf(channel) == -1){
                localSubscriptions.push(channel);
            }
        }
        this.unsubscribe = function(channel)
        {
            //console.log('client '+id+' unsubscribing to '+channel);
            if(channel in globalSubscriptions)
            {
                indx = globalSubscriptions[channel].indexOf(id);
                if(indx != -1){
                    globalSubscriptions[channel].splice(indx, 1);
                    if(globalSubscriptions[channel].length == 0){
                        delete globalSubscriptions[channel];
                        dbcom.unsubscribe(channel);
                    }
                }
            }
            indx = localSubscriptions.indexOf(channel);
            if(indx != -1){
                localSubscriptions.splice(indx, 1);
            }
        }
        this.onMessage = function(msgFn)
        {
            this._incommingMessage = msgFn;
        }
        this.end = function()
        {
            //console.log('end client id = '+id+' closing subscriptions='+localSubscriptions.join(','));
            tarr = localSubscriptions.slice(0);
            len = tarr.length;
            for(var i=0;i<len;i++){
                this.unsubscribe(tarr[i]);
            }
            localSubscriptions = [];
            delete clientLookup[id];
        }        
        var constructor = function(){
            this.id = id = rPubSubIdCounter++;
            clientLookup[id] = this;
            //console.log('new client id = '+id);
        }        
        constructor.apply(this, arguments);
    }    
    constructor.apply(this, arguments);
};

module.exports = RPubSubFactory;

I mucked around and tried to improve the efficiency as much as I could, but after doing some different speed tests, I concluded this was the fastest I could get it.

For up-to-date version: https://github.com/Jezternz/node-redis-pubsub

Share:
29,002
Josh Mc
Author by

Josh Mc

I am a web application dev by day, and a web game dev by night :)

Updated on June 19, 2020

Comments

  • Josh Mc
    Josh Mc almost 4 years

    Pretty simple question. I am building a realtime game using nodejs as my backend and I am wondering if there is any information available on which one is more reliable and which one is more efficient? I am heavily using both Redis and Socket.io throughout my code. So I want to know whether I should be utilizing Socket.io's Rooms or I would be better off using redis' pub-sub ?

    Update: Just realized there is a very important reason why you may want to use redis pub/sub over socket.io rooms. With Socket.io rooms when you publish to listeners, the (browser)clients recieve the message, with redis it is actually the (redis~on server)clients who recieve messages. For this reason, if you want to inform all (server)clients of information specific to each client and maybe do some processing before passing on to browser clients, you are better off using redis. Using redis you can just fire off an event to generate each users individual data, where as with socket.io you have to actually generate all the users unique data at once, then loop through them and send them their individual data, which almost defeats the purpose of rooms, at least for me.

    Unfortunately for my purposes I am stuck with redis for now.

    Update 2: Ended up developing a plugin to use only 2 redis connections but still allow for individual client processing, see answer below....

  • Josh Mc
    Josh Mc about 11 years
    In my current setup I am using redisStore for socket.io and redis pub-sub, but as a result every client that connects via socket.io, I need to create a corresponding redis connection. If I switch to Socket.io rooms, will it still be using a separate redis connection for each user under the hood? (I assume it will?)
  • Pascal Belloncle
    Pascal Belloncle about 11 years
    RedisStore uses a total of 3 connections (clients) per node instance. It doesn't create new ones per client connection or per room and dispatches to the correct room itself.
  • Josh Mc
    Josh Mc about 11 years
    Right, so using rooms in my case would drasticly improve efficieny as I currently use one redis client for each client (inside the socket.io onconnection callback)?
  • Pascal Belloncle
    Pascal Belloncle about 11 years
    it looks like it will, yes. You could implement logic to reuse the same connection, but leveraging socket.io implementation looks simpler, and the code should already be pretty solid.