Wie gehe ich mit Paketverlust in einem Client-Server-Netzwerkmodell um?


7

In einem Client-Server-Netzwerkmodell senden die Clients nur Befehle an den Server (dh Koordinaten eines Klicks, einer Feuerwaffe usw.), und der Server führt diese Befehle dann aus, um einen Spielstatus zu erzeugen.

Was aber, wenn der Client einen Befehl wie "Feuerwaffe" in einem Paket sendet, das verworfen wird? Hat der Benutzer die Erfahrung gemacht, dass er auf seinen Feuerknopf getippt hat, ohne dass etwas passiert, oder gibt es eine intelligente Möglichkeit, um sicherzustellen, dass solche Befehle vom Server unabhängig vom Paketverlust empfangen werden?

Das heißt, der alte Befehl "Feuerwaffe" wird mehrmals gesendet, um sicherzustellen, dass er zumindest irgendwann ausgelöst wird (Kennzeichnung des Befehls, damit der Server versteht, dass es sich um denselben Befehl handelt, der mehrmals gesendet wurde, und nicht um einen Befehl, der immer wieder ausgelöst wird). .

Ich habe mir die Dokumentation zu Quake3-, Unreal- und Source-Netzwerkmodellen angesehen, kann jedoch zu diesem speziellen Problem nichts finden. Ich glaube jedoch, dass sie es gelöst haben, weil ich mein AWP nie abgefeuert habe, ohne dass es tatsächlich geschossen hat. Wenn es eine Verzögerung gab, hat es nur eine kurze Zeit gedauert, bis es geschossen hat, aber ich denke, es hat nie den Befehl verloren.

BEARBEITEN: Ich habe hier eine Ressource gefunden, die einen Weg erklärt, wie Sie damit umgehen können: http://gafferongames.com/networking-for-game-programmers/reliability-and-flow-control/

Sie lösen es, indem sie eine Art loses ACK über UDP hinzufügen. Ich bin jedoch der Meinung, dass es einen klügeren Weg geben sollte, dies zu tun. Diese Art der Handhabung ist für den Server in Ordnung, denke ich, der hauptsächlich sicherstellen muss, dass er eine Live-Verbindung zum hat, und um Dinge wie Chat-Nachrichten zu behandeln (die nicht wirklich zeitkritisch sind, aber nicht verloren gehen dürfen), aber Ich glaube, dass ein ACK für den Client zu viel Verzögerung verursacht, wenn ein Paket verloren geht (zumindest eine volle Latenzzeit).


Ich glaube, die richtige Antwort lautet "Nicht". Der Grund, warum UDP für so etwas wie CS / LOL / ETC gut ist, ist, dass Sie ständig Statusaktualisierungen senden, die den vorherigen Status überschreiben. Sehr ähnlich einem Film-Stream. Es ist ein Kompromiss zwischen Latenz und Garantie, dass der Serverstatus perfekt geliefert wird und minimale Latenz die Priorität für Sie zu sein scheint.
ClassicThunder

@ClassicThunder Für die Server-> Client-Kommunikation wirkt UDP aufgrund der Art der von Ihnen beschriebenen Spielzustände Wunder. Der Client sendet jedoch keine Spielzustände an den Server, sondern Befehle. Diese sind unterschiedlicher Natur, da sie den vorherigen Befehl nicht überschreiben, sondern additiv darauf aufbauen. Daher ist der Paketverlust ein großes Problem bei der Client-> Server-Kommunikation.
Willem

Ein loses ACK über UDP wird immer noch ohne große Änderungen verwendet, da es funktioniert und sich bewährt hat. Sie fragen, wie Sie sicherstellen können, dass der Server eine Nachricht vom Client empfängt, ohne diese Quittung jemals an den Client zurückzusenden. Wenn Sie UDP mit ACK oder TCP verwendet haben und es als unbefriedigend empfunden haben, unter welchen Bedingungen wird es Ihren Anforderungen nicht gerecht? Ich bin alle für akademische Untersuchungen, aber dies scheint ein Fall vorzeitiger Optimierung zu sein.
LLL79

Antworten:


8

