# Netzwerkstruktur für ein kleines Spiel



## Ruzmanz (5. Jun 2010)

Guten Mittag,

ich weis nicht, ob das nun zur Spieleprogrammierung zählt oder Netzwerken. Wenn es nicht passt, dann kann man den Thread ja noch verschieben. Ich versuche mein Spiel über Socket netzfähig zu machen und hätte gerne ein paar Meinungen, Anregungen oder Verbesserungsvorschläge.

Zuersteinmal wird es ein 2D-Arcarde Game, das lediglich mit Rectangle und einfacher Kollisionsüberprüfung zurecht kommt. Es soll bis zu 8 Spieler unterstützen. Ich weis nicht wie sich das Verfahren nennt, aber da gibt es sicherlich schon einen Namen:

- Es gibt einen MainServer, der eine Liste<Socket> besitzten und über BufferedReader und BufferedWriter mit diesen kommunizieren kann.
- Der MainServer prüft alle Reader und Writer sequenziell ab (eine/keine Nachricht -> Nächster)
- Die Clients kennen sich untereinander nicht und senden den Änderungsbedarf an den MainServer, welche ohne Überprüfung auf Richtigkeit die Textzeile sofort an alle Clients übermittelt.
- Der MainServer überprüft alle paar Sekunden, ob der Client verbunden ist, indem er eine Nachricht versendet und schaut, ob eine IOExceptiong geworfen wird  (Evtl. gibt es da eine bessere Möglichkeit)
- MainServer und Client kommunizieren mitteles einem Protokol ("\name Hiho", "\ip 127.0.0.1")
- Beim Verbindungsaufbau teilt der MainServer den Clients mit, wer alles mit diesem Verbunden ist Liste<String> ipAdressen.
- Fällt der MainServer aus, so versucht der erste Client in der Liste den Serverdienst zu übernehmen und alle anderen Clients, welche durch die IOException gemerkt haben, dass der MainServer weg ist, verbinden sich mit dem ersten Client in der ipAdressen Liste. So wird der erste Client zum MainServer.

Dadurch wird gewährleistet, dass das Spiel nach einem Serverabsturtz weiterläuft. Ich denke mal, das "größte Problem" an der Sache ist, dass die Nachrichten alle sequential eintuckern und dann verarbeitet werden. Bei den Clients macht es eigentlich nicht viel aus, aber beim Server weis ich nicht, ob er damit zurechkommen wird. Es könnte natürlich aus sein, dass 8 Threads für Input und 8 Threads für Output eine größers Problem ensteht. Immerhin muss nebenbei das Spiel auch noch auf dem selben Rechner laufen.

Evtl. nochmal kurz: Nachrichten sind dann "\playerInit X,Y,PLAYER_WALK,true,false " und die werden dann verarbeitet zu player.x = ..., player.y = ...; oder "\player walk_right,speed", \"player stop,speed"



> hätte gerne ein paar Meinungen, Anregungen oder Verbesserungsvorschläge.


----------



## ice-breaker (5. Jun 2010)

Ruzmanz hat gesagt.:


> - Der MainServer prüft alle Reader und Writer sequenziell ab (eine/keine Nachricht -> Nächster)


eigentlich macht man sowas nicht sequentiell.
Für jeden InputStream gibt es einen Thread, wird erfolgreich eine Nachricht gelesen, packst du diese in eine Warteschlange. Ein ThreadPool holt sich dann aus der Wartschlange die Daten und verarbeitet diese.
Optimal wäre es dann, dies auch noch für den Outputstream zu machen, dass, wenn eine IO-Aktion mal länger dauert, nur Nachrichten an diesen User verzögert werden und nicht der ganze Server.



Ruzmanz hat gesagt.:


> - Die Clients kennen sich untereinander nicht und senden den Änderungsbedarf an den MainServer, welche ohne Überprüfung auf Richtigkeit die Textzeile sofort an alle Clients übermittelt.


wenn ich also falsche Daten sende, kann ich cheaten, weil der Server dies nicht mehr überprüft: Keine gute Ausgangslage



Ruzmanz hat gesagt.:


