web-dev-qa-db-fra.com

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

Utilisé

NodeJS, Socket.io

Problème

Imaginez qu'il y a 2 utilisateurs 1 & 2, connectés à une application via Socket.io. L'algorithme est le suivant:

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

Explication possible

Je pense que je comprends pourquoi cela se produit:

  • à l'étape 4 Server tue l'instance de socket et la file d'attente de messages à 1 également
  • De plus, à l'étape 5 1 et Server créez une nouvelle connexion (elle n'est pas réutilisée), donc même si le message est toujours en file d'attente, la connexion précédente est de toute façon perdue.

Besoin d'aide pour

Comment éviter ce type de perte de données? Je dois utiliser des battements de cœur, car je ne bloque pas les applications pour toujours. De plus, je dois toujours donner la possibilité de me reconnecter, car lorsque je déploie une nouvelle version de l'application, je ne veux aucun temps d'arrêt.

P.S. La chose que j'appelle "message" n'est pas seulement un message texte que je peux stocker dans la base de données, mais un message système précieux, dont la livraison doit être garantie, ou une interface utilisateur foireuse.

Merci!


Addition 1

J'ai déjà un système de compte utilisateur. De plus, ma candidature est déjà complexe. L'ajout de statuts hors ligne/en ligne n'aidera pas, car j'ai déjà ce genre de choses. Le problème est différent.

Découvrez l'étape 2. Sur cette étape, nous avons techniquement ne peut pas dire si U1 se déconnecte, il perd juste la connexion, disons pendant 2 secondes, probablement à cause d'un mauvais Internet. U2 lui envoie donc 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, le délai d'attente est de 60 secondes. Finalement, dans une autre connexion Internet de 10 secondes pour U1 est en place 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.

Voilà le problème, je ne veux pas une livraison à 100%.


Solution

  1. Collectez une émission (nom et données d'émission) dans l'utilisateur {}, identifiée par emitID aléatoire. Envoyer émettre
  2. Confirmez l'émission sur le côté client (renvoyez l'émission sur le serveur avec emitID)
  3. Si confirmé - supprimer 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 dans {}
  5. En cas de déconnexion ou/et de rinçage connecté {} pour l'utilisateur si nécessaire

    // Constante du serveur en attenteEmits = {};

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

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

69
igorpavlov

D'autres ont fait allusion à cela dans d'autres réponses et commentaires, mais le problème racine est que Socket.IO est juste un mécanisme de livraison, et vous ne pouvez pas dépendre de lui seul pour une livraison fiable. La seule personne qui sait avec certitude qu'un message a bien été remis au client est le client lui-même . Pour ce type de système, je recommanderais de faire 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 raté" 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, selon les besoins de votre application, vous pouvez en régler certaines parties - par exemple, vous pouvez utiliser, par exemple, une liste Redis ou un ensemble trié pour les messages, et les effacer si vous savez pertinemment qu'un client est en place à 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 doit recevoir.
  • Le serveur stocke le message dans une sorte de stockage persistant, le marquant pour U1 avec une sorte d'horodatage ou d'ID séquentiel.
  • Le serveur envoie le message à U1 via Socket.IO.
  • Le client de U1 confirme (peut-être 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 ligne :

  • U1 perd la connectivité Internet.
  • U2 envoie un message au serveur que U1 doit recevoir.
  • Le serveur stocke le message dans une sorte de stockage persistant, le marquant pour U1 avec une sorte d'horodatage ou d'ID 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 messages supplémentaires; 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 que j'ai raté."
  • Le serveur envoie à U1 tous les messages manqués du magasin de données sur la base 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 voulez absolument une livraison garantie, il est important de concevoir votre système de manière à ce que la connexion n'ait pas d'importance et que la livraison en temps réel soit simplement un bonus ; cela implique presque toujours un magasin de données d'une certaine sorte. Comme user568109 l'a mentionné dans un commentaire, il existe des systèmes de messagerie qui résument le stockage et la livraison desdits messages, et il peut être utile d'étudier une telle solution préconfigurée. (Vous devrez probablement encore écrire l'intégration Socket.IO vous-même.)

