Achtung:

Dieses Tutorial baut auf einigen Grundprinzipien der Kommunikation zwischen Computern und anderem Grundwissen auf. Verwendet werden:

  • Server-Client-Modell
  • Schicht 3 & 4 Kommunikation: IP & Port
  • Grundlegendes Threading in Java (nur für dedizierte Server)


Ziel des Tutorials

Nach diesem Tutorial kannst du die Netzwerkfunktionen der Engine benutzen:

  • Server und Clients für Computer-zu-Computer-Kommunikation aufbauen.
  • Informationen zwischen Computern austauschen.


Grundprinzip: Server und Client

Netzwerkverbindungen zwischen Computern werden im Allgemeinen als Server-Client-Verbindungen bezeichnet. Das Grundprinzip funktioniert so:

  • Ein Server bietet seine Dienste beliebig vielen Clients an.
  • Ein Client kann einen Server kontaktieren, um seine Dienste in Anspruch zu nehmen.

Entsprechend gibt es in der Engine Alpha die Klassen Server und Client. Server und Client können beide sowohl Nachrichten empfangen als auch verschicken.

Einen Server erstellen

Ein Server kann einfach mit dem Konstruktor aufgebaut werden:

public Server(int port)

Der Port ist eine Kennzahl, mit der verschiedene Internetdienste sich unterscheiden können, z.B. hat HTTP Port 80. Du kannst für deinen Server einen beliebigen Port wählen, allerdings sollte deine Portnummer größer als 1024 sein, da bis dahin die Nummern für bekannte Dienste reserviert sind (sog. Well Known Ports).

Damit ist der Server auch schon erstellt und bereit, Clients zu empfangen.

Einen Client erstellen

Nachdem ein Server begonnen hat zu warten, macht es erst Sinn, den Client zu starten. Der Client kann mit diesem Konstruktor aufgebaut werden:

public Client(String ipAdresse, int port)

Der Parameter ipAdresse beschreibt die IP-Adresse des Servers, mit dem sich der Client verbinden soll. Sie wird als String übergeben, z.B. "198.162.0.2", oder "123.56.23.1".

Der Port funktioniert wie bereits in der Server-Sektion beschrieben. Damit sich der Client auch wirklich mit dem Server verbindet, muss er nicht nur die IP-Adresse des Servers angeben sondern auch die Portnummer, auf welcher der Server lauscht.

Nachrichten verschicken und empfangen

Die grundlegenden Methoden

Hat sich ein Client mit dem Server verbunden, können beide Seiten Nachrichten austauschen. Hierfür haben beide Klassen dieselben Methoden:

Sende-Methode Funktion (Sende-Methode) -> Zugehörige Empfange-Methode Funktion (Empfange-Methode)
public void sendeString (String string)
Sendet den übergebenen Wert des entsprechenden Datentyps an den Kommunikationspartner.
empfangeString (String string)
Wird automatisch aufgerufen, wenn der Kommunikationspartner, einen Wert des entsprechenden Datentyps sendet. Der Wert wird im Parameter übergeben.
public void sendeInt (int i)
empfangeInt (int i)
public void sendeByte (byte b)
empfangeByte (byte b)
public void sendeDouble (double d)
empfangeDouble (double d)
public void sendeChar (char c)
empfangeChar (char c)
public void sendeBoolean (boolean b)
empfangeBoolean (boolean b)
public void verbindungSchließen()
Informiert den Kommunikationspartner, dass die Kommunikation ab sofort eingestellt wird (und schließt dann direkt die Verbindung).
public void verbindungBeendet()
Wird aufgerufen, wenn der Kommunikationspartner die Verbindung beendet (anschließend wird die Verbindung auch geschlossen).

Senden & Empfangen: Client

Senden und Empfangen als Client funktioniert, indem man eine eigene Klasse (z.B. MeinClient) von Client ableitet. Alle Sende&Empfange-Methoden sind bereits im Client enthalten. Die Sende-Methoden kann man also direkt ausführen (da sie von Client vererbt werden), die Empfange-Methoden, auf die man reagieren will, kann man einfach überschreiben. Konkret funktioniert dein Client also zum Beispiel so:

import ea.*;

public class MyClient
extends Client {

    //...

    public MyClient(String ipAdresse) {
        //Super-Konstruktor aufrufen. Portnummer ist identisch mit der des Servers.
        super(ipAdresse, 12345);

        //weiteres ...
    }

    //Ich will auf String-Sendungen reagieren. Deshalb überschreibe ich die
    //entsprechende Empfange-Methode!
    @Override
    public void empfangeString(String string) {
        if(string.equals("versionsnummer-angeben")) {
            //Ich kann die sende-Methode einfach aufrufen
            sendeInt(3);
        }
    }

    //...

}

Senden & Empfangen: Server

Senden und empfangen ist beim Server komplizierter. Ein Client hat immer nur eine Verbindung zu einem Server, daher ist klar, wohin er Daten sendet und woher er sie empfängt. Ein Server hingegen kann mehrere Verbindungen zu mehreren Clients gleichzeitig haben. Daher muss man sich bei jeder zu sendenden Nachricht fragen "Wohin damit?" und bei jeder empfangenen Nachricht "Woher kommt die?".

Hierzu gibt es zwei Möglichkeiten in der Engine:

  • Broadcast-Methode: Jede zu sendende Nachricht wird an jeden Client geschickt. Bei empfangenen Nachrichten wird nicht unterschieden, von welchem Client sie kommen.
  • Dedizierte Methode: Jede Verbindung zu jedem Client, die der Server hat, wird einzeln behandelt. Sie hat jeweils eigene Sende/Empfange-Methoden nur für diesen einen Client.

