A new Socket.io pattern I have created

For discussions about game development that does not fit in any of the other topics.
Post Reply
User avatar
Jackolantern
Posts: 10891
Joined: Wed Jul 01, 2009 11:00 pm

A new Socket.io pattern I have created

Post by Jackolantern »

One of the things I hated about my first attempt at a major Node.js/Socket.io application was how the socket handlers piled-up, particularly on the client. So I did some thinking and came up with this dispatcher pattern:

Code: Select all

$(function() {
    var socket = io.connect('http://localhost:3000');

    //setup a JS module
    var basicObj = (function() {
        var obj = {};
        //this is a private variable since it isn't returned in the module
        var privateName = "Jackolantern";

        //public handler function for socket input
        obj.socketHello = function(data) {
            alert(data.textVal);
            dispatcher.sendSocketData('testFunction', {testing: privateName});
        };

        return obj;
    }());

    //dispatcher module for socket communication
    var dispatcher = (function() {
        var disp = {};

        //the holder of the socket handlers
        var handlersList = {};

        //function to add a handler
        disp.addSocketListener = function(methodKey, method) {
            //overwrite the hanlder even if it exists to allow updating the handlers
            handlersList[methodKey] = method;
        };

        //function to remove a handler
        disp.removeSocketListener = function(methodKey) {
            delete handlersList[methodKey];
        };

        //function to send socket data to the server
        disp.sendSocketData = function(methodKey, extraData) {
            //add the key to the object if it is an object
            if (typeof extraData === 'object') {
                extraData.dispatchMethodKey = methodKey;
                //now send it
                socket.emit('dispatch', extraData);
            } else {
                //the extraData is something besides an object, so add it to a new object
                socket.emit('dispatch', {dispatchMethodKey: methodKey, value: extraData});
            }
        };

        //handle the basic socket.io connection
        socket.on('dispatch', function(data){
            //if we don't have a handler for this key, notify user
            if (!handlersList[data.dispatchMethodKey]) {
                console.log("Error: No socket handler registered for " + data.dispatchMethodKey);
            } else {
                //call the handler, passing on the data less the key
                var mKey = data.dispatchMethodKey;
                delete data.dispatchMethodKey;
                handlersList[mKey](data);
            }
        });

        return disp;
    }());

    //now register socket handlers
    dispatcher.addSocketListener("socketHello", basicObj.socketHello);
});
You can see that I am first connecting to Socket.io on the server, and then creating a basic module with a field and a socket handler method that needs socket response data. The next object I create is the dispatcher module. It keeps a private object on hand to maintain the handler list. I add two more methods; one is to add a new listener method, and the other is to remove one. The listeners are referred to by a customizable name, but I would suggest to refer to them by the same name as the handler method itself.

I next create a method called sendSocketData, which will replace socket.emit(). It is designed to have the same signature as emit(), but it formats the emit() call it surrounds under the covers. The next part is where the single actual socket event is handled. It uses the "dispatchMethodKey" attribute of the sent data to call the correct method that has been registered as a handler. This is not really that different than how Socket.io works underneath, except I am passing the key along with the user data and then stripping it out before it is processed on the other side. The last line demonstrates registering a handler.

Next we move on to the server. Things get slightly more complex on the server, but not too much so. Here is the code of an Express app that is integrated with Socket.io:

Code: Select all

var express = require('express');
var routes = require('./routes');
var user = require('./routes/user');
var http = require('http');
var path = require('path');
var sio = require('socket.io');

var app = express();

//start socket.io
var server = http.createServer(app);
var io = sio.listen(server);

// all environments
app.set('port', process.env.PORT || 3000);
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'ejs');
app.use(express.favicon());
app.use(express.logger('dev'));
app.use(express.json());
app.use(express.urlencoded());
app.use(express.methodOverride());
app.use(app.router);
app.use(express.static(path.join(__dirname, 'public')));

// development only
if ('development' == app.get('env')) {
  app.use(express.errorHandler());
}

