node.js - tutorial - socket.io npm




Fiabilité du transport Websocket(perte de données Socket.io lors de la reconnexion) (4)

Utilisé

NodeJS, Socket.io

Problème

Imaginez deux utilisateurs U1 et U2 connectés à une application via Socket.io. L'algorithme est le suivant:

  1. U1 perd complètement la connexion Internet (par ex. Éteint Internet)
  2. U2 envoie un message à U1 .
  3. U1 ne reçoit pas encore le message car Internet est en panne
  4. Le serveur détecte la déconnexion de U1 par timeout
  5. U1 se reconnecte à socket.io
  6. U1 ne reçoit jamais le message de U2 - il est perdu à l'étape 4, je suppose.

Explication possible

Je pense que je comprends pourquoi cela se produit:

  • à l'étape 4, le serveur tue également l'instance de socket et la file d'attente de messages vers U1
  • De plus, à l'étape 5, U1 et le serveur créent une nouvelle connexion (celle-ci n'est pas réutilisée). Ainsi, même si le message est toujours en file d'attente, la connexion précédente est perdue.

Besoin d'aide pour

Comment puis-je prévenir ce type de perte de données? Je dois utiliser des battements de coeur, parce que je ne suis pas suspendu pour toujours dans l'application. Je dois également donner la possibilité de se reconnecter, car lorsque je déploie une nouvelle version de l'application, je ne souhaite aucun temps mort.

PS Ce que j'appelle "message" n'est pas simplement un message texte que je peux stocker dans une base de données, mais un message système précieux, dont la livraison doit être garantie ou l'interface utilisateur gâchée.

Merci!

Ajout 1

J'ai déjà un système de compte utilisateur. De plus, mon application est déjà complexe. Ajouter des statuts hors connexion / en ligne ne m'aidera pas, car j'ai déjà ce genre de choses. Le problème est différent

Vérifiez l'étape 2. Sur cette étape, nous ne pouvons techniquement pas dire si U1 se déconnecte , il perd simplement la connexion, laisse-le-dire pendant 2 secondes, probablement à cause d'un mauvais Internet. Donc, U2 lui envoie un message, mais U1 ne le reçoit pas car Internet est toujours en panne pour lui (étape 3). L'étape 4 est nécessaire pour détecter les utilisateurs hors ligne. Disons que le délai d'attente est de 60 secondes. Finalement, dans 10 secondes supplémentaires, la connexion Internet de U1 est terminée et il se reconnecte à socket.io. Mais le message de U2 est perdu dans l’espace car sur le serveur U1 a été déconnecté par timeout.

C'est le problème, je ne veux pas la livraison à 100%.

Solution

  1. Recueillir un utilisateur (nom et données) dans {} utilisateur, identifié par random emitID. Envoyer émettre
  2. Confirmez l'émission côté client (renvoyez l'émission au serveur avec emitID)
  3. Si confirmé - supprime l'objet de {} identifié par emitID
  4. Si l'utilisateur est reconnecté, recherchez {} pour cet utilisateur et parcourez-le en exécutant l'étape 1 pour chaque objet de {}.
  5. En cas de déconnexion ou / et de connexion {} pour l'utilisateur si nécessaire

    // Serveur constant waitingEmits = {};

    socket.on ('reconnection', () => resendAllPendingLimits); socket.on ('confirm', (emitID) => {delete (waitingEmits [emitID]);});

    // Client socket.on ('quelque chose', () => {socket.emit ('confirm', emitID);});


Ce que je pense que vous voulez, c'est avoir un socket réutilisable pour chaque utilisateur, quelque chose comme:

Client:

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

Serveur:

// 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.
}

Ensuite, la file d'attente est préservée entre les déconnexions.

Assurez-vous que la propriété de socket chaque utilisateur est enregistrée dans la base de données et que les méthodes en font partie intégrante. La base de données n’a pas d’importance, sauvegardez-la simplement comme vous avez sauvegardé vos utilisateurs.

Cela évitera le problème mentionné dans l'Additon 1 en exigeant une confirmation du client avant de marquer le message comme envoyé. Si vous le souhaitiez vraiment, vous pouvez attribuer un identifiant à chaque message et demander au client de l'envoyer à msg-conf , puis de le vérifier.

Dans cet exemple, user est l'utilisateur modèle à partir duquel tous les utilisateurs sont copiés, ou similaire au prototype d'utilisateur.

Remarque: cela n'a pas été testé.


D'autres ont fait allusion à cela dans d'autres réponses et commentaires, mais le problème fondamental est que Socket.IO n'est qu'un mécanisme de diffusion, et vous ne pouvez pas compter uniquement sur lui pour une diffusion fiable. La seule personne qui sait avec certitude qu'un message a été remis avec succès au client est le client lui-même . Pour ce type de système, je recommanderais les affirmations suivantes:

  1. Les messages ne sont pas envoyés directement aux clients; au lieu de cela, ils sont envoyés au serveur et stockés dans une sorte de magasin de données.
  2. Les clients sont responsables de demander "qu'est-ce que j'ai manqué" lorsqu'ils se reconnectent et interrogeront les messages stockés dans le magasin de données pour mettre à jour leur état.
  3. Si un message est envoyé au serveur alors que le client destinataire est connecté, ce message sera envoyé en temps réel au client.

