Beispiel: Socket-Grundlagen


Inhalt:

Serverseite
Client
Multiple Clients versorgen
Objekte versenden

Hier werden ein paar Grundlagen der Netzwerkkommunikation über Sockets erklärt.

Sun-Grundlagentutorial zu Sockets: http://java.sun.com/docs/books/tutorial/networking/sockets/

Serverseite

Ein Serverprogramm, das auf einem bestimmten Port lauscht und z.B. auf jede Anfrage eine feste Rückgabe liefert, kann so aussehen (Beispiel mit ganz schlimmer Fehlerbehandlung!):
package de.fhw.swtprojekt.knauf.simplesocket;

import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;

public class SimpleServer
{
  public static void main(String[] args)
  {
    try
    {
      //Warte auf Anfragen auf Port 13000:
      ServerSocket serverSocket = new ServerSocket(13000);
  
      //Eine einzige Anfrage entgegennehmen:
      Socket clientSocket = serverSocket.accept();
  
      //Die Rückgabe in einen Ausgabestream schreiben:
      PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true);
      //Senden eines Newline sorgt dafür, dass der PrintWriter die Ausgabe "abschickt". Alternativ müsste "flush" aufgerufen werden.
      out.println("Serverantwort");
    }
    catch (Exception e)
    {
      e.printStackTrace();
    }
  }
 }
Dieses Programm kann unter Windows mit dem telnet-Befehl getestet werden: telnet localhost 130000
Die Ausgabe ist "Serverantwort", danach bendet sich telnet selbst, da der Server nach Abarbeitung der ersten Clientanfrage stirbt.


Client

Es wird ein Client geschrieben, der eine Verbindung zum Server aufbaut und von diesem die Antwort "Serverantwort" erhält (Beispiel ohne Fehlerbehandlung!):
package de.fhw.swtprojekt.knauf.simplesocket;

import java.io.InputStreamReader;
import java.io.BufferedReader;
import java.net.Socket;

public class SimpleClient
{
  public static void main(String[] args)
  {
    try
    {
      //Verbindung zu Port 13000 auf localhost aufbauen:
      Socket socket = new Socket ("localhost", 13000);

      //Eine Zeile lesen:
      BufferedReader in = new BufferedReader(new InputStreamReader(socketServer.getInputStream()));
      String serverResponse = in.readLine();
      System.out.println("Server-Antwort: " + serverResponse);
  
      //Socket dichtmachen:
      socket.close();
    }
    catch (Exception ex)
    {
      ex.printStackTrace();
    }
  }


Multiple Clients versorgen

Folgendes Beispiel zeigt einen Server, der Verbindungen von mehreren Servern akzeptiert. Hierzu läuft der Server in einer Endlosschleife: sobald über "serverSocket.accept()" eine Verbindung eingeht, wird ein neuer Thread gestartet der die Verarbeitung dieser Verbindung übernimmt. Der Server ist direkt danach für weitere Verbindungen bereit.

Die Kommunikation zwischen Server und Client läuft so ab: Client schickt einen String, der Server liefert das "toUpper" dieses Strings zurück.

Server-Code
package de.fhw.swtprojekt.knauf;

import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;

/**
 * Server-Seite des simplen Textverteiler-Servers.
 * 
 * @author Wolfgang Knauf
 * 
 */
public class TextServer
{

  /**Server lauscht auf Port 13000 und nimmt in einer Endlosschleife Antworten entgegen.
   * 
   * @param args
   */
  public static void main(String[] args)
  {
    ServerSocket serverSocket = null;

    //An Port 13000 binden:
    try
    {
      serverSocket = new ServerSocket(13000);
    }
    catch (IOException e)
    {
      System.out.println("Binden an Port  13000 schlug fehl: " + e.getMessage());
      System.exit(-1);
    }
    
    //In einer Endlosschleife auf eingehende Anfragen warten.
    while (true)
    {
      try
      {
        //Blocken, bis eine Anfrage kommt:
        System.out.println ("ServerSocket - accepting");
        Socket clientSocket = serverSocket.accept();
        
        //Wenn die Anfrage da ist, dann wird ein Thread gestartet, der 
        //die weitere Verarbeitung übernimmt.
        System.out.println ("ServerSocket - accept done");
        Thread threadHandler = new Thread(new TextServerHandler(clientSocket) );
        threadHandler.start();
		
		System.out.println ("ServerSocket - Thread started, next client please...");
      }
      catch (IOException e)
      {
        System.out.println("'accept' auf Port 13000 fehlgeschlagen");
        System.exit(-1);
      }
      
    }
  }
} 
Handler für einzelne Clientverbindung
package de.fhw.swtprojekt.knauf;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;

/**
 * Diese Klasse übernimmt die Kommunikation zwischen einem einzelnen Client und dem Server.
 * Sie nimmt Strings entgegen und liefert das "toUpper" zurück.
 * 
 * @author Wolfgang Knauf
 * 
 */
public class TextServerHandler implements Runnable
{
  /**
   * Dies ist die Client-Verbindung.
   * 
   */
  private Socket clientSocket;