> - Die Clients kennen sich untereinander nicht [...]
> 
> - Fällt der MainServer aus, so versucht der erste Client in der Liste den Serverdienst zu übernehmen und alle anderen Clients, welche durch die IOException gemerkt haben, dass der MainServer weg ist, verbinden sich mit dem ersten Client in der ipAdressen Liste. So wird der erste Client zum MainServer.


wenn sich die Clients nicht kennen, wie soll dann ein Client zu dem neuen Mainserver verbinden, den ein Client gestartet hat? Und wie verhinderst du, dass 2 Personen gleichzeitig denken, sie sollten nun Mainserver werden 



Ruzmanz hat gesagt.:


> Dadurch wird gewährleistet, dass das Spiel nach einem Serverabsturtz weiterläuft.


eigentlich ist es so, dass wenn der Eröffner des Spiels verschwindet, auch das Spiel zu Ende ist, das macht die Sache nämlich deutlich einfacher.



Ruzmanz hat gesagt.:


> Es könnte natürlich aus sein, dass 8 Threads für Input und 8 Threads für Output eine größers Problem ensteht. Immerhin muss nebenbei das Spiel auch noch auf dem selben Rechner laufen.


ein Computer schafft weit mehr als 8 Threads


----------



## Ruzmanz (5. Jun 2010)

Danke erstmal. Evtl. habe ich das undeutlich gesagt:

Im Prinzip sind das alle Clients und ich kann dann über eine GUI einen Server stellen. Über eine Serverliste im Internet kann ich dann fremde Server finden und drauf gehen. Bei der Verbindung zum MainServer (Host oder wie mand auch bezeichnen will) wird die eigene IP Adresse in einer Liste abgespeichert. Diese Liste<String> hat jeder Client (ausgenommen der MainServer, weil wenn der weg ist braucht er keine Verbindung suchen^^). Die Liste wird so angelegt, dass jeder die gleiche Reihenfolge hat und somit weis der erste in der Liste "Ho, das ist meine IP. Ich mach mal schnell einen Server auf, denn die Verbindung ist weg ". Die anderen denken sich dann "Oh, die Verbindung ist weg und das ist nicht meine IP. Dann schau ich mal, ob der erste einen Server erstellt hat.". Ist kein Server offen, weil alle ihre Ports gesperrt haben oder weil die eigene Verbindung weg ist, dann werden die restlichen Spieler gekickt und man kann alleine weiterspielen. Bzw. das Spiel speichern und irgendwann später fortführen. Ist zwar komplizierter, aber Spielspaß > Arbeitsaufwand. Wenn man zu 10 in einem Raum sitzt und zusammen spielt und der Server muss weg bzw. stürtzt ab .... kennen sicherlich alle "Verbindung zum Server unterbrochen". Man kann weder weitermachen, da nicht gespeichert wurde und muss von neu anfangen.



> wenn ich also falsche Daten sende, kann ich cheaten, weil der Server dies nicht mehr überprüft: Keine gute Ausgangslage



Das ist korrekt. Ich habe keinen Ansatz, wie man ein "Anti-Cheat-Protokoll" schreiben kann  Immerhin kann der Client mir alles erzählen. Ich habe einfach mal auf Geschwindigkeit gesetzt.



> ein Computer schafft weit mehr als 8 Threads



Die Frage ist nur, ob ich 16 Threads + 1 GameClient koordinieren kann ohne Deadlocks oder Synchronisationsfehler  Zumindest schau ich mir mal den ThreadPool an. Aber im Prinzip gabe ich, dass ich es so habe, wie du es beschrieben hast:

Kommunikationsseite (Ohne das Spiel)
Thread Server() -> Server.accept(); (Blockiert durch accept bzw. 5000ms Sleep, falls voll)
Thread InputStream() -> for(int anzahl server) if(ready) -> ReadLine() (20ms Sleep)
Thread OutpurStream() -> for(int anzahl server) write(x,y), newLine(), flush(); (20ms Sleep)
Thread CheckClient() -> sendeAnAlle("Noch da?") -> Exception: remove(index); (2000ms Sleep)

