Socket.IO : Managing single-user-multi-connections
What happens when there are multiple connections for a single user and you are triggering an event or calling a web service on "disconnect"?
socket.on('disconnect', function(data){
//Make a REST call to a third-party service
}
For each connection the user has, the rest service gets called. We obviously don't want that.
The one obvious solution everyone resorts to is maintaining a counter for the number of connections the user has opened and decrementing the counter for each disconnect fired and finally firing disconnect handler only when the counter reaches 0. We are going to follow the same route and compare the popular ways of managing this counter.
1. Redis
Redis is one kick-ass datastore and a widely used pubsub implementation that goes very well with socket.io. Redis provides convenience methods like "incr" and "decr" that can increment and decrement a key with current user's Id or name on "connect" and "disconnect" callbacks.
If you notice closely, this method has one small problem. The Redis keys that we use for each user should have a proper expiration set and must be cleaned up when not needed. Any improper implementation or unknown errors can cause the keys to pile up and blow.
2. Socket.io Rooms
A much better approach can be to use socket.io rooms to maintain the individual connections for a user. For every socket connection the user makes, we add the socket instance to a room dedicated for this user.
Let's call the room, "Room:Josh". Now, when Josh makes a new connection, we add the new socket to his room as follows:
io.on('connection', function(socket) {
socket.join("Room:Josh")
}
When Josh disconnects, before executing the disconnect handler, we check if there is more than one socket on the room. If so, we don't execute the handler.
var counter = io.sockets.clients(socket.room).length;
if(counter == 1){
//Make a REST call to a third-party service
}
Fortunately, for each disconnect fired for the user, socket.io automatically removes the socket from his room and deletes the room completely when there are no more socket entries in the room. You are not required to call "leave" on each socket.
In the above code, we check the counter for 1 because socket.io disconnects the user and removes the connection from his room only after executing the disconnect callback. So, there will be one "last" connection upon which we execute the handler.
This approach makes your code much more simpler and readable. The overhead of managing individual sockets are completely abstracted from the user.
If there are any other methods of handling this single-user-multiple-connections issue, please post it in the comments.
Written by Sathappan
Related protips
6 Responses
I never thought of doing something like this. I was maintaining an array of online users and storing the socket.id in a socketIDs array property attached to the user. Which, by the way, I do not recommend - the last socket always stays behind and disconnects never happen. Thanks for this, took a while of Googling, but I will surely refer to it again.
This was very insightful. I decided to write a blog post about how I used your method to replace manual management of socket ID's in Redis. http://notjoshmiller.com/socket-io-rooms-and-redis/
@velveteer Glad you found the tip useful. Thumbs up on your post about managing socket IDs in Redis.
You can also use the HTTP session store you're already using if you've got it backed up to a database like redis.
e.g. in sails:
onDisconnect: function (session, socket){
User.findOne(session.me).exec(function (err, user){
if (err) {
sails.log.error('Error occurred in onDisconnect handler: ', err);
return;
}
if (!user) {
sails.log.warn('User attached to the disconnecting socket no longer exists.');
return;
}
// ...
// --make call to 3rd party service here--
// ...
})
}
Hey here's a question. I came across your blog entry because i'm looking for a way to manage multiple rooms with multiple users, as well as broadcasting to rooms based upon the NOTIFY/LISTEN abilities of PostgreSQL.
So the reasoning.
Each user who logs in, is bound to a parent
account ie: a Manufacturer, Customer, or Agency
Customers/Agencies will place orders on Manufacturer products, however, manufacturers are also able to purchase manufacturer products.
Basically if Wile E Coyote purchases x number of items from Acme Anvils then all Acme Anvil users (employee's) should receive a notification about the purchase and it's details.
I have bound the room numbers to a UUID bound to their organization, and each user as well has their own UUID. So technically I should be able to have:
[
{
id: 1234556,
establishment: 'Looney Tunes',
users: [
{
id: 555494,
name: 'Foghorn Leghorn',
role: {
id: 1,
name: 'Administrators'
},
group: {
id: 1,
name: 'System Administrator'
}
},
{
id: 8957230,
name: 'Daffy Duck',
role: {
id: 1,
name: 'Administrators'
},
group: {
id: 1,
name: 'System Administrator'
}
},
{
id: 2340856,
name: 'Bugs Bunny',
role: {
id: 1,
name: 'Developer'
},
group: {
id: 1,
name: 'System Administrator'
}
}
]
},
{
id: 9079687,
establishment: 'Disney',
users: [
{
id: 09234859,
name: 'Mickey Mouse',
role: {
id: 1,
name: 'Administrators'
},
group: {
id: 2,
name: 'Manufacturers'
}
},
{
id: 234156,
name: 'Minnie Mouse',
role: {
id: 6,
name: 'Order Desk'
},
group: {
id: 2,
name: 'Manufacturer'
}
}
]
}
]
so basically if disney's order desk buys a looney tunes product, everyone in both rooms will be notified that the disney order desk made that purchase, where I return the JSON items and it's details, store it, and do a few other funky things like link's to that particular order details page etc because those users have the correct permissions.
of course the verbiage will be different and handled differently but I am new to Redis and more familiar with MongoDB for this kind of thing, because I want to be able to save these notification items, in order to allow scrolling through all alerts at the user convenience, rather than simply popping up notification growls...
now... all of this is dynamic, because i won't know who is logging in from which establishment and are merely managing these by a set of rules bound in the code and the db.
thoughts?
You're a genius! I had some very complex ideas to handle to this. But you made it look like a child's play!