Gebraucht
NodeJS, Socket.io
Problem
Stellen Sie sich vor, es gibt zwei Benutzer U1 und U2 , die über Socket.io mit einer App verbunden sind. Der Algorithmus ist der folgende:
- U1 verliert die Internetverbindung vollständig (z. B. schaltet das Internet aus)
- U2 sendet eine Nachricht an U1 .
- U1 empfängt die Nachricht noch nicht, da das Internet nicht verfügbar ist
- Der Server erkennt die U1- Trennung durch Heartbeat-Timeout
- U1 stellt die Verbindung zu socket.io wieder her
- U1 empfängt die Nachricht nie von U2 - sie geht in Schritt 4 verloren, denke ich.
Mögliche Erklärung
Ich glaube ich verstehe warum es passiert:
- Schritt 4 auf Server tötet Socket - Instanz und die Warteschlange von Nachrichten an U1 sowie
- Darüber hinaus erstellen U1 und Server in Schritt 5 eine neue Verbindung (diese wird nicht wiederverwendet). Selbst wenn sich die Nachricht noch in der Warteschlange befindet, geht die vorherige Verbindung trotzdem verloren.
Brauchen Sie Hilfe
Wie kann ich diese Art von Datenverlust verhindern? Ich muss Hearbeats verwenden, weil ich nicht für immer Leute in der App hängen habe. Außerdem muss ich immer noch die Möglichkeit geben, die Verbindung wiederherzustellen, da ich bei der Bereitstellung einer neuen App-Version keine Ausfallzeiten möchte.
PS Das, was ich "Nachricht" nenne, ist nicht nur eine Textnachricht, die ich in der Datenbank speichern kann, sondern eine wertvolle Systemnachricht, deren Zustellung garantiert werden muss, oder die Benutzeroberfläche ist fehlerhaft.
Vielen Dank!
Zusatz 1
Ich habe bereits ein Benutzerkontosystem. Darüber hinaus ist meine Bewerbung bereits komplex. Das Hinzufügen von Offline- / Online-Status hilft nicht weiter, da ich solche Dinge bereits habe. Das Problem ist anders.
Check out Schritt 2. In diesem Schritt können wir technisch nicht sagen, ob U1 offline geht , er verliert nur die Verbindung, sagen wir für 2 Sekunden, wahrscheinlich wegen schlechten Internets. Also sendet U2 ihm eine Nachricht, aber U1 empfängt sie nicht, weil das Internet für ihn immer noch nicht verfügbar ist (Schritt 3). Schritt 4 ist erforderlich, um Offline-Benutzer zu erkennen. Das Zeitlimit beträgt beispielsweise 60 Sekunden. In weiteren 10 Sekunden ist die Internetverbindung für U1 hergestellt und er stellt die Verbindung zu socket.io wieder her. Die Nachricht von U2 geht jedoch im Speicherplatz verloren, da auf dem Server U1 durch Timeout die Verbindung getrennt wurde.
Das ist das Problem, ich will nicht 100% liefern.
Lösung
- Sammeln Sie eine Emission (Emit Name und Daten) in {} Benutzer, identifiziert durch zufällige emitID. Senden senden
- Bestätigen Sie die Ausgabe auf der Clientseite (senden Sie die Ausgabe mit der EmitID an den Server zurück).
- Wenn bestätigt - Objekt aus {} löschen, das durch emitID identifiziert wurde
- Wenn der Benutzer erneut verbunden ist - überprüfen Sie {} für diesen Benutzer und führen Sie eine Schleife durch, indem Sie Schritt 1 für jedes Objekt in {} ausführen
- Wenn getrennt oder / und verbunden, spülen Sie {} für den Benutzer, falls erforderlich
// Server
const pendingEmits = {};
socket.on('reconnection', () => resendAllPendingLimits);
socket.on('confirm', (emitID) => { delete(pendingEmits[emitID]); });
// Client
socket.on('something', () => {
socket.emit('confirm', emitID);
});
Lösung 2 (irgendwie)
Hinzugefügt am 1. Februar 2020.
Dies ist zwar keine wirkliche Lösung für Websockets, aber jemand findet sie möglicherweise dennoch praktisch. Wir sind von Websockets auf SSE + Ajax migriert. Mit SSE können Sie eine Verbindung von einem Client aus herstellen, um eine dauerhafte TCP-Verbindung aufrechtzuerhalten und Nachrichten von einem Server in Echtzeit zu empfangen. Um Nachrichten von einem Client an einen Server zu senden, verwenden Sie einfach Ajax. Es gibt Nachteile wie Latenz und Overhead, aber SSE garantiert Zuverlässigkeit, da es sich um eine TCP-Verbindung handelt.
Da wir Express verwenden, verwenden wir diese Bibliothek für SSE https://github.com/dpskvn/express-sse , aber Sie können diejenige auswählen, die zu Ihnen passt.
SSE wird in IE und den meisten Edge-Versionen nicht unterstützt, daher benötigen Sie eine Polyfüllung: https://github.com/Yaffle/EventSource .
Antworten:
Andere haben dies in anderen Antworten und Kommentaren angedeutet, aber das Hauptproblem besteht darin, dass Socket.IO nur ein Übermittlungsmechanismus ist und Sie sich für eine zuverlässige Übermittlung nicht allein darauf verlassen können. Die einzige Person, die sicher weiß, dass eine Nachricht erfolgreich an den Client übermittelt wurde, ist der Client selbst . Für diese Art von System würde ich die folgenden Aussagen empfehlen:
Abhängig von den Anforderungen Ihrer Anwendung können Sie natürlich Teile davon optimieren. Sie können beispielsweise eine Redis-Liste oder einen sortierten Satz für die Nachrichten verwenden und diese löschen, wenn Sie wissen, dass ein Client aktiv ist miteinander ausgehen.
Hier einige Beispiele:
Glücklicher Weg :
Offline-Pfad :
Wenn Sie unbedingt eine garantierte Lieferung wünschen, ist es wichtig, Ihr System so zu gestalten, dass die Verbindung keine Rolle spielt und die Lieferung in Echtzeit einfach ein Bonus ist . Dabei handelt es sich fast immer um einen Datenspeicher. Wie der Benutzer 568109 in einem Kommentar erwähnt hat, gibt es Nachrichtensysteme, die die Speicherung und Zustellung dieser Nachrichten abstrahieren, und es kann sich lohnen, eine solche vorgefertigte Lösung zu prüfen. (Sie müssen die Socket.IO-Integration wahrscheinlich noch selbst schreiben.)
Wenn Sie nicht daran interessiert sind, die Nachrichten in der Datenbank zu speichern, können Sie sie möglicherweise in einem lokalen Array speichern. Der Server versucht, U1 die Nachricht zu senden, und speichert sie in einer Liste "ausstehender Nachrichten", bis der Client von U1 bestätigt, dass er sie empfangen hat. Wenn der Client offline ist, kann er dem Server bei seiner Rückkehr mitteilen, dass "Hey, ich wurde getrennt, bitte senden Sie mir alles, was ich verpasst habe", und der Server kann diese Nachrichten durchlaufen.
Glücklicherweise bietet Socket.IO einen Mechanismus, mit dem ein Client auf eine Nachricht "antworten" kann, die wie native JS-Rückrufe aussieht. Hier ist ein 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 });
Dies ist dem letzten Vorschlag ziemlich ähnlich, einfach ohne einen dauerhaften Datenspeicher.
Vielleicht interessieren Sie sich auch für das Konzept des Event Sourcing .
quelle
Michelles Antwort ist ziemlich zutreffend, aber es gibt noch ein paar andere wichtige Dinge zu beachten. Die Hauptfrage, die Sie sich stellen müssen, lautet: "Gibt es einen Unterschied zwischen einem Benutzer und einem Socket in meiner App?" Eine andere Möglichkeit zu fragen ist: "Kann jeder angemeldete Benutzer mehr als eine Socket-Verbindung gleichzeitig haben?"
In der Web-Welt ist es wahrscheinlich immer möglich, dass ein einzelner Benutzer mehrere Socket-Verbindungen hat, es sei denn, Sie haben speziell etwas eingerichtet, das dies verhindert. Das einfachste Beispiel hierfür ist, wenn ein Benutzer zwei Registerkarten derselben Seite geöffnet hat. In diesen Fällen ist es Ihnen nicht wichtig, eine Nachricht / ein Ereignis nur einmal an den menschlichen Benutzer zu senden. Sie müssen sie an jede Socket-Instanz für diesen Benutzer senden, damit jede Registerkarte ihre Rückrufe ausführen kann, um den UI-Status zu aktualisieren. Vielleicht ist dies für bestimmte Anwendungen kein Problem, aber mein Bauch sagt, dass es für die meisten sein würde. Wenn dies ein Problem für Sie ist, lesen Sie weiter ....
Um dies zu lösen (vorausgesetzt, Sie verwenden eine Datenbank als dauerhaften Speicher), benötigen Sie 3 Tabellen.
Die Benutzertabelle ist optional, wenn Ihre App sie nicht benötigt, das OP jedoch angibt, dass sie eine hat.
Die andere Sache, die richtig definiert werden muss, ist "Was ist eine Socket-Verbindung?", "Wann wird eine Socket-Verbindung erstellt?", "Wann wird eine Socket-Verbindung wiederverwendet?". Der Psudocode von Michelle lässt den Eindruck entstehen, dass eine Socket-Verbindung wiederverwendet werden kann. Mit Socket.IO können sie NICHT wiederverwendet werden. Ich habe gesehen, dass es viel Verwirrung stiftet. Es gibt reale Szenarien, in denen Michelles Beispiel Sinn macht. Aber ich muss mir vorstellen, dass diese Szenarien selten sind. Was wirklich passiert, ist, wenn eine Socket-Verbindung unterbrochen wird, diese Verbindung, ID usw. niemals wiederverwendet werden. Nachrichten, die speziell für diesen Socket markiert sind, werden daher niemals an Dritte gesendet, da der Client, der ursprünglich eine Verbindung hergestellt hat, erneut eine Verbindung herstellt und eine völlig neue Verbindung und eine neue ID erhält. Das heißt es '
Für ein webbasiertes Beispiel wären hier die Schritte, die ich empfehlen würde:
Weil der letzte Schritt schwierig ist (zumindest war es früher so, ich habe so etwas schon lange nicht mehr gemacht) und weil es Fälle wie Stromausfall gibt, in denen der Client die Verbindung trennt, ohne die Client-Zeile zu bereinigen, und es nie versucht Um die Verbindung mit derselben Clientzeile wiederherzustellen, möchten Sie wahrscheinlich etwas, das regelmäßig ausgeführt wird, um veraltete Client- und Nachrichtenzeilen zu bereinigen. Oder Sie können einfach alle Clients und Nachrichten für immer dauerhaft speichern und ihren Status entsprechend markieren.
Um klar zu sein, fügen Sie in Fällen, in denen ein Benutzer zwei Registerkarten geöffnet hat, der Nachrichtentabelle zwei identische Nachrichten hinzu, die jeweils für einen anderen Client markiert sind, da Ihr Server wissen muss, ob jeder Client sie empfangen hat, nicht nur jeder Benutzer.
quelle
Es scheint, dass Sie bereits ein Benutzerkontosystem haben. Sie wissen, welches Konto online / offline ist. Sie können das Ereignis zum Verbinden / Trennen behandeln:
Die Lösung besteht also darin, für jeden Benutzer Online- / Offline- und Offline-Nachrichten zur Datenbank hinzuzufügen:
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); } })
quelle
Schauen Sie hier: Behandeln Sie Browser reload socket.io .
Ich denke, Sie könnten eine Lösung verwenden, die ich mir ausgedacht habe. Wenn Sie es richtig ändern, sollte es funktionieren, wie Sie wollen.
quelle
Ich denke, Sie möchten für jeden Benutzer einen wiederverwendbaren Socket haben, etwa:
Klient:
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. }
Dann bleibt die Socket-Warteschlange zwischen den Verbindungsabbrüchen erhalten.
Stellen Sie sicher, dass die
socket
Eigenschaften jedes Benutzers in der Datenbank gespeichert werden, und machen Sie die Methoden zu einem Teil Ihres Benutzerprototyps. Die Datenbank spielt keine Rolle, speichern Sie sie einfach, obwohl Sie Ihre Benutzer gespeichert haben.Dadurch wird das in Additon 1 erwähnte Problem vermieden, indem eine Bestätigung vom Client angefordert wird, bevor die Nachricht als gesendet markiert wird. Wenn Sie wirklich wollten, können Sie jeder Nachricht eine ID geben und den Client die Nachrichten-ID an senden lassen und diese
msg-conf
dann überprüfen.In diesem Beispiel
user
handelt es sich um den Vorlagenbenutzer, von dem alle Benutzer kopiert wurden, oder um den Benutzerprototyp.Hinweis: Dies wurde nicht getestet.
quelle
users.getUserByName("U2").send("Hi")
. Wenn dann U2 online ist, ist die socket.io von U2 nicht null, sodass die Nachricht gesendet wird. Wenn der Socket von U2 null ist, wird er in die Warteschlange gestellt, bis U2 online geht.this.socket.io
wird dies nicht der Fall seinnull
, und der Server wird versuchen, die Nachrichten zuzustellen.Wie bereits in einer anderen Antwort geschrieben, glaube ich auch, dass Sie die Echtzeit als Bonus betrachten sollten: Das System sollte auch ohne Echtzeit arbeiten können.
Ich entwickle einen Unternehmens-Chat für ein großes Unternehmen (iOS, Android, Web-Frontend und .net Core + PostGres-Backend) und nachdem ich eine Möglichkeit für den Websocket entwickelt habe, die Verbindung wiederherzustellen (über eine Socket-UUID) und nicht zugestellte Nachrichten zu erhalten (in einer Warteschlange gespeichert) Ich habe verstanden, dass es eine bessere Lösung gibt: Resync über die Rest-API.
Grundsätzlich habe ich Websocket nur für Echtzeit verwendet, mit einem Integer-Tag für jede Echtzeitnachricht (Benutzer online, Schreibmaschinen, Chat-Nachricht usw.) zur Überwachung verlorener Nachrichten.
Wenn der Client eine ID erhält, die nicht monolithisch ist (+1), versteht er, dass sie nicht synchron ist. Daher werden alle Socket-Nachrichten gelöscht und eine erneute Synchronisierung aller Beobachter über die REST-API angefordert.
Auf diese Weise können wir während des Offline-Zeitraums viele Variationen im Status der Anwendung verarbeiten, ohne beim erneuten Verbinden Tonnen von Websocket-Nachrichten hintereinander analysieren zu müssen, und wir sind sicher, synchronisiert zu werden (da das letzte Synchronisierungsdatum nur von der REST-API festgelegt wird , nicht aus der Steckdose).
Der einzige schwierige Teil ist die Überwachung auf Echtzeitnachrichten von dem Moment an, in dem Sie die REST-API aufrufen, bis zu dem Moment, in dem der Server antwortet, da das, was aus der Datenbank gelesen wird, Zeit benötigt, um zum Client zurückzukehren, und in der Zwischenzeit können Abweichungen auftreten, sodass sie zwischengespeichert werden müssen und berücksichtigt.
Wir gehen in ein paar Monaten in Produktion, ich hoffe, bis dahin wieder schlafen zu können :)
quelle
Ich habe mir dieses Zeug zuletzt angesehen und denke, ein anderer Weg könnte besser sein.
Schauen Sie sich den Azure Service-Bus an. Fragen und Themen kümmern sich um die Offline-Zustände. Die Nachricht wartet darauf, dass der Benutzer zurückkommt, und erhält dann die Nachricht.
Ist das Ausführen einer Warteschlange kostenintensiv, aber für eine einfache Warteschlange sind es etwa 0,05 US-Dollar pro Million Operationen, sodass die Entwicklungskosten höher sind als die Arbeitsstunden, die zum Schreiben eines Warteschlangensystems erforderlich sind. https://azure.microsoft.com/en-us/pricing/details/service-bus/
Und Azure Bus hat Bibliotheken und Beispiele für PHP, C #, Xarmin, Anjular, Java Script usw.
Der Server sendet also eine Nachricht und muss sich nicht um deren Verfolgung kümmern. Der Client kann Nachrichten zum Zurücksenden verwenden, um bei Bedarf auch den Lastausgleich zu übernehmen.
quelle
Versuchen Sie diese Emit-Chat-Liste
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'); };
quelle