Si vous n'êtes pas intéressé par le stockage des messages dans la base de données, vous pourrez peut-être vous en sortir en les stockant 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 reçu. Si le client est hors ligne, à son retour, il peut indiquer au serveur "Hé, j'étais 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 un pseudocode:

// 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énements .

81
Michelle Tilley

Il semble que vous disposiez déjà d'un système de compte utilisateur. Vous savez quel compte est en ligne/hors ligne, vous pouvez gérer l'événement de connexion/déconnexion:

La solution est donc d'ajouter des messages en ligne/hors ligne et hors ligne sur la base de données pour chaque utilisateur:

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

La réponse de Michelle est à peu près exacte, mais il y a quelques autres choses importantes à considérer. La principale question à vous poser est: "Y a-t-il une différence entre un utilisateur et un socket dans mon application?" Une autre façon de demander est "Est-ce que chaque utilisateur connecté peut avoir plus d'une connexion socket à la fois?"

Dans le monde du Web, il est probablement toujours possible qu'un seul utilisateur dispose de plusieurs connexions de socket, sauf si vous avez spécifiquement mis en place quelque chose qui empêche cela. L'exemple le plus simple est celui où un utilisateur a deux onglets de la même page ouverts. Dans ces cas, vous ne vous souciez pas d'envoyer un message/événement à l'utilisateur humain une seule fois ... vous devez l'envoyer à chaque instance de socket pour cet utilisateur afin que chaque onglet puisse exécuter ses rappels pour mettre à jour l'état de l'interface utilisateur. Peut-être que ce n'est pas un problème pour certaines applications, mais mon instinct dit que ce serait pour la plupart. Si cela vous inquiète, lisez la suite ...

Pour résoudre ce problème (en supposant que vous utilisez une base de données comme stockage persistant), vous aurez besoin de 3 tables.

  1. utilisateurs - qui est un 1 à 1 avec de vraies personnes
  2. clients - qui représente un "onglet" qui pourrait avoir une seule connexion à un serveur de socket. (tout 'utilisateur' peut en avoir plusieurs)
  3. messages - un message qui doit être envoyé à un client (pas un message qui doit être envoyé à un utilisateur ou à une socket)

Le tableau des utilisateurs est facultatif si votre application n'en a pas besoin, mais l'OP a indiqué qu'il en avait un.

L'autre élément qui doit être correctement défini est "qu'est-ce qu'une connexion socket?", "Quand est-ce qu'une connexion socket est créée?", "Quand une connexion socket est-elle réutilisée?". Le psudocode de Michelle donne l'impression qu'une connexion de socket peut être réutilisée. Avec Socket.IO, ils NE PEUVENT PAS être réutilisés. J'ai vu être la source de beaucoup de confusion. Il existe des scénarios réels où l'exemple de Michelle a du sens. Mais je dois imaginer que ces scénarios sont rares. Ce qui se passe vraiment, c'est quand une connexion socket est perdue, que la connexion, l'ID, etc. ne seront jamais réutilisés. Ainsi, tous les messages marqués spécifiquement pour ce socket ne seront jamais remis à personne, car lorsque le client qui s'était connecté à l'origine se reconnecte, il obtient une toute nouvelle connexion et un nouvel ID. Cela signifie que c'est à vous de faire quelque chose pour suivre les clients (plutôt que les sockets ou les utilisateurs) sur plusieurs connexions de socket.

