3. Netzwerk-Programmierung


Mehrprozessorsysteme ohne gemeinsamen Hauptspeicher benötigen spezielle Leitungen zur Kommunikation.

Das Schema des Nachrichtenaustauschs ist in Abbildung 2 dargestellt.

Abbildung 2: Schema des Nachrichtenaustauschs


3.1 Sockets

Spezifikation von ServerSocket:

   public ServerSocket(int port) throws IOException 
   public Socket accept() throws IOException

Spezifikationen von Socket

   public Socket(String host, int port)
          throws UnknownHostException, IOException
   public InputStream getInputStream() throws IOException
   public OutputStream getOutputStream() throws IOException

Zuordnung von passenden Datenströmen:

Spezifikation der benötigten Konstruktoren und Methoden

   public ObjectOutputStream(OutputStream out) throws IOException 
   public void flush() throws IOException
   public ObjectInputStream(InputStream in)
          throws IOException, StreamCorruptedException

Datenübertragung mit Send-Operation ( send) und Empfangs-Operation ( recieve).

Spezifikation aus ObjectOutputStream

   public final void writeObject(Object obj)
          throws IOException

Spezifikation aus ObjectInputStream

   public final Object readObject()
          throws OptionalDataException, 
                 ClassNotFoundException, IOException

3.2 Beispiel SocketChannel

Der Konstruktor nimmt einen Socket als Eingabe und setzt die Objekt-Ströme aus den entsprechenden Strömen auf.

Die Methode close() dient zum schließen des Kanals.

send()- und receive()-Methoden dienen der Datenübertragung

   import java.io.*;
   import java.net.*;
   
   public class SocketChannel {

     private ObjectInputStream in;
     private ObjectOutputStream out;
   
     private Socket soc;
   
     public SocketChannel(Socket s) throws IOException {
       soc = s;
       if (checkOrder(s)) {
         in = new ObjectInputStream(s.getInputStream());
         out = new ObjectOutputStream(
                   s.getOutputStream());
       }
       else {
         out = new ObjectOutputStream(
                   s.getOutputStream());
         in = new ObjectInputStream(s.getInputStream());
       }
     }

     public void close() {
       if (in != null) {
          try { in.close(); } catch (IOException e) { }
       }
       if (out != null) {
          try { out.close(); } catch (IOException e) { }
       }
       if (soc != null) {
          try { soc.close(); } catch (IOException e) { }
       }
     }
   
     private boolean checkOrder(Socket s) 
                     throws IOException {
       int p1 = s.getLocalPort();
       int p2 = s.getPort();
       if (p1 < p2) return true;
       else if (p1 > p2) return false;
   
       int a1 = s.getLocalAddress().hashCode();
       int a2 = s.getInetAddress().hashCode();
       if (a1 < a2) return true;
       else if (a1 > a2) return false;
   
       throw new IOException(); // this shouldn't happen
     }
   }

Die Sende- und Empfangs-Methoden sind wie folgt.

     public void send(Object v) throws IOException {
         out.writeObject(v);
     }
Im Fehlerfall wird eine IOException ausgelöst, die dann vom Aufrufer behandelt werden muß.

     public Object receive() 
                   throws IOException, 
                          ClassNotFoundException {
       return in.readObject();
     }
Neben einer IOException kann auch noch eine ClassNotFoundException ausgelöst werden, falls das Objekt zu einer dem Empfänger unbekannten Klasse gehört.

3.3 Beispiel Kanalfabrik

Zwei Grundideen zur Lösung