Es frustriert mich immer wieder, wie viele Leute die Unterschiede zwischen TCP und UDP zu stark vereinfachen, da "TCP zuverlässig, aber langsam ist, während UDP schnell, aber unzuverlässig ist".

Darum geht es bei TCP und UDP nicht. TCP und UDP sind zwei verschiedene Abstraktionen über IP und werden für verschiedene Zwecke verwendet. Beide sehr gut auf ihrem Gebiet.

TCP ist ein Stream- orientiertes Protokoll. Sie verwenden TCP, wenn Sie viele sequentielle Daten von einem Punkt zu einem anderen übertragen möchten, um den Durchsatz zu maximieren (dh die Zeit zu minimieren, die Sie zum Senden aller Daten benötigen). Das automatische erneute Senden verlorener Datagramme und das Neuordnen sind eine der Funktionen von TCP, aber TCP bietet auch Stateful-Verbindungen, Flusskontrolle und Überlastungskontrolle. Diese Funktionen machen TCP zu einem wirklich guten Werkzeug für die Übertragung großer Datenmengen von einem Punkt zum anderen. Protokolle wie HTTP und FTP, bei denen Sie so schnell wie möglich große Datenmengen senden möchten, verwenden TCP.

UDP hingegen ist ein nachrichtenorientiertes Protokoll. Sie verwenden UDP, wenn Sie Nachrichten von einem Punkt zu einem oder mehreren Punkten senden möchten, um die Latenz zu minimieren ( dh die Zeit, die zwischen dem Senden einer Nachricht bis zum Empfang benötigt wird). Um die Latenz zu minimieren, implementiert UDP keine erneuten Übertragungen oder Neuordnungen von Datagrammen. Wenn Sie jedoch möchten, dass Ihre nachrichtenbasierte Anwendung Neuübertragungen und / oder Neuordnungen unterstützt, können Sie dies selbst implementieren, und das ist eigentlich ganz einfach! Darüber hinaus gibt es eine Reihe lustiger Dinge, die Sie in UDP tun können, die Sie mit TCP einfach nicht tun können, wie Multicast- und UDP-Locher.

TCP und UDP sind beide großartige Technologien, die für verschiedene Zwecke gedacht sind. Sie möchten sich also fragen, ob die Kommunikation in Ihrem Programm besser als Stream von Bytes oder als eine Reihe von Nachrichten dargestellt wird, und eine gute Faustregel ist:

  • Wenn es in Ihrem Programm um das Senden von Nachrichten geht, verwenden Sie UDP.

  • Wenn Ihr Programm einen Bytestrom senden soll, verwenden Sie TCP.

So sollte die Entscheidung zwischen TCP und UDP aussehen, nicht über Zuverlässigkeit, was nur einer der vielen Unterschiede zwischen ihnen ist und meiner Meinung nach der am wenigsten wichtige von allen.

So wie es möglich ist, eine Stream-Abstraktion über UDP durchzuführen, ist es möglich (und leider sehr häufig), eine Nachrichtenabstraktion über TCP durchzuführen.

Wenn Sie sich für eine Nachrichtenabstraktion über TCP entscheiden, denken Sie daran, dass Sie TCP so verwenden müssen, wie es nicht beabsichtigt war, was bedeutet, dass Sie auf einige Schwierigkeiten stoßen werden. Bestimmtes:

  • Nachrichtengrenzen: TCP behandelt Daten als einen sehr langen Bytestrom und hat daher kein Konzept für Pakete oder Nachrichten. Wenn Sie Nachrichten über TCP senden möchten, müssen Sie eine Art Nachrichtenkopf erstellen, der die Länge jeder Nachricht enthält.

  • Pufferung beim Senden: TCP erwartet, dass Sie Daten so schnell wie möglich produzieren können. TCP-Datagramme können sehr groß sein. Wenn die Netzwerkbedingungen gut sind, puffert ein TCP-Stapel Ihre Bytes, bis ein sehr großes Datagramm gesendet werden kann, wodurch der Durchsatz maximiert wird. Dies ist eine gute Sache für Streams, aber nicht so sehr für Nachrichten. Einmal zu oft wird der Puffer bei jedem Sendevorgang geleert oder es werden Optionen wie und verwendet TCP_NODELAY, und selbst dann haben Sie keine Garantie. Um den verstorbenen Richard Stevens zu zitieren :