Bien sûr, en fonction des besoins de votre application, vous pouvez ajuster certains éléments. Par exemple, vous pouvez utiliser une liste Redis ou un ensemble trié pour les messages, et les effacer si vous savez qu'un client est actif. à ce jour.

Voici quelques exemples:

Chemin heureux :

  • U1 et U2 sont tous deux connectés au système.
  • U2 envoie un message au serveur que U1 devrait recevoir.
  • Le serveur stocke le message dans une sorte de magasin persistant, le marquant pour U1 avec une sorte d'estampille temporelle ou d'identifiant séquentiel.
  • Le serveur envoie le message à U1 via Socket.IO.
  • Le client de U1 confirme (éventuellement via un rappel Socket.IO) qu'il a reçu le message.
  • Le serveur supprime le message persistant du magasin de données.

Chemin hors connexion :

  • U1 perd la connexion Internet.
  • U2 envoie un message au serveur que U1 devrait recevoir.
  • Le serveur stocke le message dans une sorte de magasin persistant, le marquant pour U1 avec une sorte d'estampille temporelle ou d'identifiant séquentiel.
  • Le serveur envoie le message à U1 via Socket.IO.
  • Le client de U1 ne confirme pas la réception car il est hors ligne.
  • Peut-être que U2 envoie à U1 quelques autres messages; ils sont tous stockés dans le magasin de données de la même manière.
  • Lorsque U1 se reconnecte, il demande au serveur "Le dernier message que j'ai vu était X / J'ai l'état X. Qu'est-ce qui me manque?"
  • Le serveur envoie à U1 tous les messages qu'il a manqués à partir du magasin de données en fonction de la demande de U1.
  • Le client de U1 confirme la réception et le serveur supprime ces messages du magasin de données.

Si vous souhaitez absolument bénéficier d'une livraison garantie, il est important de concevoir votre système de manière à ce que la connexion ne soit pas un problème, et que la livraison en temps réel est tout simplement un bonus . cela implique presque toujours un magasin de données. Comme l'utilisateur 5668109 l'a mentionné dans un commentaire, il existe des systèmes de messagerie qui soustraient le stockage et la livraison desdits messages, et il peut être intéressant de se pencher sur une telle solution préconfigurée. (Vous devrez probablement écrire vous-même l'intégration Socket.IO.)

Si vous ne souhaitez pas stocker les messages dans la base de données, vous pourrez peut-être les stocker dans un tableau local. le serveur essaie d'envoyer le message à U1 et le stocke dans une liste de "messages en attente" jusqu'à ce que le client de U1 confirme qu'il l'a bien reçu. Si le client est hors ligne, à son retour, il peut dire au serveur "Hé, j’ai été déconnecté, envoyez-moi tout ce que j’ai manqué" et le serveur peut parcourir ces messages.

Heureusement, Socket.IO fournit un mécanisme qui permet à un client de "répondre" à un message qui ressemble à des rappels JS natifs. Voici quelques pseudocodes:

// server
pendingMessagesForSocket = [];

function sendMessage(message) {
  pendingMessagesForSocket.push(message);
  socket.emit('message', message, function() {
    pendingMessagesForSocket.remove(message);
  }
};

socket.on('reconnection', function(lastKnownMessage) {
  // you may want to make sure you resend them in order, or one at a time, etc.
  for (message in pendingMessagesForSocket since lastKnownMessage) {
    socket.emit('message', message, function() {
      pendingMessagesForSocket.remove(message);
    }
  }
});

// client
socket.on('connection', function() {
  if (previouslyConnected) {
    socket.emit('reconnection', lastKnownMessage);
  } else {
    // first connection; any further connections means we disconnected
    previouslyConnected = true;
  }
});

socket.on('message', function(data, callback) {
  // Do something with `data`
  lastKnownMessage = data;
  callback(); // confirm we received the message
});

Ceci est assez similaire à la dernière suggestion, simplement sans magasin de données persistant.

Vous pouvez également être intéressé par le concept de sourcing d’événement .


Je regarde ce genre de choses dernièrement et je pense qu'un chemin différent pourrait être meilleur.

Essayez de consulter Azure Service Bus, les questions et les sujets qui traitent des états hors ligne. Le message attend que l'utilisateur revienne, puis il reçoit le message.

C’est un coût pour exécuter une file d’attente, mais c’est comme à 0,05 dollar par million d’opérations pour une file d’attente de base, donc le coût du dev serait plus élevé en heures de travail que d’écrire un système de file d’attente. https://azure.microsoft.com/en-us/pricing/details/service-bus/

Et azure bus a des bibliothèques et des exemples pour PHP, C #, Xarmin, Anjular, Java Script, etc.

Donc, le serveur envoie un message et n'a pas besoin de s'inquiéter de le suivi. Le client peut utiliser le message pour le renvoyer car un moyen peut gérer l’équilibrage de la charge si nécessaire.






socket.io