Skip to content

Affidabilità del trasporto Websocket (perdita di dati Socket.io durante la riconnessione)

Non devi più indagare di più su internet perché sei nello spazio giusto, abbiamo la soluzione che devi trovare e senza problemi.

Soluzione:

Altri hanno accennato a questo in altre risposte e commenti, ma il problema di fondo è che Socket.IO è solo un meccanismo di consegna, e voi non si può dipendere solo da esso per una consegna affidabile. L'unica persona che sa con certezza che un messaggio è stato consegnato al client con successo è il cliente stesso. Per questo tipo di sistema, consiglierei di fare le seguenti asserzioni:

  1. I messaggi non vengono inviati direttamente ai client; vengono invece inviati al server e memorizzati in un qualche tipo di archivio dati.
  2. I client sono responsabili di chiedere "cosa mi sono perso" quando si riconnettono, e interrogano i messaggi memorizzati nell'archivio dati per aggiornare il loro stato.
  3. Se un messaggio viene inviato al server mentre il client destinatario è connesso, quel messaggio sarà inviato in tempo reale al client.

Naturalmente, a seconda delle esigenze dell'applicazione, si possono regolare alcune parti di questo sistema: per esempio, si può usare un elenco Redis o un insieme ordinato per i messaggi e cancellarli se si sa per certo che un client è aggiornato.


Ecco un paio di esempi:

Percorso felice:

  • U1 e U2 sono entrambi collegati al sistema.
  • U2 invia al server un messaggio che U1 dovrebbe ricevere.
  • Il server memorizza il messaggio in un qualche tipo di archivio persistente, contrassegnandolo per U1 con un qualche tipo di timestamp o ID sequenziale.
  • Il server invia il messaggio a U1 tramite Socket.IO.
  • Il client di U1 conferma (forse tramite un callback Socket.IO) di aver ricevuto il messaggio.
  • Il server cancella il messaggio persistente dall'archivio dati.

Percorso offline:

  • U1 perde la connettività Internet.
  • U2 invia al server un messaggio che U1 dovrebbe ricevere.
  • Il server memorizza il messaggio in un qualche tipo di archivio persistente, contrassegnandolo per U1 con un qualche tipo di timestamp o ID sequenziale.
  • Il server invia il messaggio a U1 tramite Socket.IO.
  • Il client di U1 non conferma la ricezione, perché è offline.
  • Forse U2 invia a U1 qualche altro messaggio; tutti vengono memorizzati nell'archivio dati allo stesso modo.
  • Quando U1 si ricollega, chiede al server: "L'ultimo messaggio che ho visto è stato X / ho lo stato X, cosa mi sono perso".
  • Il server invia a U1 tutti i messaggi che gli sono sfuggiti dall'archivio dati in base alla richiesta di U1.
  • Il client di U1 conferma la ricezione e il server rimuove i messaggi dall'archivio dati.

Se si vuole assolutamente una consegna garantita, allora è importante progettare il sistema in modo tale che la connessione non sia importante e che la consegna in tempo reale sia semplicemente una bonus; questo comporta quasi sempre un archivio di dati di qualche tipo. Come ha detto l'utente 568109 in un commento, esistono sistemi di messaggistica che astraggono dalla memorizzazione e dalla consegna dei messaggi, e potrebbe valere la pena di esaminare una soluzione preconfezionata di questo tipo. (Probabilmente si dovrà comunque scrivere da soli l'integrazione di Socket.IO).

Se non si è interessati a memorizzare i messaggi nel database, si può fare a meno di memorizzarli in un array locale; il server cerca di inviare il messaggio a U1 e lo memorizza in un elenco di "messaggi in attesa" finché il client di U1 non conferma di averlo ricevuto. Se il client è offline, quando torna può dire al server "Ehi, sono stato disconnesso, per favore mandami tutto quello che mi sono perso" e il server può iterare i messaggi.

Fortunatamente, Socket.IO fornisce un meccanismo che consente a un client di "rispondere" a un messaggio che assomiglia a callback JS nativi. Ecco un po' di pseudocodice:

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

Questo è abbastanza simile all'ultimo suggerimento, semplicemente senza un archivio dati persistente.


Potrebbe interessarvi anche il concetto di event sourcing.

La risposta di Michelle è praticamente esatta, ma ci sono altre cose importanti da considerare. La domanda principale da porsi è: "C'è differenza tra un utente e un socket nella mia applicazione?". Un altro modo per chiederlo è: "Ogni utente connesso può avere più di una connessione socket alla volta?".