2.11. Wie kann ich einen Socket zwingen, die Daten in seinem Puffer zu senden?

Du kannst es nicht erzwingen. Zeitraum. TCP entscheidet selbst, wann es Daten senden kann. Normalerweise sendet TCP beim Aufrufen von write () an einem TCP-Socket zwar ein Segment, es gibt jedoch keine Garantie und keine Möglichkeit, dies zu erzwingen. Es gibt viele Gründe, warum TCP kein Segment sendet: Ein geschlossenes Fenster und der Nagle-Algorithmus sind zwei Dinge, die sofort in den Sinn kommen.

  • Pufferung beim Empfang: Selbst wenn Sie es schaffen, den Puffer bei jedem Senden zu leeren, gibt es keine Garantie dafür, dass Sie für jede von Ihnen gesendete Spülung einen Empfang erhalten. Es ist durchaus möglich, dass Sie mehrere Nachrichten in einem einzigen recvAnruf erhalten, da diese am Empfangsende gepuffert wurden (insbesondere in Netzwerken mit hohen Paketverlustraten). Sie müssen dann eine Nachrichtenempfangswarteschlange implementieren, was ziemlich dumm ist, wenn Sie TCP verwenden.

  • Heartbeats und Erkennung von Verbindungsabbrüchen: TCP verfügt über einen eigenen Mechanismus zum Erkennen von Verbindungsabbrüchen, die jedoch für Anwendungen zur Nachrichtenübermittlung normalerweise zu langsam sind (das nicht konfigurierbare Keep-Alive-Datagramm wird etwa alle zwei Stunden gesendet). Einmal zu oft implementieren die Leute ihre eigenen Hearbeat-Protokolle im selben TCP-Stream und fragen dann "wie man das Schließen von TIME_WAIT erzwingt", "wie man eine geschlossene Verbindung erkennt" oder ähnliches.

Keines dieser Probleme tritt auf, wenn Sie Ihr nachrichtenorientiertes Programm mit UDP erstellen:

  • Nachrichtengrenzen: Jede Nachricht ist ein separates UDP-Datagramm.

  • Pufferung beim Senden: Existiert nicht. Ein sendtoAnruf sendet sofort ein Datagramm.

  • Pufferung beim Empfang: Existiert nicht. Sie erhalten nur ein Datagramm pro recvfromAnruf.

  • Erkennung von Verbindungsabbrüchen: UDP ist nicht verbindungsorientiert. Sie können ein einfaches Timeout mit Ihren eigenen Parametern implementieren, um zu berücksichtigen, dass ein Client nicht mehr vorhanden ist.

Wenn Sie TCP für die Nachrichtenübermittlung verwenden möchten, weil Sie der Meinung sind, dass dies einfach ist, tun Sie dies nicht.

Für das gefürchtete Zuverlässigkeitsproblem können Sie nun eine einfache Zuverlässigkeit implementieren, indem Sie zwei Felder in jede Nachricht einfügen: Das erste ist eine permanent ansteigende Nachrichten-ID und das zweite ist die größte Nachrichten-ID, die Sie von einem bestimmten Client erhalten haben. Wenn sich Ihre interne Anzahl zu stark von der vom Kunden angegebenen Anzahl unterscheidet, senden Sie sie erneut ab der ersten Nachricht, die der Kunde nicht erhalten hat. Mit Wrapover können Sie dies mit nur zwei zusätzlichen Bytes pro Nachricht tun.

Das Beste an Ihrer eigenen Zuverlässigkeitsschicht ist, dass Sie sie auf Ihre Bedürfnisse abstimmen können: Einige Nachrichtentypen sind möglicherweise zuverlässig, andere ohne. Sie können verschiedene Kanäle haben, jeder mit seinen eigenen Sequenznummern, Sie können diese Nummern verwenden, um Verzögerungen vorherzusagen, und viele andere großartige Dinge.

