Es wurde bereits erläutert, dass eine Übertragung von Daten über Netzwerke in einer Client-Server-Architektur immer aus einem Client und einem Server besteht, die Daten austauschen. Weiterhin kann man unterscheiden zwischen Inhalten, die ausgetauscht werden, und Protokolldaten, die den Zweck und Aufbau der gesendeten bzw. empfangenen Daten angeben. Im Folgenden werden die Funktionen des Servers, des Clients und des Protokolls erläutert. Protokollformat In Kapitel 5.4 (Streams, Sicherheit und Networking) wurde bereits erläutert, dass in einer Client-Server-Architektur eine Protokollkomponente, die den Einsatzzweck gesendeter bzw. empfangener Daten festlegt, durchaus sinnvoll ist. Eine derartige Komponente kann entweder in den Code des Servers bzw. des Clients integriert sein oder im Fall eines sehr komplexen Protokolls auch in Form einer weiteren Klassenhierarchie. Betrachtet man das Beispiel Schiffe versenken, so stellt man fest, dass Client und Server immer die folgenden zwei Arten von Daten austauschen: Information über die Art der Daten, ob bspw. die Daten Schusskoordinaten enthalten, oder ob Server bzw. Client einen Treffer erzielt haben. Koordinaten Es ist daher die Aufgabe des Protokolls, ein vollständiges Spielobjekt zu senden bzw. zu empfangen. Hierzu wurde die Klasse Daten implementiert, die wie folgt realisiert ist: import java.awt.*; public class Daten { static final int SCHUSS = 0; static final int WASSER = 1; static final int TREFFER = 2; static final int UNBEKANNT = 3; Point p; int status; public Daten () { p = new Point(-1,-1); status = UNBEKANNT; } } Diese Klasse wird sowohl vom Server als auch vom Client eingesetzt. Server bzw. Client können Schüsse abfeuern und Wassertreffer oder Treffer melden. Jedes Daten-Objekt besteht daher aus der Art dieser Daten (status) und einem Koordinatenpaar, das angibt, worauf sich die Statusinformation bezieht. Im Folgenden wird die Verwendung dieser Klasse im Server-Programm bzw. im Client-Programm erklärt. Server Der Server muss, ähnlich wie auch der Client, die folgenden Aufgaben durchführen: Aufbauen einer Netzverbindung Senden von Daten Empfangen von Daten Schließen einer Verbindung Diese Schritte wurden wie folgt implementiert. Den Aufbau einer Verbindung hat der Leser bereits kennengelernt. import java.net.*; import java.io.*; import java.awt.*; public class SVServer { String sendeZeile, empfangsZeile; PrintWriter senden; BufferedReader empfangen; ServerSocket serverSocket; Socket clientSocket; public SVServer() throws IOException { serverSocket = null; try { serverSocket = new ServerSocket(4444); } catch (IOException e) { System.err.println("Port nicht verfügbar: 4444."); System.exit(1); } clientSocket = null; try { clientSocket = serverSocket.accept(); } catch (IOException e) { System.err.println("Accept fehlgeschlagen."); System.exit(1); } senden = new PrintWriter(clientSocket.getOutputStream(), true); empfangen= new BufferedReader(new InputStreamReader(clientSocket.getInputStream()) ); } Wie bereits erläutert wurde, werden hierbei Sockets eingerichtet, über die Daten gesendet bzw. empfangen werden können. Zum Senden und Empfangen werden Streams implementiert. Die im Folgenden beschriebene Methode getDaten() empfängt Daten vom Client. Hierbei findet auch das Protokoll Verwendung. Die Methode liest zuerst die Statusinformation, anschließend das Koordinatenpaar. public Daten getData () throws IOException{ Daten d = new Daten(); //Auslesen der Statusinformation if ((empfangsZeile = empfangen.readLine()) != null){ if (empfangsZeile.equalsIgnoreCase("0")) d.status = d.SCHUSS; if (empfangsZeile.equalsIgnoreCase("2")) d.status = d.TREFFER; } //Empfangen der Koordinaten if ((empfangsZeile = empfangen.readLine()) != null) d.p.x = Integer.parseInt(empfangsZeile); if ((empfangsZeile = empfangen.readLine()) != null) d.p.y = Integer.parseInt(empfangsZeile); System.out.println("Client schickt: ("+d.p.x+","+d.p.y+")"); return d; } Zum Senden von Daten wird die Methode sendData() verwendet, die die Statusinformation und ein Koordinatenpaar sendet. Hierbei sei auf einen kleinen Trick hingewiesen. Um eine Zahl in einen String konvertieren zu können, wird eine Kombination aus Anführungszeichen und der Zahl verwendet. public void sendData (Daten d) { //Senden der Statusinfo, der x- und y-Koordinaten senden.println(""+d.status); senden.println(""+d.p.x); senden.println(""+d.p.y); } Zum Abbau der Verbindung müssen die Streams und die Sockets mittels close geschlossen werden. public void beenden () { try { senden.close(); empfangen.close(); clientSocket.close(); serverSocket.close(); } catch (IOException e) { System.out.println("Fehler beim Schliessen der Verbindung"); } } } Client Die Funktionalität des Clients unterscheidet sich von der des Servers in Bezug auf den Aufbau und den Abbau einer Verbindung. Die Prozeduren zum Senden und Empfangen der Daten sind jedoch gleich. import java.io.*; import java.net.*; import java.awt.*; public class SVClient { String sendeZeile, empfangsZeile; Socket SVSocket; PrintWriter senden = null; BufferedReader empfangen= null; Daten daten; GUISVUser s; public SVClient (GUISVUser s) throws IOException{ this.s = s; SVSocket = null; try { SVSocket = new Socket("localhost", 4444); senden = new PrintWriter(SVSocket.getOutputStream(), true); empfangen = new BufferedReader(new InputStreamReader(SVSocket.getInputStream() )); } catch (UnknownHostException e) { System.err.println("Host unbekannt: localhost."); System.exit(1); } catch (IOException e) { System.err.println("I/O abgelehnt."); s.ausgabeFenster("Probleme bei der Netzwerkverarbeitung"); return; } } Der Verbindungsaufbau des Clients wurde bereits in Kapitel 5.4 detailliert betrachtet. Es sei darauf hingewiesen, dass eine Fehlermeldung ausgegeben werden muss, wenn keine Netzwerkverbindung zustande kommt. Hierzu wird die Methode ausgabeFenster aufgerufen, die ein dementsprechendes Popup-Fenster erzeugt. Die Methoden getData und sendData entsprechen denen des Servers. public Daten getData () throws IOException{ Daten d = new Daten(); if ((empfangsZeile = empfangen.readLine()) != null){ if (empfangsZeile.equalsIgnoreCase("0")) d.status = d.SCHUSS; if (empfangsZeile.equalsIgnoreCase("2")) d.status = d.TREFFER; } if ((empfangsZeile = empfangen.readLine()) != null) d.p.x = Integer.parseInt(empfangsZeile); if ((empfangsZeile = empfangen.readLine()) != null) d.p.y = Integer.parseInt(empfangsZeile); System.out.println("Server schickt: ("+d.p.x+","+d.p.y+")"); return d; } public void sendData (Daten d) { //Senden der Statusinfo, der x- und y-Koordinaten senden.println(""+d.status); senden.println(""+d.p.x); senden.println(""+d.p.y); } Ähnlich wie beim Server werden abschließend die Streams und die Sockets geschlossen. public void beenden () throws IOException{ try { senden.close(); empfangen.close(); SVSocket.close(); } catch (IOException e) { s.ausgabeFenster("Probleme bei der Netzwerkverarbeitung"); } } } Es sei darauf hingewiesen, dass im Unterschied zu Kapitel 5.4 das Protokoll in die Netzwerkklassen integriert wurde. Aufgrund der geringen Komplexität würde eine eigenständige Verarbeitung nicht sinnvoll sind.