Was ist besser? Viele kleine TCP-Pakete oder ein langes? [geschlossen]

16

Für ein Spiel, das ich mache, sende ich eine ganze Menge Daten von und zu einem Server.

Ich sende momentan Standortdaten wie diese:

sendToClient((("UID:" + cl.uid +";x:" + cl.x)));
sendToClient((("UID:" + cl.uid +";y:" + cl.y)));
sendToClient((("UID:" + cl.uid +";z:" + cl.z)));

Offensichtlich werden die entsprechenden X-, Y- und Z-Werte gesendet.

Wäre es effizienter, solche Daten zu senden?

sendToClient((("UID:" + cl.uid +"|" + cl.x + "|" + cl.y + "|" + cl.z)));
joehot200
quelle
2
Nach meiner begrenzten Erfahrung liegt der Paketverlust normalerweise unter 5%.
Mucaho
5
Sendet sendToClient tatsächlich ein Paket? Wenn ja, wie haben Sie das gemacht?
user253751
1
@mucaho Ich habe es noch nie selbst gemessen, aber ich bin überrascht, dass TCP so rau an den Rändern ist. Ich hätte mir mehr als 0,5% oder weniger erhofft.
Panzercrisis
1
@Panzercrisis Ich muss dir zustimmen. Ich persönlich halte einen Verlust von 5% für inakzeptabel. Wenn Sie daran denken, dass ich beispielsweise ein neues Schiff verschicke, das im Spiel aufgetaucht ist, wäre sogar eine Wahrscheinlichkeit von 1%, dass dieses Paket nicht empfangen wird, katastrophal, da ich unsichtbare Schiffe bekommen würde.
joehot200
2
Jungs nicht ausflippen, ich meinte die 5% als Obergrenze :) in Wirklichkeit ist es viel besser, wie andere Kommentare bemerken.
Mucaho

Antworten:

28

Ein TCP-Segment hat ziemlich viel Overhead. Wenn Sie eine 10-Byte-Nachricht mit einem TCP-Paket senden, senden Sie tatsächlich Folgendes:

  • 16 Byte IPv4-Header (erhöht sich auf 40 Byte, wenn IPv6 allgemein verwendet wird)
  • 16 Byte TCP-Header
  • 10 Bytes Nutzlast
  • Zusätzlicher Overhead für die verwendeten Datenverbindungs- und Physical-Layer-Protokolle

Dies führt zu 42 Byte Datenverkehr für den Transport von 10 Byte Daten. Sie nutzen also nur weniger als 25% Ihrer verfügbaren Bandbreite. Und das erklärt noch nicht den Overhead, den die Protokolle auf niedrigerer Ebene wie Ethernet oder PPPoE verbrauchen (aber diese sind schwer abzuschätzen, weil es so viele Alternativen gibt).

Außerdem stellen viele kleine Pakete eine größere Belastung für Router, Firewalls, Switches und andere Netzwerkinfrastrukturgeräte dar. Wenn Sie, Ihr Dienstanbieter und Ihre Benutzer nicht in hochwertige Hardware investieren, kann dies zu einem weiteren Engpass führen.

Aus diesem Grund sollten Sie versuchen, alle verfügbaren Daten auf einmal in einem TCP-Segment zu senden.

Hinweise zum Umgang mit Paketverlust : Wenn Sie TCP verwenden, müssen Sie sich darüber keine Gedanken machen. Das Protokoll selbst stellt sicher, dass alle verlorenen Pakete erneut gesendet und in der richtigen Reihenfolge verarbeitet werden. Sie können also davon ausgehen, dass alle von Ihnen gesendeten Pakete auf der anderen Seite eintreffen und in der Reihenfolge, in der Sie sie senden. Der Preis dafür ist, dass Ihr Player bei Paketverlust eine erhebliche Verzögerung erleidet, da ein verworfenes Paket den gesamten Datenstrom anhält, bis es erneut angefordert und empfangen wurde.

Wenn dies ein Problem ist, können Sie immer UDP verwenden. Aber dann müssen Sie Ihre eigene Lösung für verlorene und nicht mehr funktionierende Nachrichten finden (dies garantiert zumindest, dass die Nachrichten, die ankommen, vollständig und unbeschädigt ankommen).