Lauf nicht vor UDP weg, weil andere dir gesagt haben, "es ist schwer". Sie haben wahrscheinlich nie (ernsthaft) UDP verwendet und haben wahrscheinlich nicht einmal (ernsthaft) TCP verwendet. Tatsächlich ist UDP viel einfacher und verständlicher als TCP.

Lesen Sie Stevens 'Unix-Netzwerkprogrammierung. Und dann noch einmal lesen.


Ich habe UDP ausgewählt, weil möglicherweise unzuverlässige Nachrichten vorliegen und es sich um Nachrichten handelt. Ich glaube, dass WebRTC (was ich verwenden werde) die Flusskontrolle implementiert, sodass ich nicht viel Logik auf meine Netzwerkimplementierung setzen muss, außer den Besonderheiten des Spielnetzwerkmodells.
Willem

1
Dieser Beitrag enthält viele interne Informationen zu Protokollen. Ich kann einfach nicht damit einverstanden sein, neuen Programmierern zu raten, TCP-Mechanismen unter Verwendung von UDP als Basis erneut zu implementieren. Dieser Rat ist der gleiche, den Sie neuen Programmierern empfohlen haben, reines C anstelle von Java zu verwenden, da "Sie sich keine Gedanken über Klassen machen müssen" und "es ist schneller, weil es keine Speicherbereinigung gibt".
Wondra

1
@wondra: Sie sagen also nur, dass Benutzer UDP-Funktionen wie Nachrichtengrenzen neu implementieren und TCP-Funktionen wie Pufferung vollständig missbrauchen sollten, nur wegen einer TCP-Funktion, an der Sie interessiert sind? Der Vergleich von TCP mit UDP beim Vergleich von Java mit C ist falsch: Sowohl TCP als auch UDP sind Protokolle der Transportschicht auf hoher Ebene. Wenn ein Vergleich durchgeführt werden soll, wäre dies zwischen Java und Scheme: Sie können sicherlich etwas tun, das der funktionalen Programmierung in Java ähnelt, sowie etwas, das OOP in Scheme ähnelt, aber es gibt bessere Tools, um das zu tun, was Sie tun möchten.
Panda Pyjama

@wondra: Außerdem gibt es nichts "internes" oder niedriges an dem, was ich geschrieben habe. Stream-orientiert zu sein ist das Hauptmerkmal von TCP, und Nachrichtenorientierung ist das Hauptmerkmal von UDP. Dies ist keine obskure Information.
Panda Pyjama

Die grundlegende Schnittstelle für TCP und UDP ist in vielen Sprachen ziemlich gleich. Was nicht in der Schnittstelle ist, ist intern. Ich sage nur, dass Sie mich niemals davon überzeugen werden, viele Zeilen für die Fehlerprüfung (Bestätigung, Nummerierung und CRC) zu implementieren. Warum sollten Sie die Garbage Collection in ANSI C implementieren, um etwas mehr Leistung (= besser für das einzelne Programm optimiert) gegenüber Java zu erzielen? Dies ist die Frage, die jeder für sich selbst beantworten muss. Ich sage nein zur vorzeitigen Optimierung (zumindest für Anfänger). Wenn er fragen muss, wie es geht, ist er ein Anfänger.
Wondra

1

Um Paketverluste zu behandeln, müssen Sie zunächst entscheiden, welches Netzwerkprotokoll Sie verwenden möchten: TCP oder UDP.

  • TCP: ist zuverlässig, sendet aber nur langsam einfache Nachrichten (hat eine höhere Latenz)

  • UDP: hat eine geringe Latenz, aber um Ihr Ziel zu erreichen, dass der Benutzer einige Paketverluste nicht bemerkt, müssen Sie eine zusätzliche Schicht entwickeln, die Ihre Kommunikation verlangsamt.

Diese zusätzliche Schicht entspricht dem Unterschied zwischen UDP x TCP.

Sie müssen die Details des TCP-Protokolls studieren und viele Funktionen implementieren. Sie müssen mindestens ACK-Pakete und Paketbestellungen entwickeln. Wenn Ihre Ebene nicht gut entwickelt ist, kann sie sogar langsamer als TCP sein.

Vielleicht können Sie beide Protokolle im selben Spiel mischen.