Nel mondo del web è probabilmente sempre possibile che un singolo utente abbia più connessioni socket, a meno che non si sia specificamente messo in atto qualcosa che lo impedisca. L'esempio più semplice è quello di un utente che ha due schede della stessa pagina aperte. In questi casi non ci si preoccupa di inviare un messaggio/evento all'utente umano una sola volta... è necessario inviarlo a ogni istanza di socket per quell'utente, in modo che ogni scheda possa eseguire le proprie callback per aggiornare lo stato dell'interfaccia utente. Forse questo non è un problema per alcune applicazioni, ma il mio istinto mi dice che lo è per la maggior parte di esse. Se questo è un problema per voi, continuate a leggere....

Per risolvere questo problema (supponendo di utilizzare un database come memoria persistente) sono necessarie 3 tabelle.

  1. utenti - che è un 1 a 1 con le persone reali
  2. clienti - che rappresenta una "scheda" che potrebbe avere una singola connessione a un server socket. (ogni "utente" può avere più connessioni)
  3. messaggi - un messaggio che deve essere inviato a un client (non un messaggio che deve essere inviato a un utente o a un socket)

La tabella degli utenti è opzionale se l'applicazione non la richiede, ma l'OP ha detto di averne una.

L'altra cosa che deve essere definita correttamente è "cos'è una connessione socket?", "quando viene creata una connessione socket?", "quando viene riutilizzata una connessione socket?". Il codice psudocode di Michelle fa sembrare che una connessione socket possa essere riutilizzata. Con Socket.IO, non possono essere riutilizzate. Ho visto che è fonte di molta confusione. Ci sono scenari reali in cui l'esempio di Michelle ha senso. Ma devo immaginare che questi scenari siano rari. Ciò che accade realmente è che quando una connessione socket viene persa, tale connessione, ID, ecc. non verrà mai riutilizzata. Quindi tutti i messaggi contrassegnati per quel socket non saranno mai consegnati a nessuno, perché quando il client che si era connesso originariamente si riconnette, ottiene una connessione completamente nuova e un nuovo ID. Questo significa che è necessario fare qualcosa per tracciare i client (piuttosto che i socket o gli utenti) attraverso connessioni multiple ai socket.

Quindi, per un esempio basato sul web, ecco la serie di passi che raccomanderei:

  • Quando un utente carica un client (in genere una singola pagina web) che può creare una connessione socket, aggiungere una riga al database dei client collegata al suo ID utente.
  • Quando l'utente si connette effettivamente al server socket, passare l'ID del client al server con la richiesta di connessione.
  • Il server deve verificare che l'utente sia autorizzato a connettersi e che la riga del client nella tabella dei client sia disponibile per la connessione e consentire/negare di conseguenza.
  • Aggiornare la riga del client con l'ID del socket generato da Socket.IO.
  • Inviare tutti gli elementi della tabella dei messaggi collegati all'ID del client. All'inizio della connessione non ce ne sono, ma se il client cerca di riconnettersi, potrebbero essercene.
  • Ogni volta che è necessario inviare un messaggio a quel socket, aggiungere una riga nella tabella dei messaggi collegata all'ID del client generato (non all'ID del socket).
  • Tentare di emettere il messaggio e ascoltare il client con la conferma di ricezione.
  • Quando si ottiene la conferma, cancellare la voce dalla tabella dei messaggi.
  • Si potrebbe voler creare una logica sul lato client che scarti i messaggi duplicati inviati dal server, dato che è tecnicamente una possibilità, come alcuni hanno sottolineato.
  • Poi, quando un client si disconnette dal server socket (di proposito o per errore), NON cancellare la riga del client, ma solo l'ID del socket. Questo perché lo stesso client potrebbe provare a riconnettersi.
  • Quando un client tenta di riconnettersi, inviare lo stesso ID client inviato con il tentativo di connessione originale. Il server lo considererà come una connessione iniziale.
  • Quando il client viene distrutto (l'utente chiude la scheda o si allontana), è il momento di eliminare la riga del client e tutti i messaggi per questo client. Questo passaggio può essere un po' complicato.

Poiché l'ultimo passaggio è complicato (almeno lo era, non faccio nulla di simile da molto tempo) e poiché ci sono casi, come la perdita di corrente, in cui il client si disconnette senza ripulire la riga del client e non tenta mai di riconnettersi con la stessa riga del client, è probabile che si voglia avere qualcosa che venga eseguito periodicamente per ripulire tutte le righe di client e messaggi obsolete. Oppure, si possono memorizzare permanentemente tutti i client e i messaggi per sempre e contrassegnare il loro stato in modo appropriato.

Quindi, per essere chiari, nel caso in cui un utente abbia due schede aperte, si aggiungeranno due messaggi identici alla tabella dei messaggi, ciascuno contrassegnato per un client diverso, perché il server deve sapere se ogni client li ha ricevuti, non solo ogni utente.

Ci piacerebbe se potessi condividere questa sezione se ti è stata d'aiuto.



Utilizzate il nostro motore di ricerca

Ricerca
Generic filters

Lascia un commento

Il tuo indirizzo email non sarà pubblicato.