Mehrprozessorsysteme ohne gemeinsamen Hauptspeicher
benötigen spezielle Leitungen zur Kommunikation.
- Ethernet
- Fast-Ethernet
- TCP/IP, Sockets
- Myrinet
- SCI
- ATM
- Programmierung erfordert die Definition von
- geeigneten Leitungen
- Übertragungsprotokollen
- explizite Befehle zum Senden und Empfangen
- kein Schutz von gemeinsamen Variablen erforderlich
Das Schema des Nachrichtenaustauschs ist in Abbildung 2
dargestellt.
Abbildung 2:
Schema des Nachrichtenaustauschs
|
- Java Kommunikation basiert auf TCP/IP Sockets
- von praktisch allen Betriebssystemen unterstützt
- nahezu jede Treiber-Software von Netzwerk Hardware
bietet Unterstützung für TCP/IP
- insbesondere auch High Performance Netzwerke
- Sockets sind aus Programmiersicht die
Software-Schnittstelle zum Netzwerk
- entsprechen etwa den Filehandles
- Java-Netzwerk-Support im Package java.net
- eine oder mehrere Punkt-zu-Punkt Verbindungen
- keine 1-zu-n- oder m-zu-n-Verbindungsnetzwerke
- Implementierung von Verbindungen mit
- Java Klassen ServerSocket und Socket
- Socket-Verbindung wird unsymmetrisch aufgebaut
- ein Ende (der Server Socket)
wartet auf Verbindungswunsch
- anderes Ende (der Client Socket)
versucht eine Verbindung aufzubauen
- wenn dies auf beiden Seiten gelingt
besteht ein zuverlässiger Verbindungskanal
- ab dann Senden und Empfangen möglich
Spezifikation von ServerSocket:
public ServerSocket(int port) throws IOException
public Socket accept() throws IOException
- Konstruktor ServerSocket erzeugt einen neuen
Server Socket an port
- bei Portnummer 0 an einem beliebigen freien Port
- Methode accept() wartet auf einen Verbindungswunsch
- gibt einen Socket für die Verbindung zurück
- blockiert bis eine Verbindung zustande kommt
Spezifikationen von Socket
public Socket(String host, int port)
throws UnknownHostException, IOException
public InputStream getInputStream() throws IOException
public OutputStream getOutputStream() throws IOException
- Konstruktor Socket erzeugt neuen Client Socket
- zu dem angegebenen host und port
- stellt Eingabe- und Ausgabe-Strom zur Verfügung
- Zugriff mit getInputStream() und getOutputStream()
- Strom (Stream) ist eine unformatierte und unstrukturierte Folge
von Daten
- InputStream und OutputStream
bestehen aus Folgen von Bytes
Zuordnung von passenden Datenströmen:
- reine Unicode Zeichen mit Reader und Writer
- im folgenden ObjectStream
- Austausch beliebiger serialisierbarer ( serializable)
Objekte
- Umwandlung von Java-Objekten in einen Daten-Strom
- und typsicheres Versenden und Empfangen
- auch für Datei-Ströme verwendbar
- akzeptiert nur Objekte, die das java.io.Serializable
Interface implementieren
- ist eins der wesentlichen neuen Features von Java 1.1.
- auomatische Serialisierung überschreibbar
- dann muß man die Klasse selbst kodieren und dekodieren
- bei Filehandles keine Serialisierung sinnvoll
- Strom-Header enthält eindeutige Identifikation
der Objekt-Strom-Klasse
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
- Konstruktor ObjectOutputStream erzeugt neuen
Objekt-Ausgabestrom
- zu einem gegebenen OutputStream
- flush() verschickt Daten unmittelbar
- Konstruktor ObjectInputStream erzeugt neuen
Objekt-Eingabestrom
- zu einem gegebenen InputStream
- Passt die Identifikation nicht wird eine
StreamCorruptedException ausgelöst
- z.B. inkompatible JDK-Version, inkompatible Serialisierung
- blockiert, bis ein Objekt-Ausgabe-Strom die
entsprechenden Daten gesendet
Datenübertragung mit Send-Operation ( send)
und Empfangs-Operation ( recieve).
Spezifikation aus ObjectOutputStream
public final void writeObject(Object obj)
throws IOException
- writeObject() schreibt ein Objekt auf den
Ausgabe-Strom
- der gesamte Graph des Objekts wird zerlegt (serialisiert)
- und mit dem Klassennamen und Klassensignatur geschrieben
Spezifikation aus ObjectInputStream
public final Object readObject()
throws OptionalDataException,
ClassNotFoundException, IOException
- readObject() liest ein Objekt von dem
Eingabe-Strom
- der Klassenname und Klassensignatur wird gelesen
- der gesamte Graph des Objekts wird gelesen
- und rekonstruiert
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 Reihenfolge der Erzeugung der Objekt-Ströme
out und in muß vertauscht sein
- da sich Objekt-Ströme bei dem
Verbindungsaufbau über die Verwendung der gleichen
Klassen und JDK-Versionen einigen müssen
- Methode checkOrder vertauscht
- auf dem gleichen Host sind die Ports verschieden,
sonst die IP-Adressen
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.
- gleichzeitiger Aufbau von mehreren Punkt-zu-Punkt
Verbindungen
- etwas mühsam, da Sockets immer eine Unterscheidung
in Clients und Server erzwingen
- z.B. 100 Prozesse in 2d-Torus-Topologie
- pro Prozeß vier Verbindungen (links, rechts, oben und unten)
- Deadlock falls die Reihenfolge des
Verbindungsaufbaus nicht abgestimmt
- z.B. nicht alle Prozesse können in einer Reihe den
linken Kanal gleichzeitig aktivieren
- und Client kann erst versuchen Verbindung aufzubauen,
wenn Server-Teil gestartet
Zwei Grundideen zur Lösung
- die accept()-Aufrufe in Thread auszulagern
- Aufrufe von new Socket() wiederholen, falls
Server noch nicht bereit
- Implementierung in ChannelFactory
- zwei Methoden getChannel() erzeugen SocketChannel
- für die Client-Seite mit angebenem Hostnamen
- für die Server-Seite ohne Hostnamen
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);
}
- in run() wird in einer Endlosschleife
mit accept() gewartet
- trifft eine Anfrage ein, wird aus dem Socket ein
SocketChannel erzeugt
- wird in einem Puffer buf gespeichert
- dann warten auf die nächste Anfrage
- getChannel() liefert einen der gespeicherten
SocketChannel
- falls keine Verbindung gespeichert, blockiert die
Methode bei buf.get().
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();
}
- auf Client-Seite ist getChannel(String h, int p) umfangreicher
- Versuch mit new Socket(host,port) eine
Socket-Verbindung aufzubauen
- in einer while-Schleife solange, bis die
Verbindung zustande kommt
- d.h. bis der gewünschte Server-Teil bereit ist
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:
Aufruf der Konstruktoren ChannelFactory().
Damit wird ein Thread gestartet, der Verbindungsanfragen
vom Server-Socket entgegennimmt und speichert.
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.
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.
- Punkt 1 kann auf jedem beteiligten Rechner terminieren
denn nur Thread gestartet, der auf Verbindungen wartet
- damit terminieren auch alle Programmteile mit Punkt 2 einmal,
denn sie können die gewünschte Verbindung aufbauen.
- damit können auch alle Programmteile mit Punkt 3 terminieren,
denn alle Verbindungen sind schon aufgebaut und gespeichert.
- somit ist das Deadlock-Problem beim Verbindungsaufbau gelöst
Kompliziertere Verbindungsstrukturen mit
virtuellen Kanälen oder PVM oder MPI.
- Ausführung von Methoden auf einem entfernten Rechner
- Prinzip:
- Eingabeparameter werden über eine Netzverbindung
zu einem (entfernten) Rechner geschickt
- die entsprechende Methode wird dort ausgeführt
- die Ergebnisse werden über die Netzverbindung
zurück geschickt
- Analogon zu Remote Procedure Call (RPC)
- schon viele Jahre im Einsatz
- RMI Spezialfall der allgemeinen Kommunikation
- Anstelle von expliziten Sende- und Empfangsoperationen
ein Methodenaufruf mit implizitem Senden und Empfangen
- aber allgemeine Kommunikation auch durch RMI nachbildbar
- Sendeoperation auf eine Methode
- Empfangsoperation auf eine zweite Methode
- Kommunikation eins Servers mit einem Client mittels "call-back"
- versenden von Objekten deren innere Zustände vom Server
modifiziert werden
- z.B. mit setValue und getValue
- Einige [6]
sind der Auffassung, daß Socket-Kommunikation
gänzlich überflüssig ist,
RMI alleine reicht und ist "besser objektorientiert"
- Andere [8],
sehen in wissenschaftlichen Codes doch einige Anwendungsfälle,
bei denen RMI alleine nicht ausreicht,
oder wo zumindest Socket-Kommunikation eine bessere Performance bietet
- Wieder Andere [7]
konzentrieren sich auf eine Optimierung der RMI- und
Serialisierungs-Implementierung
Abbildung 3:
Remote Method Invocation, JDK 1.1
|
RMI Architektur in Abbildung 3
- Computer 'A' bezeichnet den Rechner, von dem aus
eine Methode auf dem entfernten Rechner 'B' aufgerufen werden soll
- 'Client' bezeichnet den Benutzerprozess in Rechner A
- 'Server' bezeichnet den Prozeß, der die aufrufbare Methode
bereitstellt
weitere Bestandteile von RMI
- ein Verzeichnis, genannt 'Repository', der aufrufbaren Objekte
- dieses Repository muß vor beginn aller
RMI Aktivitäten gestartet sein
- Server verwenden
rmi.Naming.bind() oder rmi.Naming.rebind(),
um ein Objekt unter einem Namen bei dem Repository zu registrieren
- Clients sehen mit
rmi.Naming.lookup() bei dem Repository nach ob ein
gewünschtes Objekt vorhanden ist machen es lokal zugänglich
- beim Aufruf im Client wird zunächst ein sogenanntes
'Stub' Objekt aktiviert
- diese sendet die Eingabeparameter an ein 'Skeleton' genanntes
Objekt auf dem Server (JDK 1.1)
- das Skeleton Objekt ruft dann auf dem
Rechner B die gewünschte Methode auf
- der Returnwert wird nach der Terminierung der
Methode an das Stub Objekt zurück geschickt
- dieses übergibt den Wert dann
an das aufrufende Objekt
- der Einsatz von Stub und Skeleton erfolgt transparent
- mit JDK 1.2 wird kein Skeleton mehr benötigt
Die wesentlichen Schritte der RMI-Kommunikation:
- Eine entfernte Klasse muß ein definiertes Interface haben,
das das Remote-Interface erweitert.
Alle Remote-Methoden müssen RemoteExceptions
auslösen können.
- Zu dem Interface muß eine
passende Implementierung existieren.
- 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.
- Das RMI Repository, ein Verzeichnis von Server-Methoden,
muß mit dem Daemon
rmiregistry aktiviert und gestartet werden.
- Ein Serverprozess muß ein Exemplar object der
Remote-Klasse erzeugen und mit
rmi.Naming.bind(
"name",
object)
beim Repository anmelden.
- Der Clientprozeß muß ein lokales Exemplar
Interface object
des Remote- Interfaces erzeugen. Dies geschieht
mit
(Interface)
rmi.Naming.lookup( "rmi://host/method" )
- 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
- der RMI Server muß einen speziellen RMISecurityManager()
installieren
- greift bei RMI-Anfragen auf eine Datei mit der Spezifikation der
Sicherheitsrestriktionen zurück
- und entscheidet ob ein bestimmter Client die Methode aufrufen
darf oder nicht
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";
};
- Java-Hilfsmittel zur Programmierung der Kommunikation
zwischen Prozessen
- Socket-Kommunikation und ChannelFactory
- Remote Method Invocation (RMI)
- meist in Client-Server-Schema gepresst
© Universität Mannheim, Rechenzentrum, 1999.