A new Socket.io pattern I have created
Posted: Fri Apr 25, 2014 2:17 am
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:
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:
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:
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:
...instead of...
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!
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);
});
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!'});
});
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});
}
};
Code: Select all
socket.dispatchData('socketHello', {name: 'Tim', textVal: 'This is from the server, yay!'});
Code: Select all
socket.emit('dispatch', {dispatchMethodKey: 'socketHello', name: 'Tim', textVal: 'This is from the server, yay!'});
------
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!