Der Konstruktor der Klasse ChannelFactory erzeugt einen Server-Socket und startet sich dann selbst als Thread.

   import java.io.*;
   import java.net.*;
   
   public class ChannelFactory extends Thread {
     
     public final static int DEFAULT_PORT = 4711;
     private int port;
   
     private BoundedBuffer buf = new BoundedBuffer(10);
   
     private ServerSocket srv = null;
   
     public ChannelFactory(int p) {
       if (p<=0) { port = DEFAULT_PORT; } 
          else { port = p; }
       try {
           srv = new ServerSocket(port);
           this.start();
           System.out.println(
                  "server started on port "+port);
       } catch (IOException e) 
               { System.out.println(e); }
     }
   
     public ChannelFactory() {
       this(DEFAULT_PORT); 
     }
     public void run() {
       while (true) {
         try {
             System.out.println(
                 "waiting for connection");
             Socket s = srv.accept();
             System.out.println("connection accepted");
             SocketChannel c = new SocketChannel(s);
             buf.put( (Object) c );
         } catch (IOException e) {
           System.out.println(e);
         }
       }
     }
   
     public SocketChannel getChannel() 
            throws IOException {
       return (SocketChannel)buf.get();
     }

     public SocketChannel getChannel(String h, int p) 
            throws IOException {
       if (p<=0) { p = port; } 
       SocketChannel c = null;
       System.out.println("connecting to "+h);
       while (c == null) {
           try { c = new SocketChannel( 
                     new Socket(h, p) );
           } catch (IOException e) {
             System.out.println(e);
             // wait server ready
             System.out.println("Server on "+h+
                    " not ready");
             try {
                 Thread.currentThread().sleep(
                        (int)(5000));
             } catch (InterruptedException e1) { } 
           }
       }
       System.out.println("connected");
       return c;
     }
   }

ChannelFactory löst das Reihenfolge-Problem und das Deadlock-Problem.

Das Reihenfolge-Problem wird durch Warten und Neuversuch in der Methode getChannel(String h, int p) gelöst.

Die Lösung des Deadlock-Problems wie folgt.

Wir setzen voraus, daß für jeden Prozeß genau einmal der Konstruktor ChannelFactory() und für jeden Kanal genau einmal die Methode getChannel() und die Methode getChannel(String h, int p) aufgerufen wird.

Betrachten wir die drei Programmzustände:
  1. Aufruf der Konstruktoren ChannelFactory(). Damit wird ein Thread gestartet, der Verbindungsanfragen vom Server-Socket entgegennimmt und speichert.

  2. Aufruf von getChannel(String h, int p) auf allen Client-Seiten der Kanäle. Dabei wird die Verbindung zu Sockets aus Punkt 1 hergestellt. Falls diese noch nicht bereit sind, wird gewartet.

  3. Aufruf von allen getChannel() auf den Server-Seiten der Kanäle. Dabei werden nur noch die gespeicherten Verbindungen zurückgegeben. Falls noch keine gespeichert sind, wird gewartet.

Kompliziertere Verbindungsstrukturen mit virtuellen Kanälen oder PVM oder MPI.

3.4 Remote Method Invocation (RMI)

Abbildung 3: Remote Method Invocation, JDK 1.1


RMI Architektur in Abbildung 3

weitere Bestandteile von RMI

Die wesentlichen Schritte der RMI-Kommunikation:

  1. Eine entfernte Klasse muß ein definiertes Interface haben, das das Remote-Interface erweitert. Alle Remote-Methoden müssen RemoteExceptions auslösen können.
  2. Zu dem Interface muß eine passende Implementierung existieren.
  3. Mit Hilfe des RMI-Compilers rmic müssen aus der Remote-Implementierung die entsprechenden Klassen für Stub und Skeleton abgeleitet werden. Mit JDK 1.2 wird kein Skeleton mehr benötigt.
  4. Das RMI Repository, ein Verzeichnis von Server-Methoden, muß mit dem Daemon rmiregistry aktiviert und gestartet werden.
  5. Ein Serverprozess muß ein Exemplar object der Remote-Klasse erzeugen und mit rmi.Naming.bind( "name", object) beim Repository anmelden.
  6. Der Clientprozeß muß ein lokales Exemplar Interface object
    des Remote- Interfaces erzeugen. Dies geschieht mit
    (Interface) rmi.Naming.lookup( "rmi://host/method" )
  7. Alle Remote-Methoden dieser Instanz können dann mit
    result = object.method(parm) wie lokale Methoden verwendet werden.

Mit Java 2, aka JDK 1.2, gibt es ein ausgefeiltes und flexibles Sicherheitsmanagement

eine Datei die alles erlaubt:

  grant {
      permission java.security.AllPermission;
  };

eine Datei die von Überall den Zugang nur über die Ports 4000-5000 erlaubt:

  grant {
      permission java.net.SocketPermission "*:4000-5000", "connect,accept";
  };

3.5 Zusammenfassung


© Universität Mannheim, Rechenzentrum, 1999.