Philipp
quelle
1
Ist der Aufwand für die Wiederherstellung des Paketverlusts durch TCP je nach Paketgröße unterschiedlich hoch?
Panzercrisis
@Panzercrisis Nur insofern, als es ein größeres Paket gibt, das erneut gesendet werden muss.
Philipp
8
Ich würde bemerken, dass das Betriebssystem mit ziemlicher Sicherheit den Nagles-Algorithmus en.wikipedia.org/wiki/Nagle%27s_algorithm auf ausgehende Daten anwenden wird. Dies bedeutet, dass es egal ist, ob Sie in der App separate Schreibvorgänge ausführen oder sie kombinieren bevor Sie sie tatsächlich über TCP verteilen.
Vality
1
@Vality Die meisten Socket-APIs, die ich verwendet habe, ermöglichen das Aktivieren oder Deaktivieren von nagle für jeden Socket. Für die meisten Spiele würde ich empfehlen, es zu deaktivieren, da eine niedrige Latenz in der Regel wichtiger ist als die Einsparung von Bandbreite.
Philipp
4
Nagles Algorithmus ist einer, aber nicht der einzige Grund, warum Daten auf der Sendeseite gepuffert werden können. Es gibt keine Möglichkeit , Datensendungen zuverlässig zu erzwingen. Pufferung / Fragmentierung kann auch nach dem Senden von Daten überall auftreten, entweder auf NATs, Routern, Proxys oder sogar auf der Empfängerseite. TCP übernimmt keine Garantie für die Größe und den Zeitpunkt, zu dem Sie Daten erhalten, sondern nur dafür, dass diese ordnungsgemäß und zuverlässig ankommen. Wenn Sie Größengarantien benötigen, verwenden Sie UDP. Die Tatsache , dass TCP scheint einfacher ist es , das beste Werkzeug für alle Probleme macht nicht zu verstehen!
Panda Pyjama
10

Ein großer ist besser.

Wie Sie sagten, ist Paketverlust der Hauptgrund. Pakete werden in der Regel in Frames fester Größe gesendet. Es ist daher besser, einen Frame mit einer großen Nachricht als 10 Frames mit 10 kleinen Frames aufzunehmen.

Bei Standard-TCP ist dies jedoch kein wirkliches Problem, es sei denn, Sie deaktivieren es. (Es heißt Nagles Algorithmus , und für Spiele sollten Sie ihn deaktivieren.) TCP wartet auf eine feste Zeitüberschreitung oder bis das Paket "voll" ist. Wo "voll" ist, wäre eine leicht magische Zahl, die teilweise durch die Rahmengröße bestimmt wird.

Elva
quelle
Ich habe von Nagles Algorithmus gehört, aber ist es wirklich eine gute Idee, ihn zu deaktivieren? Ich kam gerade direkt von einer StackOverflow-Antwort, in der jemand sagte, dass es effizienter ist (und aus offensichtlichen Gründen möchte ich Effizienz).
Joehot200
6
@ joehot200 Die einzig richtige Antwort darauf ist "es kommt darauf an". Es ist effizienter für das Senden vieler Daten, aber nicht für das Echtzeit-Streaming, das Spiele normalerweise benötigen.
D-Seite
4
@ joehot200: Nagles Algorithmus interagiert schlecht mit einem Algorithmus für verzögerte Bestätigung, den bestimmte TCP-Implementierungen manchmal verwenden. Einige TCP-Implementierungen verzögern das Senden einer ACK, nachdem sie einige Daten erhalten haben, wenn sie erwarten, dass bald darauf weitere Daten folgen (da das Bestätigen des späteren Pakets implizit auch das frühere Paket bestätigen würde). Nagles Algorithmus besagt, dass eine Einheit kein Teilpaket senden sollte, wenn sie einige Daten gesendet, aber keine Bestätigung gehört hat. Manchmal interagieren die beiden Ansätze schlecht, und jede Partei wartet darauf, dass die andere etwas
unternimmt
2
... ein "Sanity Timer" startet und löst die Situation (in der Größenordnung von einer Sekunde).
Superkatze
2
Leider verhindert das Deaktivieren des Nagle-Algorithmus nicht das Puffern auf der anderen Host-Seite. Durch Deaktivieren des Nagle-Algorithmus wird nicht sichergestellt, dass Sie recv()für jeden send()Anruf einen Anruf erhalten. Dies ist das, wonach die meisten Menschen suchen. Verwenden Sie ein Protokoll, das dies garantiert, wie es UDP tut. "Wenn alles, was Sie haben, TCP ist, sieht alles aus wie ein Stream"
Panda Pyjama
6

Alle vorherigen Antworten sind falsch. In der Praxis spielt es keine Rolle, ob Sie einen langen send()Anruf oder mehrere kleine send()Anrufe tätigen.

Wie Phillip feststellt, hat ein TCP-Segment einen gewissen Overhead, aber als Anwendungsprogrammierer haben Sie keine Kontrolle darüber, wie Segmente generiert werden. In einfachen Worten:

Ein send()Aufruf wird nicht unbedingt in ein TCP-Segment übersetzt.

