13

I've the following code working in my server-side, it's all ok. But, I want to keep the same connection between n tabs, because when I open a new tab, looks like I've disconnected from the first tab... So, how can I keep the same connection?

client.js

socket.emit("connected", {user: inputUser.val()};

app.js

var express = require("express"),
app = express(),
http = require("http").Server(app),
io = require("socket.io")(http),
users = {};

io.on("connection", function(socket) {
    socket.on("connected", function(data) {
        socket.user = data.user;

        users[socket.user] = socket;

        updateUsers();
    });

    function updateUsers() {
        io.emit("users", Object.keys(users));
    }

    socket.on("typing", function(data) {
        var userMsg = data.user;

        if(userMsg in users) {
            users[userMsg].emit("typing", {user: socket.user});
        }
    });

    socket.on("disconnect", function(data) {
        if(!socket.user) {
            return;
        }

        delete users[socket.user];

        updateUsers();
    });

});

var port = Number(process.env.PORT || 8000);

http.listen(port, function() {
    console.log("Server running on 8000!");
});

Update: The typing event above works fine... So I tried the typing event according to the answer:

var express = require("express"),
    app = express(),
    http = require("http").Server(app),
    io = require("socket.io")(http),
    users = {};

io.on("connection", function(socket) {
    socket.on("connected", function(data) {
        socket.user = data.user;

        // add this socket to the Set of sockets for this user
        if (!users[socket.user]) {
            users[socket.user] = new Set();
        }
        users[socket.user].add(socket);

        updateUsers();
    });

    function updateUsers() {
        io.emit("users", Object.keys(users));
    }

    socket.on("typing", function(data) {
        var userMsg = data.user;

        if(userMsg in users) {
            users[userMsg].emit("typing", {user: socket.user});
        }
    });

    socket.on("disconnect", function(data) {
        if(!socket.user) {
            return;
        }

        // remove socket for this user
        // and remove user if socket count hits zero
        if (users[socket.user]) {
            users[socket.user].delete(socket);
            if (users[socket.user].size === 0) {
                delete users[socket.user];
            }
        }

        updateUsers();
    });

});

var port = Number(process.env.PORT || 8000);

http.listen(port, function() {
    console.log("Server running on 8000!");
});

But it is giving the following error:

users[userMsg].emit("typing", {user: socket.user}); ^

TypeError: users[userMsg].emit is not a function

Update²: To fix the typing event error, I just changed to:

socket.on("typing", function(data) {
    var userMsg = data.user;

    if(userMsg in users) {
        for(let userSet of users[userMsg]) {
            userSet.emit("typing", {user: socket.user});
        }
    }
});
Igor
  • 645
  • 4
  • 16
  • 37
  • Possible duplicate of [Manage multiple tabs (but same user) in socket.io](https://stackoverflow.com/questions/12166187/manage-multiple-tabs-but-same-user-in-socket-io) – StefansArya Dec 15 '17 at 07:09

5 Answers5

17

There is no simple way to share a single socket.io connection among multiple tabs in the same browser. The usual model for multiple tabs would be that each tab just has its own socket.io connection.

The opening of a new tab and a new socket.io connection should not, on its own, cause your server to think anything was disconnected. If your code is doing that, then that is a fault in your code and it is probably easier to fix that particular fault.

In fact, if you want to explicitly support multiple tabs and be able to recognize that multiple tabs may all be used by the same user, then you may want to change your server side code so that it can keep track of multiple sockets for a single user, rather than how it is currently coded to only keep track of one socket per user.

If your server code is really just trying to keep track of which users online, then there's probably an easier way to do that by referencing counting each user. I will post a code example in a bit.

var express = require("express"),
app = express(),
http = require("http").Server(app),
io = require("socket.io")(http),
users = {};

io.on("connection", function(socket) {
    socket.on("connected", function(data) {
        socket.user = data.user;

        // increment reference count for this user
        if (!users[socket.user]) {
            users[socket.user] = 0;
        }
        ++users[socket.user];

        updateUsers();
    });

    function updateUsers() {
        io.emit("users", Object.keys(users));
    }

    socket.on("disconnect", function(data) {
        if(!socket.user) {
            return;
        }

        // decrement reference count for this user
        // and remove user if reference count hits zero
        if (users.hasOwnProperty(socket.user)) {
            --users[socket.user];
            if (users[socket.user] === 0) {
                delete users[socket.user];
            }
        }

        updateUsers();
    });

});

var port = Number(process.env.PORT || 8000);

http.listen(port, function() {
    console.log("Server running on 8000!");
});

If you need the users object to have the socket object in it, then you can change what is stored in the users object to be a Set of sockets like this:

var express = require("express"),
app = express(),
http = require("http").Server(app),
io = require("socket.io")(http),
users = {};

io.on("connection", function(socket) {
    socket.on("connected", function(data) {
        socket.user = data.user;

        // add this socket to the Set of sockets for this user
        if (!users[socket.user]) {
            users[socket.user] = new Set();
        }
        users[socket.user].add(socket);

        updateUsers();
    });

    function updateUsers() {
        io.emit("users", Object.keys(users));
    }

    socket.on("disconnect", function(data) {
        if(!socket.user) {
            return;
        }

        // remove socket for this user
        // and remove user if socket count hits zero
        if (users[socket.user]) {
            users[socket.user].delete(socket);
            if (users[socket.user].size === 0) {
                delete users[socket.user];
            }
        }

        updateUsers();
    });

});

var port = Number(process.env.PORT || 8000);

http.listen(port, function() {
    console.log("Server running on 8000!");
});
jfriend00
  • 683,504
  • 96
  • 985
  • 979
  • Thanks for the answer, I'm trying to fire a `typing` event, but without success... I've updated my code in the question... How can I fix it? – Igor Dec 08 '16 at 03:09
  • 4
    @Igor - I put a lot of effort into my answer to exactly what you originally asked and now you've changed the question. Please do not do that. Try to put your ***real question*** in the original question so you don't change things up on people. I'm guessing that `var userMsg = data.user;` should probably be `var userMsg = socket.user;` and then if you're using the `Set` solution, you have to change `users[userMsg].emit(...)` to iterate through the set and send to each socket in the set if you're trying to send the same messages to all the tabs open by that user. – jfriend00 Dec 08 '16 at 03:14
  • I'm sorry, I recognize that the effort of your response and how good it was, but now I tried to implement this event and it did not work, so I came back to ask for help... – Igor Dec 08 '16 at 03:18
  • Now it works! I updated my question with the correction. Thank you very much for your answer and attention, and I apologize again. Thank you very much! – Igor Dec 08 '16 at 03:45
  • @jfriend00 How to send to each socket (many tabs, same user)? isnt users[userMsg ].emit() send to one tab only ? – user3304007 Jan 27 '17 at 15:02
  • 1
    @user3304007 - As my answer attempts to explain, if you want to send to multiple tabs, you must keep track of what user each incoming connection belongs to and then when you want to send to all connections for a user, you have to iterate through the current connections and send to all connections from that user. It would be multiple `.emit()` calls. Or, you could put each connection from a given user into a room and use one `.emit()` to the room. – jfriend00 Jan 27 '17 at 17:16
  • 5 years later and this is still useful. I'm working with Python, but just the idea of adding to the users connections on connect and subtracting on disconnect really helped. – Sachin Raja Jan 12 '21 at 03:15
  • But when i close on tab from multiple tabs i am disconnected from all open tabs. – Viral Jadav Apr 15 '21 at 07:38
4

For anyone still having this issue. here is how i fixed it. let me explain. once the page refreshes or a new tab is opened, socket dosen't really care so it opens a new connection every time . this is more of a advantage than disadvantage. the best way to tackle the issue is on the server side, once a user logs in with his or her user name , you can send that name along with the query options on the client so it can be used as a unique identifier. in my case i used a token

this.socket = io.connect(`${environment.domain}` , {
      query: {token: this.authservice.authToken}
    });

then on the server side you can create an empty array to a key and an array of values. the username of the user will be used as a key and the corresponding array of socket as the value. in my own case like i said i used a token

const users = [ ]
socket.nickname = (decoded token username);
users[socket.nickname] = [socket];

then you can perform a simple logic to check if a user already exists in an array, if it does, push the new socket to the array of the user

if ( user.username in users) {
                    console.log('already exists')
                    users[user.username].push(socket);
                } 

if it dosent, just create a new key and add the socket as the key.(make sure its an array because a user can always refresh or open a new tab with the same account and you dont want the chat message to deliver in one tab and not deliver in another)

else {
                    socket.nickname = username;
                    users[socket.nickname] = [socket];

                }

then to emit a message you simply loop through the array and emit the message accordingly. this way each tab gets the message

socket.on('chat', (data) => {

            if (data.to in users) { 

                for(let i = 0; i < users[data.to].length; i++) {
                    users[data.to][i].emit('chat', data)
                }
                for(let i = 0; i < users[data.user].length; i++) {
                    users[data.user][i].emit('chat', data)
                }


            }
       })

you can add a disconnect logic to remove the socket from the users array too to save memory, so only currently open tabs acre active and closed tabs are removed. i hope it solved your problem

ayotycoon
  • 199
  • 2
  • 12
  • I made a mix of what you said and what @jfriend00 said just above and it allows me to understand. But this is a touchy issue, only for good developers ;) – Thomas Aumaitre May 20 '20 at 13:40
  • I am using this system for non logged user. Do you think the IP address is a good alternative ? if the guy uses a tablet, a smartphone, and a Mac on the same wifi is it gonna be alright ? I am gonna test when I get motivated lol – Thomas Aumaitre May 20 '20 at 13:44
  • @ThomasAumaitre wsorry for the late reply, i dndnt get notifications. Well if you are using a non-logged in user, you might as well emit directly to the socketId of that user – ayotycoon Jul 01 '20 at 12:54
1

My solution is joining socket to a room with specific user Id.

io.on('connection', async (socket) => {

socket.join('user:' + socket.handshake.headers.uid) // The right way is getting `uid` from cookie/token and verifying user


})

One advantage is sending data to specific user (sending to all tabs)

io.to('user:' + uid).emit('hello');

Hope it's helpful!

Amir Movahedi
  • 1,802
  • 3
  • 29
  • 52
0

I belive the best way is create a channel for the user and unique it by their ID, so, when you need to receive or send something you use the channel and every socket connected to it will receive.

0

Another solution is to save the flag to localStorage and use eventListener to change localStorage. Do not connect when another connection exists. and save message in local storage for send with master tab.