  /**
   * Konstruktor, dem der Client-Socket übergeben wird.
   * 
   * @param _clientSocket Socket-Verbindung zum Client.
   */
  public TextServerHandler(Socket _clientSocket)
  {
    this.clientSocket = _clientSocket;
  }

  /**Thread wird ausgeführt: alle Daten, die der Client schickt, in Upper Case konvertieren
   * und zurückschicken. 
   * 
   */
  @Override
  public void run()
  {
    try
    {
      System.out.println("Thread " + Thread.currentThread().getId() + " für Clientanfrage gestartet !");
      // Eingabe-Reader und Ausgabe-Writer erzeugen.
      // Es wird hier nur mit Text gearbeitet und zeilenweise eingelesen bzw. geschrieben.
      PrintWriter out = new PrintWriter(this.clientSocket.getOutputStream(), true);
      BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));

      // In einer Endlosschleife auf Eingaben horchen bis die Verbindung beendet wird
      // und ein "readLine" dadurch nichts mehr zurückliefert. 
      String strInput = null;
      System.out.println ("Thread " + Thread.currentThread().getId() + " für Clientanfrage wartet auf Antwort...");
      while ((strInput = in.readLine()) != null)
      {
        System.out.println("Thread " + Thread.currentThread().getId() + ": Eingabe: " + strInput);

        // Die Eingabe in "UpperCase" umwandeln und als komplette Zeile an Client zurückschieben.
        // "println" sorgt dafür, dass die Daten gesendet werden.
        out.println(strInput.toUpperCase());
        
        System.out.println("Thread " + Thread.currentThread().getId() + " für Clientanfrage next round");
      }

      System.out.println("Thread " + Thread.currentThread().getId() + " ist fertig!");
    }
    catch (IOException ioEx)
    {
      System.out.println("Fehler beim Schreiben:" + ioEx.getMessage());
    }
  }
}
Client
Der Client liest Tastatureingaben von der Standardeingabe, bis der User "X" eingibt. Die Eingaben werden zum Server geschickt, dieser liefert das "toUpper" der Eingabe zurück.
package de.fhw.swtprojekt.knauf.sockettest.client;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
import java.net.UnknownHostException;

/**Client-Main-Class, baut eine Socket-Verbindung zum Server auf und schickt die 
 * Tastatureingaben zeilenweise dahin. Eingabe von "X" beendet das Programm.
 * 
 * @author Wolfgang Knauf
 *
 */
public class TextClient
{
  /**Main-Methode, hier läuft der ganze Thread ab.
   * 
   * @param args
   */
  public static void main(String[] args)
  {
    System.out.println ("Text eingeben, 'Enter' schickt ihn zum Server. Eingabe von 'X' zum Beenden!");
    
    // Tastatureingaben werden eingelesen:
    BufferedReader reader = new BufferedReader ( new InputStreamReader (System.in));
    
    //Server-Verbindung aufbauen:
    Socket socketServer = null;
    try
    {
      socketServer = new Socket ("localhost", 13000);
    }
    catch (UnknownHostException ex)
    {
      System.out.println("UnknownHostException bei Verbindung zu Host 'localhost', Port 13000: " + ex.getMessage());
      System.exit(-1);
    }
    catch (IOException ex)
    {
      System.out.println("IOException bei Verbindung zu Host 'localhost', Port 13000: " + ex.getMessage());
      System.exit(-1);
    }
    
    try
    {
      //Eingabe-Reader/Ausgabe-Writer erzeugen:
      PrintWriter out = new PrintWriter(socketServer.getOutputStream(), true);
      BufferedReader in = new BufferedReader(new InputStreamReader(socketServer.getInputStream()));
      //Solange der User etwas eingibt (und danach Enter drückt), werden die Daten
      //zum Server geschickt. Eingabe von "X" beendet alles.
      String textInput = null;
      while ( (textInput = reader.readLine() ) != null && !"X".equals(textInput))
      {
        System.out.println("Client-Eingabe: " + textInput);
        
        //Ab zum Server:
        out.println(textInput);
        out.flush();

        System.out.println("Warten auf Server-Antwort...");
        //Server schickt uns "toUpper" der Eingabe:
        String textServer = in.readLine();
        System.out.println("Server-Antwort: " + textServer);
      }
      //User hat "X" eingegeben: Socket dichtmachen.
      socketServer.close();
    }
    catch (IOException e)
    {
      System.out.println ("IOException: " + e.getMessage());
      System.exit(-1);
    }
  }
}

