Gibt es ein Muster zum Schreiben eines rundenbasierten Servers, der über Sockets mit n Clients kommuniziert?

14

Ich arbeite an einem allgemeinen Spieleserver, der Spiele für eine beliebige Anzahl von TCP-Socket-vernetzten Clients verwaltet, die ein Spiel spielen. Ich habe ein Design zusammen mit Klebeband gehackt, das funktioniert, aber gleichzeitig fragil und unflexibel zu sein scheint. Gibt es ein etabliertes Muster für das Schreiben von Client / Server-Kommunikation, das robust und flexibel ist? (Wenn nicht, wie würden Sie verbessern, was ich unten habe?)

Grob habe ich das:

  • Während der Einrichtung eines Spiels verfügt der Server über einen Thread für jeden Player-Socket, der synchrone Anforderungen von einem Client und Antworten vom Server verarbeitet.
  • Sobald das Spiel läuft, werden jedoch alle Threads mit Ausnahme von einem Schlaf-Thread durch alle Spieler nacheinander durchlaufen, um über ihren Zug zu kommunizieren (in umgekehrter Anfrage-Antwort-Reihenfolge).

Hier ist ein Diagramm von dem, was ich derzeit habe; Klicken für größere / lesbare Version oder 66kB PDF .

Ablaufdiagramm des Durchflusses

Probleme:

  • Die Spieler müssen genau nacheinander mit genau der richtigen Nachricht antworten. (Ich nehme an, ich könnte jeden Spieler mit zufälligem Mist antworten lassen und erst weitermachen, wenn er mir einen gültigen Zug gibt.)
  • Es erlaubt Spielern nicht, mit dem Server zu sprechen, es sei denn, sie sind an der Reihe. (Ich könnte sie vom Server über andere Spieler informieren lassen, aber keine asynchrone Anfrage bearbeiten.)

Endgültige Anforderungen:

  • Leistung ist nicht von größter Bedeutung. Dies wird hauptsächlich für Nicht-Echtzeit-Spiele verwendet, und hauptsächlich, um AIs gegeneinander auszuspielen, nicht für nervöse Menschen.

  • Das Spiel wird immer rundenbasiert sein (auch wenn die Auflösung sehr hoch ist). Jeder Spieler erhält immer einen Zug, bevor alle anderen Spieler an die Reihe kommen.

Die Implementierung des Servers geschieht in Ruby, wenn das einen Unterschied macht.

Phrogz
quelle
Wenn Sie einen TCP-Socket pro Client verwenden, würde diese Obergrenze dann nicht für Clients (65535-1024) gelten?
o0 '.
1
Warum ist das ein Problem, Lohoris? Die meisten Leute werden Schwierigkeiten haben, 6000 gleichzeitige Benutzer zu bekommen,
egal 60000.
@ Kylotan In der Tat. Wenn ich mehr als 10 gleichzeitige AIs habe, werde ich überrascht sein. :)
Phrogz
Mit welchem ​​Tool haben Sie aus Neugier das Diagramm erstellt?
Matthias
@matthias Leider war das zu viel benutzerdefinierte Arbeit in Adobe Illustrator.
Phrogz

Antworten:

10

Ich bin nicht sicher, was genau Sie erreichen wollen. Es gibt jedoch ein Muster, das ständig auf Spieleservern verwendet wird und möglicherweise hilfreich ist. Verwenden Sie Nachrichtenwarteschlangen.

Genauer gesagt: Wenn Clients Nachrichten an den Server senden, verarbeiten Sie diese nicht sofort. Analysieren Sie sie stattdessen und stellen Sie sie für diesen bestimmten Client in eine Warteschlange. Gehen Sie dann in einer Hauptschleife (möglicherweise sogar in einem anderen Thread) nacheinander alle Clients durch, rufen Sie Nachrichten aus der Warteschlange ab und verarbeiten Sie sie. Wenn die Verarbeitung anzeigt, dass der Zug dieses Kunden beendet ist, fahren Sie mit dem nächsten fort.

Auf diese Weise müssen Kunden nicht ausschließlich Turn-by-Turn arbeiten. Nur so schnell, dass Sie etwas in der Warteschlange haben, wenn der Client verarbeitet wird (Sie können natürlich entweder auf den Client warten oder ihn überspringen, wenn er verzögert ist). Und Sie können Unterstützung für asynchrone Anforderungen hinzufügen, indem Sie eine "asynchrone" Warteschlange hinzufügen: Wenn ein Client eine spezielle Anforderung sendet, wird diese dieser speziellen Warteschlange hinzugefügt. Diese Warteschlange wird häufiger überprüft und verarbeitet als die Warteschlangen der Kunden.