Es steht dem Betriebssystem völlig frei , alle Ihre Daten zu puffern und in einem Segment zu senden, oder das lange zu nehmen und es in mehrere kleine Segmente aufzuteilen.

Dies hat mehrere Implikationen, aber die wichtigste ist, dass:

Ein send()Aufruf oder ein TCP-Segment führt nicht unbedingt zu einem erfolgreichen recv()Aufruf am anderen Ende

Der Grund dafür ist, dass TCP ein Stream- Protokoll ist. TCP behandelt Ihre Daten als einen langen Strom von Bytes und hat absolut kein Konzept von "Paketen". Wenn send()Sie diesem Stream Bytes hinzufügen und recv()Bytes von der anderen Seite entfernen. TCP puffert und teilt Ihre Daten aggressiv, wo immer es angebracht erscheint, um sicherzustellen, dass Ihre Daten so schnell wie möglich auf die andere Seite gelangen.

Wenn Sie "Pakete" mit TCP senden und empfangen möchten, müssen Sie Paketstartmarkierungen, Längenmarkierungen usw. implementieren. Wie wäre es stattdessen mit einem nachrichtenorientierten Protokoll wie UDP? UDP garantiert, dass ein send()Anruf in ein gesendetes Datagramm und in einen recv()Anruf übersetzt wird!

Wenn Sie nur TCP haben, sieht alles wie ein Stream aus

Panda-Pyjama
quelle
1
Das Präfixieren jeder Nachricht mit einer Nachrichtenlänge ist nicht so schwierig.
ysdx
Sie müssen einen Schalter umlegen, wenn es um die Paketaggregation geht, unabhängig davon, ob Nagles Algorithmus in Kraft ist oder nicht. Es ist nicht ungewöhnlich, dass das Netzwerk im Spiel ausgeschaltet ist, um eine schnelle Zustellung von unterfüllten Paketen zu gewährleisten.
Lars Viklund
Dies ist vollständig OS- oder sogar bibliotheksspezifisch. Auch Sie haben viel Kontrolle - wenn Sie wollen. Es ist wahr, dass Sie nicht die totale Kontrolle haben. TCP kann immer zwei Nachrichten kombinieren oder eine teilen, wenn sie nicht zur MTU passt. Sie können sie aber trotzdem in die richtige Richtung deuten. Festlegen verschiedener Konfigurationseinstellungen, manuelles Senden von Nachrichten im Abstand von 1 Sekunde oder Puffern von Daten und Senden in einem Durchgang.
Dorus
@ysdx: nein, nicht auf der senderseite, aber ja auf der empfängerseite. Da Sie keine Garantie dafür haben, wo genau Sie Daten erhalten recv(), müssen Sie eine eigene Pufferung vornehmen, um dies auszugleichen. Ich würde es in die gleiche Schwierigkeit wie die Implementierung von Zuverlässigkeit über UDP einordnen.
Panda Pyjama
@ Pagnda Pyjama: Eine naive Implementierung der empfangenden Seite ist: while(1) { uint16_t size; read(sock, &size, sizeof(size)); size = ntoh(size); char message[size]; read(sock, buffer, size); handleMessage(message); }(Aus Gründen der Kürze wird auf Fehlerbehandlung und partielle Lesevorgänge verzichtet, es ändert sich jedoch nicht viel). Dadurch ist eine selectnicht viel mehr Komplexität hinzuzufügt und wenn Sie TCP verwenden , müssen Sie wahrscheinlich sowieso die Teilnachricht puffern. Die Implementierung einer robusten Zuverlässigkeit über UDP ist viel komplizierter.
ysdx
3

Viele kleine Pakete sind in Ordnung. Wenn Sie sich Sorgen über den TCP-Overhead machen, fügen Sie einfach einen ein bufferstream, der bis zu 1500 Zeichen enthält (oder was auch immer Ihre TCP-MTUs sind, am besten dynamisch anfordern), und lösen Sie das Problem an einem Ort. Auf diese Weise sparen Sie den Overhead von ~ 40 Bytes für jedes zusätzliche Paket, das Sie sonst erstellt hätten.

Trotzdem ist es immer noch besser, weniger Daten zu senden und größere Objekte zu erstellen. Natürlich ist das Senden kleiner "UID:10|1|2|3als das Senden UID:10;x:1UID:10;y:2UID:10;z:3. Auch an diesem Punkt sollten Sie das Rad nicht neu erfinden. Verwenden Sie eine Bibliothek wie protobuf , die solche Daten auf eine 10-Byte-Zeichenfolge oder weniger reduzieren kann.