Der Outputstream bekommt lediglich eine Nachricht und schmeißt die zum GameClient, damit der damit etwas rechnen soll. Passiert was im Spiel, dann sagt der Client dem Server sendeAnAlle(\neuerSpieler xy). So verarbeitet jeder seine eigenen Informationen und ergänzt die der anderen.


----------



## ice-breaker (5. Jun 2010)

Ruzmanz hat gesagt.:


> Die Liste wird so angelegt, dass jeder die gleiche Reihenfolge hat und somit weis der erste in der Liste "Ho, das ist meine IP. Ich mach mal schnell einen Server auf, denn die Verbindung ist weg ". Die anderen denken sich dann "Oh, die Verbindung ist weg und das ist nicht meine IP. Dann schau ich mal, ob der erste einen Server erstellt hat.".


und was ist wenn der Client 1 später als die anderen Clients feststellt, dass der Server weg ist und somit die übrigen Clients in einer Zeitspanne versuchen auf den Server zu verbinden, in der der 2. Client noch gar keinen Server aufgemacht hat? 
Beim Timing musst du immer solche Dinge bedenken.
Da die Folien bei uns an der Uni leider passwortgeschützt sind, kann ich sie dir nicht verlinken, aber such mal nach dem "Bully Algorithmus", der ist für dein Problem ideal 
Es geht dabei darum, dass sich die einzelnen Clients absprechen, wer nun der Master wird und dabei hat immer der älteste Client den Vorrang, aber selbst wenn mitten in der Prozedur ein Client verschwindet, ist das keein Problem, der Algorithmus ist absolut ausfallsicher.



Ruzmanz hat gesagt.:


> Das ist korrekt. Ich habe keinen Ansatz, wie man ein "Anti-Cheat-Protokoll" schreiben kann  Immerhin kann der Client mir alles erzählen. Ich habe einfach mal auf Geschwindigkeit gesetzt.


der Server muss prüfen, was ihm der Client sendet.
Wenn mir der Client sagt, er hat in einer Truhe (Rollenspiel) 50 Gold gefunden, der Server aber weiß, dass die Kiste leer ist, hast du einen Cheater enttarnt.
Wenn dir ein Client sagt, dass er sich auf den Koordinaten 50,51 befindet, vor 200msec aber von dort noch Meilen entfernt war, dann wird wohl auch lügen.
Du musst dein Spiel so bauen, dass die Clients eigentlich dumm sind, also ihre Daten vom Server bekommen: Nicht der Client sagt ich habe 50G in der Truhe gefunden sondern der Client sagt:
"Ich öffne die Truhe auf 33,44". Der Server prüft dann ob der Client wirklich da steht, ob sich dort eine Truhe befindet und sendet dem Client dann, was sich in der Truhe befunden hat und nun dem Client gehört.



Ruzmanz hat gesagt.:


> Die Frage ist nur, ob ich 16 Threads + 1 GameClient koordinieren kann ohne Deadlocks oder Synchronisationsfehler


generell ist es möglich 
Aber da es auch einige Bücher zu dem Thema gibt, zeigt dies, dass es auch nicht einfach ist.
2 einfache Tipps:

Wenn du einen Lock hast, und andere Methoden aufrufst, ist das Risiko hoch, dass sich irgendwelche Anforderungen überschneiden
Wenn du mehrere Locks anfordern musst, tu dies immer in der gleichen Reihenfolge
Generell kann ich das Buch "Java Concurrency in Practice" empfehlen, die ersten beiden Kapitel sind wirklich schwer zu lesen, aber danach kommen soviele gute Tips, das ist einfach nur gigantisch.



Ruzmanz hat gesagt.:


> Kommunikationsseite (Ohne das Spiel)
> Thread Server() -> Server.accept(); (Blockiert durch accept bzw. 5000ms Sleep, falls voll)
> Thread InputStream() -> for(int anzahl server) if(ready) -> ReadLine() (20ms Sleep)
> Thread OutpurStream() -> for(int anzahl server) write(x,y), newLine(), flush(); (20ms Sleep)
> Thread CheckClient() -> sendeAnAlle("Noch da?") -> Exception: remove(index); (2000ms Sleep)