app.get('/', routes.index);
app.get('/users', user.list);
app.get('/test', function(req, res) {
    res.render('tester', {value: 'Testing it'});
});

server.listen(app.get('port'), function(){
    console.log('Express server listening on port ' + app.get('port'));
});

var testFunction = function(data) {
    console.log("Back from client: " + data.testing);
};

//dispatcher module for Socket.io communication
var dispatcher = (function() {
    var disp = {};

    //the holder of the socket handlers
    var handlersList = {};

    //function to add a handler
    disp.addSocketListener = function(methodKey, method) {
        //overwrite the hanlder even if it exists to allow updating the handlers
        handlersList[methodKey] = method;
    };

    //function to remove a handler
    disp.removeSocketListener = function(methodKey) {
        delete handlersList[methodKey];
    };

    //function to receive incoming data
    disp.incomingSocketData = function(data) {
        //if we don't have a handler for this key, notify user
        if (!handlersList[data.dispatchMethodKey]) {
            console.log("Error: No socket handler registered for " + data.dispatchMethodKey);
        } else {
            //call the handler, passing on the data less the key
            var mKey = data.dispatchMethodKey;
            delete data.dispatchMethodKey;
            handlersList[mKey](data);
        }
    };

    return disp;
}());

//register a handler for the response from the client
dispatcher.addSocketListener('testFunction', testFunction);


//begin working with the socket object and connections
io.sockets.on('connection', function(socket){

    //the single event that has to stay inside of the connection handler
    socket.on('dispatch', function(data) {
        dispatcher.incomingSocketData(data);
    });

    //Socket.prototype.dispatchData() is a convenience method defined in socket.io >> lib >> socket.js
    socket.dispatchData('socketHello', {name: 'Tim', textVal: 'This is from the server, yay!'});


});
The top of the file is your typical Express and Socket.io boilerplate until we get to the testFunction global function. This is simply used as a test function, hence the name. Of course in a real large application you would be using modules, and registering that module's methods as handlers. Directly below that is the server-side Dispatcher module. It is a little bit leaner than the client-side version since we can't add the single Socket.io event handler within the module. Below the Dispatcher module, there is a demonstration of adding a socket handler on the server-side, which is exactly the same as the client.

Directly below that is the required io.sockets.on("connection", fn) Socket.io connection event. Inside that is the one required socket event handler, "dispatch". The callback of dispatch simply forwards the event into the Dispatcher module to be processed and sent to where it needs to go.

The final piece of code is a convenience method added directly into Socket.io, at the bottom of node_modules >> socket.io >> lib >> socket.js:

Code: Select all

Socket.prototype.dispatchData = function(key, obj) {
    //if the object is an object, simply add the key to it
    if (typeof obj === 'object') {
        obj.dispatchMethodKey = key;
        //now send it
        this.emit('dispatch', obj);
    } else {
        //the obj is something besides an object, so add it to a new object
        this.emit('dispatch', {dispatchMethodKey: key, value: obj});
    }
};
Similar to the client-side code, dispatchData is meant to replace Socket.emit(). It is used in the exact same way, but under the covers it is working similar to the client-side version where it is actually calling the "dispatch" socket event and adding the event name you pass in as the dispatchMethodKey. This way your calls can look like:

Code: Select all

socket.dispatchData('socketHello', {name: 'Tim', textVal: 'This is from the server, yay!'});
...instead of...

Code: Select all

socket.emit('dispatch', {dispatchMethodKey: 'socketHello', name: 'Tim', textVal: 'This is from the server, yay!'});
I think it is worth extending Socket.io for lol.

------

Ok, so what is the point of all of this? Basically, once you get this boilerplate set up, all you have to do is add the socket listeners, and then you can code the rest of your application as if the Socket.io events are coming directly into your modules and objects! It really keeps it a lot cleaner and structured. You can keep functionality where it is supposed to be instead of in piles-upon-piles of Socket.io event callbacks. My Node.js MUD got over-run with several thousand lines of Socket.io event callbacks, and it became very hard to maintain and modify.