UDP sollte nur verwendet werden, wenn Sie Daten liefern, die problemlos verloren gehen können. Wenn Sie beispielsweise das Paket "Feind 1 befindet sich auf Feld A" verpassen, ist es kein großes Problem, wenn Sie die Position des Feindes von Zeit zu Zeit wiederholen. Wenn Sie das zweite Paket mit "Feind 1 ist auf Feld A" oder "Feind 1 ist auf Feld B" erhalten, spielt es keine Rolle, wo es vorher war. Sie platzieren den Feind einfach auf dem zuletzt empfangenen Feld.

Wenn Sie nicht möchten, dass Ihr Player irgendwelche Schüsse verliert, verwenden Sie TCP (oder eine zuverlässige modifizierte UDP-Nachricht).


Aber wenn ich ACK nicht verwenden möchte? Ich möchte nur eine gewisse Redundanz einbauen, damit Befehle nicht übersehen werden, es sei denn, der Spieler bleibt stark zurück. Oder könnte es sein, dass das Client-Server-Netzwerkmodell häufig TCP für die Client-> Server-Kommunikation von Befehlen verwendet, während UDP für die Server-> Client-Kommunikation von vollständigen Spielzuständen verwendet wird (die ohne Probleme verloren gehen können)?
Willem

Sie können die Verwendung eines ACK-Algorithmus (oder TCP) vermeiden und einen eigenen Algorithmus für Redundanz entwickeln, aber das würde ich nicht vorschlagen. Ist die gleiche Situation, wenn jemand beschließt, seinen eigenen kryptografischen Algorithmus zu erstellen, anstatt einen weit verbreiteten wie RSA zu verwenden.
Zanon

Mein Vorschlag ist, TCP (für Informationen, die Sie nicht verpassen dürfen) mit UDP (für Informationen, die keine Probleme verursachen, wenn Sie sie verpassen) zu mischen.
Zanon

Das ist ein guter Vorschlag, aber der Grund, warum ich denke, dass es einen besseren Weg mit UDP geben sollte, ist, dass Informationen zu viele Probleme verursachen, wenn Sie genügend Pakete verpassen, was bedeutet, dass ich nur einen Schutz gegen den Verlust kleiner Pakete haben muss muss nicht für große Paketverluste arbeiten, da sich das Spiel dann in diesem Szenario ohnehin selbst zurücksetzen muss.
Willem

Ich habe auch gelesen, dass TCP in Kombination einen Paketverlust in UDP verursacht. Außerdem müssen die Spielerbefehle den Server so schnell wie möglich erreichen, da sie bestimmen, wie schnell sich das Spiel anfühlt.
Willem

0

Sofern Sie nicht an den Details der Implementierung Ihrer eigenen Zuverlässigkeitsschicht mit UDP interessiert sind, würde ich die Verwendung von RakNet empfehlen , insbesondere da es Open Source ist. Dies ist der richtige Weg, wenn Sie sich darauf konzentrieren, ein Spiel und kein Netzwerkprotokoll zu erstellen.

Mit RakNet können Sie Pakete mit bestimmten Zuverlässigkeitsflags wie RELIABLE_ORDERED senden. Dies stellt sicher, dass jedes Paket ankommt (es wird erneut gesendet, wenn es nicht bestätigt wird) und verwendet ein zugrunde liegendes Bestellsystem, so dass die Pakete auf der Empfangsseite in der richtigen Reihenfolge verarbeitet werden. Dies wäre sehr wichtig, wenn Sie Eingaben für so etwas wie ein Kampfspiel senden, bei dem die Reihenfolge für die Berechnung von Zügen und dergleichen entscheidend ist. Dies alles ist mit einer Bibliothek wie RakNet sehr einfach zu erreichen.

Dies erhöht jedoch die Bandbreite, aber Sie können entsprechend auswählen, welche Pakete zuverlässig und geordnet sein müssen und welche Sie sich leisten können, zu verlieren.

Bearbeiten: Beachten Sie auch die folgenden Artikel, um einen schnellen Multiplayer- Modus zu implementieren. Mit einer leistungsstarken Bibliothek wie RakNet ist das ganz einfach.

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.