Websocket transport reliability (Socket.io data loss during reconnection)

前端 未结 8 2318
梦谈多话
梦谈多话 2020-12-02 04:58

Used

NodeJS, Socket.io

Problem

Imagine there are 2 users U1 & U2, connected to an app via Socket.io. The al

相关标签:
8条回答
  • 2020-12-02 05:16

    Michelle's answer is pretty much on point, but there are a few other important things to consider. The main question to ask yourself is: "Is there a difference between a user and a socket in my app?" Another way to ask that is "Can each logged in user have more than 1 socket connection at one time?"

    In the web world it is probably always a possibility that a single user has multiple socket connections, unless you have specifically put something in place that prevents this. The simplest example of this is if a user has two tabs of the same page open. In these cases you don't care about sending a message/event to the human user just once... you need to send it to each socket instance for that user so that each tab can run it's callbacks to update the ui state. Maybe this isn't a concern for certain applications, but my gut says it would be for most. If this is a concern for you, read on....

    To solve this (assuming you are using a database as your persistent storage) you would need 3 tables.

    1. users - which is a 1 to 1 with real people
    2. clients - which represents a "tab" that could have a single connection to a socket server. (any 'user' may have multiple)
    3. messages - a message that needs sent to a client (not a message that needs sent to a user or to a socket)

    The users table is optional if your app doesn't require it, but the OP said they have one.

    The other thing that needs properly defined is "what is a socket connection?", "When is a socket connection created?", "when is a socket connection reused?". Michelle's psudocode makes it seem like a socket connection can be reused. With Socket.IO, they CANNOT be reused. I've seen be the source of a lot of confusion. There are real life scenarios where Michelle's example does make sense. But I have to imagine those scenarios are rare. What really happens is when a socket connection is lost, that connection, ID, etc will never be reused. So any messages marked for that socket specifically will never be delivered to anyone because when the client who had originally connected, reconnects, they get a completely brand new connection and new ID. This means it's up to you to do something to track clients (rather than sockets or users) across multiple socket connections.

    So for a web based example here would be the set of steps I'd recommend:

    • When a user loads a client (typically a single webpage) that has the potential for creating a socket connection, add a row to the clients database which is linked to their user ID.
    • When the user actually does connect to the socket server, pass the client ID to the server with the connection request.
    • The server should validate the user is allowed to connect and the client row in the clients table is available for connection and allow/deny accordingly.
    • Update the client row with the socket ID generated by Socket.IO.
    • Send any items in the messages table connected to the client ID. There wouldn't be any on initial connection, but if this was from the client trying to reconnect, there may be some.
    • Any time a message needs to be sent to that socket, add a row in the messages table which is linked to the client ID you generated (not the socket ID).
    • Attempt to emit the message and listen for the client with the acknowledgement.
    • When you get the acknowledgement, delete that item from the messages table.
    • You may wish to create some logic on the client side that discards duplicate messages sent from the server since this is technically a possibility as some have pointed out.
    • Then when a client disconnects from the socket server (purposefully or via error), DO NOT delete the client row, just clear out the socket ID at most. This is because that same client could try to reconnect.
    • When a client tries to reconnect, send the same client ID it sent with the original connection attempt. The server will view this just like an initial connection.
    • When the client is destroyed (user closes the tab or navigates away), this is when you delete the client row and all messages for this client. This step may be a bit tricky.

    Because the last step is tricky (at least it used to be, I haven't done anything like that in a long time), and because there are cases like power loss where the client will disconnect without cleaning up the client row and never tries to reconnect with that same client row - you probably want to have something that runs periodically to cleanup any stale client and message rows. Or, you can just permanently store all clients and messages forever and just mark their state appropriately.

    So just to be clear, in cases where one user has two tabs open, you will be adding two identical message to the messages table each marked for a different client because your server needs to know if each client received them, not just each user.

    0 讨论(0)
  • 2020-12-02 05:16

    Try this emit chat list

    io.on('connect', onConnect);
    
    function onConnect(socket){
    
      // sending to the client
      socket.emit('hello', 'can you hear me?', 1, 2, 'abc');
    
      // sending to all clients except sender
      socket.broadcast.emit('broadcast', 'hello friends!');
    
      // sending to all clients in 'game' room except sender
      socket.to('game').emit('nice game', "let's play a game");
    
      // sending to all clients in 'game1' and/or in 'game2' room, except sender
      socket.to('game1').to('game2').emit('nice game', "let's play a game (too)");
    
      // sending to all clients in 'game' room, including sender
      io.in('game').emit('big-announcement', 'the game will start soon');
    
      // sending to all clients in namespace 'myNamespace', including sender
      io.of('myNamespace').emit('bigger-announcement', 'the tournament will start soon');
    
      // sending to individual socketid (private message)
      socket.to(<socketid>).emit('hey', 'I just met you');
    
      // sending with acknowledgement
      socket.emit('question', 'do you think so?', function (answer) {});
    
      // sending without compression
      socket.compress(false).emit('uncompressed', "that's rough");
    
      // sending a message that might be dropped if the client is not ready to receive messages
      socket.volatile.emit('maybe', 'do you really need it?');
    
      // sending to all clients on this node (when using multiple nodes)
      io.local.emit('hi', 'my lovely babies');
    
    };

    0 讨论(0)
  • 2020-12-02 05:18

    It is seem that you already have user account system. You know which account is online/offline, you you can handle connect/disconnect event:

    So the solution is, add online/offline and offline messages on database for each user:

    chatApp.onLogin(function (user) {
       user.readOfflineMessage(function (msgs) {
           user.sendOfflineMessage(msgs, function (err) {
               if (!err) user.clearOfflineMessage();
           });
       })
    });
    
    chatApp.onMessage(function (fromUser, toUser, msg) {
       if (user.isOnline()) {
          toUser.sendMessage(msg, function (err) {
              // alert CAN NOT SEND, RETRY?
          });
       } else {
          toUser.addToOfflineQueue(msg);
       }
    })
    
    0 讨论(0)
  • 2020-12-02 05:21

    Look here: Handle browser reload socket.io.

    I think you could use solution which I came up with. If you modify it properly, it should work as you want.

    0 讨论(0)
  • 2020-12-02 05:23

    What I think you want is to have a reusable socket for each user, something like:

    Client:

    socket.on("msg", function(){
        socket.send("msg-conf");
    });
    

    Server:

    // Add this socket property to all users, with your existing user system
    user.socket = {
        messages:[],
        io:null
    }
    user.send = function(msg){ // Call this method to send a message
        if(this.socket.io){ // this.io will be set to null when dissconnected
            // Wait For Confirmation that message was sent.
            var hasconf = false;
            this.socket.io.on("msg-conf", function(data){
                // Expect the client to emit "msg-conf"
                hasconf = true;
            });
            // send the message
            this.socket.io.send("msg", msg); // if connected, call socket.io's send method
            setTimeout(function(){
                if(!hasconf){
                    this.socket = null; // If the client did not respond, mark them as offline.
                    this.socket.messages.push(msg); // Add it to the queue
                }
            }, 60 * 1000); // Make sure this is the same as your timeout.
    
        } else {
            this.socket.messages.push(msg); // Otherwise, it's offline. Add it to the message queue
        }
    }
    user.flush = function(){ // Call this when user comes back online
        for(var msg in this.socket.messages){ // For every message in the queue, send it.
            this.send(msg);
        }
    }
    // Make Sure this runs whenever the user gets logged in/comes online
    user.onconnect = function(socket){
        this.socket.io = socket; // Set the socket.io socket
        this.flush(); // Send all messages that are waiting
    }
    // Make sure this is called when the user disconnects/logs out
    user.disconnect = function(){
        self.socket.io = null; // Set the socket to null, so any messages are queued not send.
    }
    

    Then the socket queue is preserved between disconnects.

    Make sure it saves each users socket property to the database and make the methods part of your user prototype. The database does not matter, just save it however you have been saving your users.

    This will avoid the problem mentioned in Additon 1 by requiring a confirmation from the client before marking the message as sent. If you really wanted to, you could give each message an id and have the client send the message id to msg-conf, then check it.

    In this example, user is the template user that all users are copied from, or like the user prototype.

    Note: This has not been tested.

    0 讨论(0)
  • 2020-12-02 05:26

    Been looking at this stuff latterly and think different path might be better.

    Try looking at Azure Service bus, ques and topic take care of the off line states. The message wait for user to come back and then they get the message.

    Is a cost to run a queue but its like $0.05 per million operations for a basic queue so cost of dev would be more from hours work need to write a queuing system. https://azure.microsoft.com/en-us/pricing/details/service-bus/

    And azure bus has libraries and examples for PHP, C#, Xarmin, Anjular, Java Script etc.

    So server send message and does not need to worry about tracking them. Client can use message to send back also as means can handle load balancing if needed.

    0 讨论(0)
提交回复
热议问题