This also brings Socket.io more in line with the way other popular languages handle event listeners, such as Java, Python, C#, and even vanilla Javascript.

I hope this could be helpful to someone else! :cool:
The indelible lord of tl;dr
User avatar
kaos78414
Posts: 507
Joined: Thu Jul 22, 2010 5:36 am

Re: A new Socket.io pattern I have created

Post by kaos78414 »

Have you thought about packaging this up for distribution with NPM? I'm sure there are some people out there, myself included, who may be able to use this or would be willing to make improvements / pull requests. If you put it on github I might be able to fork and play with it at some point (not sure when I'd have the time), but this definitely looks like something I'd want to use for my MUD.

EDIT: Obviously it would take a little work to make it independent of Express / Socket.io. Unless you wanted to just have socket.io as a dependency and just consider this an extension of it.

EDIT2: Actually, upon examining it a bit, if you just released the "dispatcher" class as a module, that would about do it.

EDIT3: You could do it like this actually -

Code: Select all

exports.socketio = function ( socketio ) {
    socketio.Socket.prototype.dispatchData = function(key, obj) {
      //if the object is an object, simply add the key to it
      if (typeof obj === 'object') {
          obj.dispatchMethodKey = key;
          //now send it
          this.emit('dispatch', obj);
      } else {
          //the obj is something besides an object, so add it to a new object
          this.emit('dispatch', {dispatchMethodKey: key, value: obj});
      }
  };

  return socketio;
};

exports.dispatcher = (function() {
    var disp = {};

    //the holder of the socket handlers
    var handlersList = {};

    //function to add a handler
    disp.addSocketListener = function(methodKey, method) {
        //overwrite the hanlder even if it exists to allow updating the handlers
        handlersList[methodKey] = method;
    };

    //function to remove a handler
    disp.removeSocketListener = function(methodKey) {
        delete handlersList[methodKey];
    };

    //function to receive incoming data
    disp.incomingSocketData = function(data) {
        //if we don't have a handler for this key, notify user
        if (!handlersList[data.dispatchMethodKey]) {
            console.log("Error: No socket handler registered for " + data.dispatchMethodKey);
        } else {
            //call the handler, passing on the data less the key
            var mKey = data.dispatchMethodKey;
            delete data.dispatchMethodKey;
            handlersList[mKey](data);
        }
    };

    return disp;
}());
And then when defining the app people can just do:

Code: Select all

customSocket = require('jacksocketio');
socketio = require('socket.io');
io = customSocket.socketio(socketio);
dispatcher = customSocket.dispatcher;
Or whatever you wanna call the vars. Just an idea :)
w00t
User avatar
Jackolantern
Posts: 10891
Joined: Wed Jul 01, 2009 11:00 pm

Re: A new Socket.io pattern I have created

Post by Jackolantern »

Interesting! I really did want to make it a module, since I knew this was a lot of boilerplate for people to write. The issue I ran in to was the modification to the Socket object, but I figured I just didn't know the proper way to extend it. I really wanted the convenience method for sending the data, so I just kind of left it as it was.

But I knew there must have been a better way to extend Socket, and this seems to be it! I will definitely be looking at this and seeing if I can get it packaged up nicely, because adding it to NPM is what I really want to do!

Thank you so much :)
The indelible lord of tl;dr
User avatar
kaos78414
Posts: 507
Joined: Thu Jul 22, 2010 5:36 am

Re: A new Socket.io pattern I have created

Post by kaos78414 »

I just googled "extend socket.io" and found this repo: https://github.com/lmjabreu/socket.io-wildcard

That's how they're doing it. I'm not sure if that's the best way, but it should do for now. I like that it decouples the two "parts" of this module so that if someone only wants one part they're free to use only that one part. But yeah let me know if/when you get it onto a repo and I'll clone it and play with it in a project so I can see if there's anything useful I could contribute :)
w00t
Post Reply

Return to “General Development”