Broadcast-Methode

Diese Methode ist die einfachere. Senden funktioniert hierbei genau wie beim Client. Die Klasse Server hat ebenfalls alle Senden-Methoden. Ein solcher Aufruf sorgt dafür, dass die zu übermittelnde Nachricht direkt an alle Clients übermittelt wird.

Empfangen ist ein klein wenig umständlicher. Hierfür musst du das Interface Empfaenger implementieren, welches einfach nur alle 'Empfange-Methoden beinhaltet. Dieses kannst du anschließend als globalen Empfänger beim Server anmelden. Hierfür gibt es in der Klasse Server die Methode:

public void globalenEmpfaengerSetzen (Empfaenger e)

Nach Aufruf dieser Methode wird für jede Nachricht, die der Server empfängt - ganz egal von welchem Client - die entsprechende Empfangen-Methode des übergebenen Empfängers ausgeführt. Konkret kann das zum Beispiel so aussehen:

public class MeineBroadcastServerApp 
implements Empfaenger {
    
    //Der eigentliche Server
    private Server server;

    // ...

    public MeineBroadcastServerApp() {
        //Beliebige Portnummer (>1024); der zugehörige Client muss
        //Allerdings dieselbe Portnummer haben!
        server = new Server(54321);

        //Melde dieses Objekt als globalen Empfänger für den Server an.
        server.globalenEmpfaengerSetzen(this);
    }

    /* --- Empfaenger-Methoden --- */

    @Override
    public void empfangeString (String string) {
        server.sendeString("Info an alle: Ich habe gerade \" " + string + " \" übersendet bekommen.");
    }

    @Override
    public void verbindungBeendet () {
        server.sendeString("Info an alle: Jemand hat gerade seine Verbindung zu mir getrennt");
    }

         // Info: Diese Methoden müssen implementiert werden.
         //       Sie bleiben leer, da sie hier nicht verwendet werden.

    @Override
    public void empfangeInt (int i) {
    }

    @Override
    public void empfangeByte (byte b) {
    }

    @Override
    public void empfangeDouble (double d) {
    }

    @Override
    public void empfangeChar (char c) {
    }

    @Override
    public void empfangeBoolean (boolean b) {
    }
}

Dedizierte Methode: Die Klasse NetzwerkVerbindung

Möchtest du, dass dein Server jeden Client einzeln behandelt, musst du anders vorgehen. Für jede Verbindung eines Servers zu einem Client gibt es eine Instanz der Klasse NetzwerkVerbindung. Diese enthält alle Sende-Methoden. Für Empfange-Methoden hingegen nimmst du - wie beim Broadcast-Verfahren - ein Empfaenger-Objekt. Melde es an der NetzwerkVerbindung an mit:

public void empfaengerHinzufuegen(Empfaenger e)

Der Server gibt seine Netzwerkverbindung über eine Methode heraus:

public NetzwerkVerbindung naechsteVerbindungAusgeben ()

Die Methode naechsteVerbindungAusgeben() gibt die nächste NetzwerkVerbindung. Solange es noch Clients gibt, deren Netzwerverbindungen nicht durch diese Methode ausgegeben wurden, wird die älteste dieser Verbindungen zurückgegeben (First Come First Served - Verfahren). Wird diese Methode aufgerufen, wenn es keine neuen Verbindungen mehr gibt, bleibt der Thread, in dem diese Methode aufgerufen wurde eingefrohren (im Wartezustand), bis sich ein neuer Client am Server anmeldet.

Dieses Modul ist sicherlich eines der komplexeren der Engine. Ein Blick auf konkrete Projekte kann hier sicherlich helfen. Hier findest du:

Beide Projekte demonstrieren recht anschaulich die wichtigsten Arbeitsschritte bei der Netzwerkkommunikation mit der Engine.

Sonstiges zu den Netzwerkfunktionen

Firewall

Es kann passieren, dass deine Firewall den Internetzugang deines Spiels blockiert. Wenn du keine Verbindung zustande bekommst, könnte es an so einer Blockade liegen. Füge dann eine Ausnahme in deiner Firewall für dein Spiel hinzu.


Fehlermeldungen

Wenn du die Engine mit BlueJ benutzt, kann es passieren, dass dein Programm nicht mehr funktioniert, nachdem du es schonmal gestartet hast. Das liegt daran, dass BlueJ die virtuelle Maschine in der Regel nicht mit deinem Spiel beendet. Das Problem lässt sich leicht beheben, indem du die virtuelle Maschine manuell zurücksetzt. Mache dazu einen Rechtsklick auf den Balken links unten und klicke auf virtuelle Maschine zurücksetzen.


Automatisches Ressourcen aufräumen

Normalerweise entsteht beim Schließen von Verbindungen zwischen Computern (sog. Streams) schnell "Müll": Wenn man vergisst, nach Abbrechen der Verbindung auf einer Seite, dasselbe auch auf der anderen Seite zu tun, können schnell Fehler passieren. Die Netzwerkfunktion der Engine stellt sicher, dass solche Situationen nicht passieren können. Wenn du selbst eine Netzwerkkommunikation (außerhalb der Engine) implementierst, solltest du darauf achten, alle Streams zu schließen, die du geöffnet hast.

Das funktioniert leider nicht standardmäßig auf BlueJ, da die Laufzeitumgebung nicht mit der Software beendet wird (s.o).