Keine Ursache
quelle
1

Hardware-Threads lassen sich nicht gut genug skalieren, um "einen pro Spieler" für eine dreistellige Anzahl von Spielern als vernünftige Idee zu definieren, und die Logik, zu wissen, wann sie aktiviert werden müssen, ist eine Komplexität, die zunehmen wird. Besser ist es, ein asynchrones E / A-Paket für Ruby zu finden, mit dem Sie Daten senden und empfangen können, ohne einen gesamten Programmthread anhalten zu müssen, während der Lese- oder Schreibvorgang stattfindet. Dies löst auch das Problem des Wartens auf die Antwort von Spielern, da bei einem Lesevorgang keine Threads hängen bleiben. Stattdessen kann Ihr Server einfach überprüfen, ob ein Zeitlimit abgelaufen ist, und den anderen Spieler entsprechend benachrichtigen.

Grundsätzlich ist "asynchrone E / A" das gesuchte "Muster", obwohl es eigentlich kein Muster ist, sondern eher ein Ansatz. Anstatt explizit "read" für einen Socket aufzurufen und das Programm anzuhalten, bis die Daten eingegangen sind, richten Sie das System so ein, dass der "onRead" -Handler aufgerufen wird, sobald Daten verfügbar sind, und Sie setzen die Verarbeitung bis zu diesem Zeitpunkt fort.

Jede Steckdose hat eine Drehung

Jeder Spieler hat einen Zug und jeder Spieler hat eine Buchse, die Daten sendet, was ein bisschen anders ist. Eines Tages möchten Sie möglicherweise nicht eine Steckdose pro Spieler. Möglicherweise verwenden Sie überhaupt keine Sockets. Halten Sie die Verantwortungsbereiche getrennt. Tut mir leid, wenn dies nach einem unwichtigen Detail klingt, aber wenn Sie Konzepte in Ihrem Design zusammenführen, die anders sein sollten, wird es schwieriger, bessere Ansätze zu finden und zu diskutieren.

Kylotan
quelle
1

Es gibt sicherlich mehr als eine Möglichkeit, dies zu tun, aber persönlich würde ich die einzelnen Threads vollständig überspringen und einfach eine Ereignisschleife verwenden. Die Art und Weise, wie Sie dies tun, hängt etwas von der verwendeten E / A-Bibliothek ab, aber im Grunde sieht Ihre Hauptserverschleife folgendermaßen aus:

  1. Richten Sie einen Verbindungspool und einen Listening-Socket für neue Verbindungen ein.
  2. Warten Sie, bis etwas passiert.
  3. Wenn es sich bei dem Objekt um eine neue Verbindung handelt, fügen Sie sie dem Pool hinzu.
  4. Wenn es sich bei dem Objekt um eine Anfrage eines Kunden handelt, prüfen Sie, ob es sich um ein Objekt handelt, das Sie sofort bearbeiten können. Wenn ja, machen Sie das; Wenn nicht, stellen Sie es in eine Warteschlange und (optional) senden Sie eine Bestätigung an den Client.
  5. Überprüfen Sie auch, ob sich in der Warteschlange etwas befindet, das Sie jetzt bearbeiten können. wenn ja, mach es.
  6. Kehren Sie zu Schritt 2 zurück.

Nehmen wir zum Beispiel an, Sie haben n Kunden, die an einem Spiel beteiligt sind. Wenn dann zuerst n-1 von ihnen ihre Züge einsenden, überprüfen Sie einfach, ob der Zug gültig ist, und senden eine Nachricht zurück, die besagt, dass der Zug empfangen wurde, aber Sie warten immer noch darauf, dass andere Spieler ziehen. Letztendlich n Spieler umgezogen sind, werden alle von Ihnen gespeicherten Züge verarbeitet und die Ergebnisse an alle Spieler gesendet.

Sie können dieses Schema auch so verfeinern, dass es Zeitüberschreitungen enthält. Die meisten E / A-Bibliotheken sollten über einen Mechanismus verfügen, mit dem sie warten können, bis neue Daten eingehen oder eine bestimmte Zeit verstrichen ist.

Natürlich können Sie so etwas auch mit individuellen Threads für jede Verbindung implementieren, indem Sie diese Threads alle Anforderungen weiterleiten lassen, die sie nicht direkt an einen zentralen Thread (entweder einen pro Spiel oder einen pro Server) weiterleiten, der eine Schleife ausführt oben gezeigt, außer dass es mit den Verbindungshandler-Threads und nicht direkt mit den Clients kommuniziert. Ob Sie dies einfacher oder komplizierter finden als den Single-Thread-Ansatz, liegt ganz bei Ihnen.

Ilmari Karonen
quelle