Donc, pour un exemple basé sur le Web, voici l'ensemble des étapes que je recommanderais:

  • Lorsqu'un utilisateur charge un client (généralement une seule page Web) qui a le potentiel de créer une connexion socket, ajoutez une ligne à la base de données des clients qui est liée à leur ID utilisateur.
  • Lorsque l'utilisateur se connecte réellement au serveur de socket, transmettez l'ID client au serveur avec la demande de connexion.
  • Le serveur doit valider que l'utilisateur est autorisé à se connecter et la ligne client dans la table clients est disponible pour la connexion et autoriser/refuser en conséquence.
  • Mettez à jour la ligne client avec l'ID de socket généré par Socket.IO.
  • Envoyez tous les éléments du tableau des messages connectés à l'ID client. Il n'y en aurait pas lors de la connexion initiale, mais si cela venait du client essayant de se reconnecter, il pourrait y en avoir.
  • Chaque fois qu'un message doit être envoyé à ce socket, ajoutez une ligne dans le tableau des messages qui est liée à l'ID client que vous avez généré (pas l'ID de socket).
  • Essayez d'émettre le message et écoutez le client avec l'accusé de réception.
  • Lorsque vous obtenez l'accusé de réception, supprimez cet élément du tableau des messages.
  • Vous souhaiterez peut-être créer une logique côté client qui supprime les messages en double envoyés depuis le serveur, car c'est techniquement une possibilité, comme certains l'ont souligné.
  • Ensuite, lorsqu'un client se déconnecte du serveur de socket (volontairement ou par erreur), NE supprimez PAS la ligne client, effacez simplement l'ID de socket au maximum. En effet, ce même client peut essayer de se reconnecter.
  • Lorsqu'un client tente de se reconnecter, envoyez le même ID client qu'il a envoyé avec la tentative de connexion d'origine. Le serveur affichera cela comme une connexion initiale.
  • Lorsque le client est détruit (l'utilisateur ferme l'onglet ou s'éloigne), c'est lorsque vous supprimez la ligne client et tous les messages pour ce client. Cette étape peut être un peu délicate.

Parce que la dernière étape est délicate (au moins c'était le cas, je n'ai rien fait de tel depuis longtemps), et parce qu'il y a des cas comme une coupure de courant où le client se déconnecte sans nettoyer la ligne client et n'essaye jamais pour vous reconnecter avec cette même ligne client - vous souhaiterez probablement avoir quelque chose qui s'exécute périodiquement pour nettoyer les lignes client et message périmées. Ou, vous pouvez simplement stocker de façon permanente tous les clients et messages pour toujours et simplement marquer leur état de manière appropriée.

Donc, pour être clair, dans les cas où un utilisateur a deux onglets ouverts, vous ajouterez deux messages identiques à la table des messages, chacun marqué pour un client différent, car votre serveur doit savoir si chaque client les a reçus, pas seulement chaque utilisateur.

1
Ryan

Essayez cette liste d'émission de chat

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
Oladimeji Ajeniya

Ce que je pense que vous voulez, c'est d'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 de socket est préservée entre les déconnexions.

Assurez-vous qu'il enregistre la propriété socket de chaque utilisateur dans la base de données et intégrez les méthodes à votre prototype utilisateur. La base de données n'a pas d'importance, enregistrez-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 vouliez vraiment, vous pourriez attribuer un identifiant à chaque message et demander au client d'envoyer l'identifiant du message à msg-conf, puis vérifiez-le.

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

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

0
Ari Porad

Regardez ici: gérer le rechargement du navigateur socket.io .

Je pense que vous pourriez utiliser la solution que j'ai trouvée. Si vous le modifiez correctement, il devrait fonctionner comme vous le souhaitez.

0
Are Wojciechowski

J'ai regardé ces trucs dernièrement et je pense qu'un chemin différent pourrait être mieux.

Essayez de regarder le bus Azure Service, les ques et le sujet, prenez soin des états hors ligne. Le message attend que l'utilisateur revienne, puis il reçoit le message.

Est un coût pour exécuter une file d'attente, mais c'est comme 0,05 $ par million d'opérations pour une file d'attente de base, de sorte que le coût du développement serait supérieur aux heures de travail nécessaires pour écrire un système de file d'attente. https://Azure.Microsoft.com/en-us/pricing/details/service-bus/

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

Le serveur envoie donc un message et n'a pas à se soucier de leur suivi. Le client peut également utiliser un message pour le renvoyer car il peut gérer l'équilibrage de charge si nécessaire.

0
True Solutions