使用 Socket.io 处理多个选项卡

Working with multiple tabs with Socket.io

我的服务器端有以下代码,一切正常。但是,我想在 n 个选项卡之间保持相同的连接,因为当我打开一个新选项卡时,看起来我已经与第一个选项卡断开连接......那么,我怎样才能保持相同的连接?

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!");
});

更新: 上面的 typing 事件工作正常......所以我根据答案尝试了 typing 事件:

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!");
});

但出现以下错误:

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

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

更新²: 为了修复 typing 事件错误,我只是更改为:

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});
        }
    }
});

没有简单的方法可以在同一浏览器的多个选项卡之间共享单个 socket.io 连接。多个选项卡的通常模型是每个选项卡都有自己的 socket.io 连接。

新选项卡的打开和新的 socket.io 连接本身不应导致您的服务器认为任何东西已断开连接。如果您的代码这样做,那么这是您的代码中的一个错误,修复该特定错误可能更容易。

事实上,如果您想明确支持多个选项卡并且能够识别多个选项卡可能全部由同一用户使用,那么您可能需要更改您的服务器端代码,以便它可以跟踪单个用户的多个套接字,而不是当前编码为仅跟踪每个用户一个套接字的方式。

如果您的服务器代码真的只是试图跟踪哪些用户在线,那么可能有一种更简单的方法,即对每个用户进行引用计数。我稍后会 post 一个代码示例。

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!");
});

如果您需要 users 对象在其中包含 socket 对象,那么您可以将用户对象中存储的内容更改为 Set 套接字,如下所示:

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!");
});

对于仍然遇到此问题的任何人。这是我修复它的方法。 让我解释。 一旦页面刷新或打开新选项卡,socket 并不关心,因此它每次都会打开一个新连接。这利大于弊。解决该问题的最佳方法是在服务器端,一旦用户使用他或她的用户名登录,您可以将该名称与客户端的查询选项一起发送,以便将其用作唯一标识符。在我的例子中,我使用了一个令牌

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

然后在服务器端,您可以为一个键和一个值数组创建一个空数组。用户的用户名将用作键,相应的套接字数组用作值。在我自己的情况下,就像我说的,我使用了一个令牌

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

然后你可以执行一个简单的逻辑来检查用户是否已经存在于数组中,如果存在,则将新套接字推送到用户的数组中

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

如果它不发送,只需创建一个新密钥并将套接字添加为密钥。(确保它是一个数组,因为用户总是可以使用相同的帐户刷新或打开一个新选项卡,而您不想要聊天消息在一个选项卡中传送而不在另一个选项卡中传送)

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

                }

然后要发出消息,您只需循环遍历数组并相应地发出消息。这样每个选项卡都会收到消息

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)
                }


            }
       })

您也可以添加断开连接逻辑以从用户数组中删除套接字以节省内存,因此仅删除当前打开的选项卡和关闭的选项卡。我希望它解决了你的问题

我认为最好的方法是为用户创建一个通道并通过他们的 ID 唯一化它,因此,当您需要接收或发送某些内容时,您可以使用该通道,并且连接到它的每个套接字都会收到。

另一种解决方案是将标志保存到localStorage 并使用eventListener 更改localStorage。 存在另一个连接时不要连接。 并将消息保存在本地存储中,以便通过主选项卡发送。

我的解决方案是将套接字连接到具有特定用户 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


})

一个优点是向特定用户发送数据(发送到所有选项卡)

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

希望对您有所帮助!