Das Einzige, was Sie nicht vergessen sollten, ist das Einfügen von FlushBefehlen in Ihren Stream an den relevanten Stellen, denn sobald Sie aufhören, Daten zu Ihrem Stream hinzuzufügen, wartet er möglicherweise unendlich, bevor er etwas sendet. Wirklich problematisch, wenn Ihr Client auf diese Daten wartet und Ihr Server nichts Neues sendet, bis der Client den nächsten Befehl sendet.

Paketverlust ist etwas, das Sie hier geringfügig beeinflussen können. Jedes von Ihnen gesendete Byte kann möglicherweise beschädigt werden, und TCP fordert automatisch eine erneute Übertragung an. Kleinere Pakete bedeuten eine geringere Wahrscheinlichkeit, dass jedes einzelne Paket beschädigt wird. Da sich die Gesamtkosten jedoch summieren, senden Sie noch mehr Bytes, wodurch die Wahrscheinlichkeit eines verlorenen Pakets noch größer wird. Wenn ein Paket verloren geht, puffert TCP alle nachfolgenden Daten, bis das fehlende Paket erneut gesendet und empfangen wird. Dies führt zu einer großen Verzögerung (Ping). Während der Gesamtverlust an Bandbreite aufgrund von Paketverlust vernachlässigbar sein kann, wäre der höhere Ping für Spiele unerwünscht.

Fazit: Senden Sie so wenig Daten wie möglich, senden Sie große Pakete und schreiben Sie keine eigenen Methoden auf niedriger Ebene, sondern verlassen Sie sich auf bekannte Bibliotheken und Methoden wie bufferstreamund protobuf, um das schwere Heben zu bewältigen.

Dorus
quelle
Eigentlich ist es für solche einfachen Dinge einfach, deine eigenen zu rollen. Es ist viel einfacher, als eine 50-seitige Dokumentation durchzugehen, um die Bibliothek einer anderen Person zu verwenden, und danach müssen Sie sich immer noch mit deren Fehlern und Fallstricken befassen.
Pacerier
Es stimmt, es bufferstreamist trivial, seine eigenen zu schreiben. Deshalb habe ich es eine Methode genannt. Sie möchten immer noch alles an einem Ort erledigen und Ihre Pufferlogik nicht in Ihren Nachrichtencode integrieren. Was die Objektserialisierung angeht, bezweifle ich sehr, dass Sie etwas Besseres erhalten als die Tausenden von Arbeitsstunden, die andere dort einbringen, auch wenn Sie es dennoch versuchen. Ich rate Ihnen dringend, Ihre Lösung mit bekannten Implementierungen zu vergleichen.
Dorus
2

Obwohl ich selbst ein Neuling in der Netzwerkprogrammierung bin, möchte ich meine begrenzten Erfahrungen mit ein paar Punkten teilen:

  • TCP bedeutet einen Mehraufwand - Sie müssen die relevanten Statistiken messen
  • UDP ist die De-facto- Lösung für Netzwerk-Gaming-Szenarien, aber alle Implementierungen, die darauf angewiesen sind, verfügen über einen zusätzlichen CPU-seitigen Algorithmus, um zu berücksichtigen, dass Pakete verloren gehen oder nicht ordnungsgemäß gesendet werden

In Bezug auf Messungen sollten folgende Metriken berücksichtigt werden:

Wie bereits erwähnt, sollten Sie dies tun, wenn Sie feststellen, dass Sie in gewisser Hinsicht nicht eingeschränkt sind und UDP verwenden können. Es gibt einige UDP-basierte Implementierungen, sodass Sie weder das Rad neu erfinden noch gegen jahrelange Erfahrung und nachgewiesene Erfahrung ankämpfen müssen. Solche erwähnenswerten Implementierungen sind:

Fazit: Da eine UDP-Implementierung eine TCP-Implementierung (um den Faktor 3) übertreffen könnte, ist es sinnvoll, sie in Betracht zu ziehen, sobald Sie Ihr Szenario als UDP-freundlich identifiziert haben. Sei gewarnt! Es ist immer eine schlechte Idee, den vollständigen TCP-Stack über UDP zu implementieren.

Teodron
quelle
Ich habe UDP verwendet. Ich bin gerade erst auf TCP umgestiegen. Der Paketverlust von UDP war für wichtige Daten, die der Client benötigte, einfach inakzeptabel. Ich kann Bewegungsdaten über UDP senden.
joehot200
Das Beste, was Sie also tun können, ist: Verwenden Sie TCP nur für wichtige Vorgänge ODER verwenden Sie eine UDP-basierte Softwareprotokollimplementierung (wobei Enet einfach und UDT gut getestet ist). Aber zuerst messen Sie den Verlust und entscheiden, ob UDT Ihnen einen Vorteil bringt.
Teodron