ehrlich gesagt kann ich kein Stück nachvollziehen, was das bedeuten soll, das klingt aber sehr stark nach deinem alten sequentiellen Modell, was definitiv in die Hose gehen wird. Denn die read-Aufrufe blockieren bis Daten da sind. Sleeps sollte eigentlich auch nie erforderlich sein, dann hast du einen Fehler gemacht.


----------



## Landei (5. Jun 2010)

Darf ich mal ganz doof fragen, warum du das selbst klöppelst?

jgn - Project Hosting on Google Code
RedDwarf Server


----------



## Ruzmanz (5. Jun 2010)

> ehrlich gesagt kann ich kein Stück nachvollziehen, was das bedeuten soll, das klingt aber sehr stark nach deinem alten sequentiellen Modell, was definitiv in die Hose gehen wird. Denn die read-Aufrufe blockieren bis Daten da sind. Sleeps sollte eigentlich auch nie erforderlich sein, dann hast du einen Fehler gemacht.



Das war meine alte Strucktur. Dachte, das wäre so was wie ThreadPool .. habe ich wohl falsch verstanden -> Ich glaube ich schau mir noch heute ein paar Vorlesungen über Threads allgemein an. Also mein Ansatz funktioniert, da ich den getestet habe. Ich möchte nur erstmal eigne Ideen entwickeln und dann auf evtl. vorhandene Methoden zurückgreifen. So würde das aussehen: (Das in.ready() blockiert nur, wenn es in einer Schleife gebraucht wird, hier ist es lediglich eine if-Abfrage . Kannst es dir mal angucken, wenn du willst, aber im Prinzip habe ich verstanden, dass ich es ändern soll)


```
/**
     * Verwaltung des BufferedReader des MainServers. Lauscht alle Streams
     * nacheinander ab und sendet die ankommende Information sofort an alle
     * Clients weiter.
     */
    private void runMainServer() {
        while(!com.isNewHost()) {
            // BufferedReader eines Clients
            BufferedReader in = com.getNextBufferedReader();
            if (in == null) { 
                try {
                    sleep(80);
                } catch (InterruptedException ex) {
                    // Uncaught Exception
                }
            } else {
                try {
                    if (in.ready()) {
                        String s = in.readLine();
                        System.out.println(s);
                        // Input an alle Clients verschicken.
                        com.broadcast(s);
                    }
                } catch (IOException ex) {
                    com.removeClient(false);
                }
            }
            try {
                sleep(20);
            } catch (InterruptedException ex) {
                // Uncaught Exception
            }
        }
    }
```



> Sleeps sollte eigentlich auch nie erforderlich sein, dann hast du einen Fehler gemacht



Wie meinst du das eigentlich mit dem Sleep? Ich habe immer angenommen, dass ein Thread ein bisschen pausieren muss, damit die anderen Prozesse nicht "blockiert" werden. Zumindest dachte ich mir, dass 20ms angebracht sind bei 8 Clients.



> Darf ich mal ganz doof fragen, warum du das selbst klöppelst?
> 
> jgn - Project Hosting on Google Code
> RedDwarf Server



Danke, schau ich mir dann mal an. Wollte grade schreiben, dass ich meine eigenen Ideen umzusetzten versuche -> Erfahrung gewinnen / Horizont erweitern usw. :rtfm: Desshalb programmiere ich auch ein Spiel, da können alle Themen rein, die man grade machen möchte. Basics, Threads, Sockets, GUI, usw.


----------



## ice-breaker (5. Jun 2010)

Landei hat gesagt.:


> jgn - Project Hosting on Google Code
> RedDwarf Server


Ehrlich gesagt missfällt mir der RedDwarf-Ansatz (Projekt Darkstar) und die Informationen zu Jgn sind wirklich mehr als mikrig.
Da er es nur zum lernen macht, halte ich es für ok, ein Framework für soetwas zu nutzen, ohne überhaupt einmal selbst mit Sockets und deren Problemen gearbeitet zu haben, bringt doch nicht viel.
Und das ist ja nun nicht etwas bei dem man Ewigkeiten zum Entwickeln braucht und es dann nichts taugt im Gegensatz zu fertiger Software (ORM-Wrapper, Web-Framework usw)