Objekte versenden

Bisher wurden nur Strings verschickt. Man kann allerdings auch komplette Objekte in ihrer serialisierten Form übers Netzwerk verschicken, indem man sie auf dem Client in einen Bytestream serialisiert und auf Serverseite de-serialisiert.

Server-Seite (erwartet z.B. ein Objekt vom Typ "Point" vom Client):
import java.awt.Point;
import java.io.ObjectInputStream;

....
  
  try
  {
    //ObjectInputStream erlaubt das Einlesen von binär serialisierten Objekten:
    ObjectInputStream in = new ObjectInputStream(socketServer.getInputStream());
  
    //Hier wird ein Point-Objekt erwartet:  
    Point point = (Point) in.readObject();
	
    ...
  }
  catch (IOException e)
  {
    ...
  }
  catch (ClassNotFoundException classEx)
  {
    ...
  }
Client-Seite (will "Point"-Objekte zum Server verschicken):
import java.io.ObjectOutputStream;
import java.awt.Point;

...

  try
  {
    ObjectOutputStream out = new ObjectOutputStream(clientSocket.getOutputStream());
    
    out.writeObject( new Point (10, 20) );
  }
  catch (IOException ioEx)
  {
    ..
  }
Man kann beliebige Klassen übers Netz schieben. Für diese Klassen gelten nur zwei Grundregeln: Idealerweise liegt die zu übertragende Klasse in einer eigenen JAR-Datei, die von Client und Server gemeinsam referenziert wird.

Java ist in der Lage, alle Membervariablen der Klasse beim Serialisieren ebenfalls in Bytes umzuwandeln. Soll dies für einzelne Variablen nicht geschehen, müssen sie mit dem Schlüsselwort transient markiert werden.

Falls man eine Klasse baut, die nicht über reines Membervariablen-serialisieren verarbeitbar ist, muss man weitere Methoden implementieren, um eigene Logik ins Spiel zu bringen (siehe Javadoc zu
java.io.ObjectInputStream):
  private void writeObject(java.io.ObjectOutputStream out)
     throws IOException
 private void readObject(java.io.ObjectInputStream in)
     throws IOException, ClassNotFoundException;
 private void readObjectNoData() 
     throws ObjectStreamException;

ACHTUNG 1:

Der Konstruktor des ObjectInputStream blockiert scheinbar, wenn auf dem Inputstream noch keine Daten anliegen. Deshalb kann man ihn erst aufrufen, wenn man weiß, dass im nächsten Schritt Daten vom Server zu erwarten sind. Angenommen, das obige "Multiple Clients"-Beispiel sei so modifiziert, dass der Client die Strings über einen ObjectInputStream zum Server schickt, dieser schickt seine Antworten wie gehabt über einen PrintWriter. Dann würde der Code der Schleife so aussehen:
      ObjectInputStream in = null;
      
      while ( (textInput = reader.readLine() ) != null && !"X".equals(textInput))
      {
        out.println(textInput);
        out.flush();

        if (in == null)
        {
          in = new ObjectInputStream(socketServer.getInputStream());
        }
        String textServer = (String) in.readObject();
        System.out.println("Server-Antwort: " + textServer);
      }
Wichtig ist hier, dass der ObjectInputStream direkt vor dem ersten Einlesen erzeugt wird. Warum dies nur einmal geschieht, wird im nächsten Hinweisfeld begründet.

ACHTUNG 2:

Folgende Fehlermeldung taucht bei der zweiten Anfrage auf:
java.io.StreamCorruptedException: invalid type code: AC
IOException: invalid type code: AC
	at java.io.ObjectInputStream.readObject0(Unknown Source)
	at java.io.ObjectInputStream.readObject(Unknown Source)
	at ...eigener Code...
Hier war die Ursache, dass der ObjectInputStream auf Server-Seite zum Einlesen der Daten bei JEDEM Lauf innerhalb der Verarbeitungsschleife neu erzeugt wurde:
      BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
      String strInput = null;
      while ((strInput = in.readLine()) != null)
      {
        ObjectOutputStream out = new ObjectOutputStream(clientSocket.getOutputStream());
        out.writeObject(strInput.toUpperCase());
        out.flush();
      }
Dies muss außerhalb der Schleife einmalig pro Socket-Verbindung geschehen. Ich vermute, dass das gleiche auch für einen ObjectOutputStream gilt!



Stand 25.03.2009
Historie:
29.03.2008: Erstellt
25.03.2009: Threading-Bug im "Multiple Clients versorgen"-Beispiel, die simplen Beispiele enthalten vollen Code.