Ruzmanz hat gesagt.:


> Das in.ready() blockiert nur, wenn es in einer Schleife gebraucht wird, hier ist es lediglich eine if-Abfrage


alleine schon, dass es blockieren kann, ist für dich mehr als ungünstig, denn blockiert es einmal, wird auch von anderen Clients nicht mehr gelesen.



Ruzmanz hat gesagt.:


> aber im Prinzip habe ich verstanden, dass ich es ändern soll


wäre sehr angebracht.



Ruzmanz hat gesagt.:


> Wie meinst du das eigentlich mit dem Sleep? Ich habe immer angenommen, dass ein Thread ein bisschen pausieren muss, damit die anderen Prozesse nicht "blockiert" werden. Zumindest dachte ich mir, dass 20ms angebracht sind bei 8 Clients.


dein Betriebssystem teilt die Rechenzeit den Threads zu, also irgendwann wird einfach immer ein Thread abgewürgt und der nächste darf laufen, deine Sleeps bringen da gar nichts.
Das Problem mit so einer Architektur wie du momentan hast, wird nur sein, dass es Unmengen Rechenleistung fressen kann, weil es die Schleife immer durchläuft ohne etwas zu tun. Durch den eventbasierten Ansatz, den ich dir oben vorgeschlagen habe, treten solche Probleme nicht auf.


----------



## Ruzmanz (5. Jun 2010)

> Für jeden InputStream gibt es einen Thread, wird erfolgreich eine Nachricht gelesen, packst du diese in eine Warteschlange. Ein ThreadPool holt sich dann aus der Wartschlange die Daten und verarbeitet diese.
> Optimal wäre es dann, dies auch noch für den Outputstream zu machen, dass, wenn eine IO-Aktion mal länger dauert, nur Nachrichten an diesen User verzögert werden und nicht der ganze Server


.

Wie genau mache ich das mit dem OutputStream? Wenn ich z.B. 8 Threads für den Output habe und eine Warteschlange:

ArrayList<String> warteschlange;

Woher weis ich nun, ob jeder das erste Element versickt hat und ob ich das erste löschen kann? Muss ich nun den ThreadPool verwenden, um die Nachrichten zu verarbeiten und dann an alle Threads zuweisen (das ist im Prinzip auch sequenziell). Hm ...


----------



## ice-breaker (5. Jun 2010)

ja also ich hatte da eben mal folgende Architektur (so in der Art, habe es auf dich angepasst):


```
class QueueOutputString implements Runnable {
  private final Player p;
  private final String output;

  // ...

  public void run() {
    OutputStream out = p.getOutputStream();
    synchronized(out) {
      out.write(output.getBytes());
    }
  }
}
```

es sah noch anders aus, aber ich kann ja nicht die ganze Architektur hier posten.
Ich habe es vor allem so gebaut, weil meine Architektur es so vorsah, dass Messages in einer Queue von der Business-Logik abgearbeitet werden und dann als Antwort-Messages in einer weiteren Queue für die Clients landen, dadurch hatte ich eben erreicht, dass das Framework für die Business-Logik sich nicht um das IO-Zeug und Synchronisierung auf den Sockets kümmern muss, es muss nur die Logik implementieren.

was dann eben in etwa so aussah (simpler Echo-Server):

```
class ExecutionHandler implements Handler {
  // ...
  public void messageHandle(Player p, String message, ServerContext s) {
    String echo = p.getName + ": " + message;
    s.getOutputQueue().execute(new QueueOutputString(p, echo));
  }
}
```

So müsste eben für jeden Spieler die Nachricht in die Wartschlange gelegt werden, 3-4 Threads holen sich diese dann aus der Wartschlange und führen die Aktion aus.
Wenn eine Exception auftritt (in der Klasse QueueOutputString müsste dies natürlich dem ServerContext mitgeteilt werden, der dann eben den Spieler vom Server trennt).


----------

