© APSIS GmbH extern.gif (1249 Byte), Polling, 2008


Kapitel 12. Prozesse

Prozesse

Nebenläufige Prozesse können nebeneinander dargestellt werden:

Prozess 1 Prozess 2

Abb.: Nebenläufige Prozesse

Solange die Prozesse wirklich unabhängig voneinander ablaufen, tauchen keine Probleme auf. Wenn sie aber auf dieselben Betriebsmittel zugreifen, können Konflikte entstehen. Sie müssen untereinander kommunizieren und eine Entscheidung treffen, wer wann die gemeinsam benötigten Betriebsmittel in Anspruch nehmen kann: Sie müssen synchronisiert werden.

Ein Beispiel sei hierfür der Straßenverkehr. Die Fahrzeuge entsprechen den parallelen Prozessen; Betriebsmittel ist die Straßenfläche. Die Prozesse laufen unabhängig voneinander, solange sie nicht an einer Kreuzung angekommen sind. Hier kann ein Konflikt entstehen, wenn mehrere Prozesse dieses Betriebsmittel gleichzeitig in Anspruch nehmen möchten: Wer hat Vorfahrt? Wer kann die Kreuzungsfläche als Betriebsmittel zuerst in Anspruch nehmen? Die Synchronisierung wird mit Hilfe einer Regel gelöst: rechts vor links. Dabei kann es allerdings zu einer Systemverklemmung (deadlock) kommen, wenn vier Fahrzeuge gleichzeitig an der Kreuzung ankommen.

Disjunkte und voneinander abhängige Prozesse

Solange die Prozesse keine gemeinsamen Betriebsmittel in Anspruch nehmen, sprechen wir von disjunkten und voneinander unabhängigen Prozessen. Beispielsweise kann das sequenzielle Programmstück

x1 = max(a1, b1); // vier Werte
y1 = max (c1, d1);
z1 = x1 + y1;
x2 = max(a2, b2); // vier andere Werte
y2 = max(c2, d2);
z2 = x2 - y2;

in zwei parallele Prozesse aufgeteilt werden:

x1 = max(a1, b1);
x2 = max(a2, b2);
y1 = max(c1, d1);
y2 = max(c2, d2);
z1 = x1 + y1;
z2 = x2 - y2;

Abb.: Disjunkte Prozesse

Im Besitz zweier Prozessoren läuft diese Lösung doppelt so schnell als die sequenzielle Lösung ab. Die beiden Prozesse sind disjunkt, da sie keine gemeinsamen Daten haben: Der erste Prozess verarbeitet a1, b1, c1 und d1; die Ergebnisse werden in x1, y1 und z1 geschrieben. Der zweite Prozess verarbeitet a2, b2, c2 und d2; die Ergebnisse werden in x2, y2 und z2 geschrieben.

Ein ähnliches Programmstück kann aber auch dann konfliktfrei ablaufen, wenn die Eingangsdaten gemeinsam sind:

x1 = max (a1, b1);
x2 = max (a1, b1);
y1 = max (c1, d1);
y2 = max (c1, d1);
z1 = x1 + y1;
z2 = x2 - y2;

Abb.: Unabhängige Prozesse

Die gemeinsamen Objekte a1, b1, c1 und d1 werden nur gelesen. Die zu beschreibenden Daten x1, y1 und z1 bzw. x2, y2 und z2 werden von jeweils nur einem Prozess angefasst. Die Prozesse sind hier zwar nicht disjunkt, aber voneinander unabhängig.

Konflikte entstehen, wenn gemeinsame Daten beschrieben werden:

a = a * a;
a = a / 2;
meldung(a);
meldung(a);

Abb.: Abhängige Prozesse

Wir sprechen hier von voneinander abhängigen Prozessen. Das Ergebnis hängt von der Ausführungsreihenfolge ab und ist somit nicht determiniert. Die beiden Prozesse müssen also synchronisiert werden. Ein weiteres Beispiel ist das Zählen:

Beobachter
Berichterstatter
while (true)
while (true) {
   if (ereignis)
   meldung(zaehler);
      zaehler ++;
   zaehler = 0; }

Abb.: Synchronisierung

Hier hängt die Liste der ausgegebenen Werte ebenfalls von der Ausführungsreihenfolge der beiden Prozesse ab.

Kritische Abschnitte und gegenseitiger Ausschluss

Die veränderlichen Daten in den obigen Beispielen heißen kritische Betriebsmittel; sie sind nicht gemeinsam benutzbar. Die nur lesbaren Daten sind gemeinsam benutzbare Betriebsmittel. Wir nennen den Abschnitt eines Prozesses unkritisch, in dem kein Zugriff auf kritische Betriebsmittel erfolgt. In einem kritischen Abschnitt erfolgt dementsprechend Zugriff auf kritische Betriebsmittel.

Ein kritischer Abschnitt muss unter gegenseitigem Ausschluss ausgeführt werden. Dies bedeutet, dass ein Prozess exklusiv und ununterbrochen abläuft; der andere Prozess muss von Anfang bis Ende des kritischen Abschnitts ruhen. Dies wird durch Synchronisierung sichergestellt.

Synchronisierung

Das einfachste Werkzeug für Synchronisierung ist eine gemeinsam benutzte Boolesche Variable:

boolean s = true;
while (true)
while (true)
   if (s) {
   if (!s) {
      kritischerAbschnitt1();
      kritischerAbschnitt2();
      s = false;
      s = true;
      unkritischerAbschnitt1(); }
      unkritischerAbschnitt2(); }

Abb.: Synchronisierung über eine Boolesche Variable

Die beiden obigen Prozesse können ihre kritischen Abschnitte nur abwechselnd ausführen. Bei dieser einfachen Synchronisierung ist allerdings eine gegenseitige Blockade (Systemverklemmung) möglich, wenn ein Prozess in seinem kritischen Abschnitt auf weitere Betriebsmittel wartet, die von anderen Pro­zessen belegt sind. Darüber hinaus ist das aktive Warten (busy waiting) unwirtschaftlich. Deswegen sind mächtigere Operationen für Synchronisierung notwendig.

Die beiden bekanntesten Mechanismen für die Synchronisierung nebenläufiger Prozesse sind Semaphore und Monitore.

Semaphore

Ein Semaphor ist eine Klasse mit zwei (ununterbrechbaren) Operationen (traditionell p und v genannt), mit deren Hilfe der Eintritt in kritische Abschnitte synchronisiert werden kann. Seine Implementierung kann man sich folgendermaßen vorstellen:

public class Semaphor {
   private int wert;
   public Semaphor(int wert) { // Anzahl der anfänglich zugelassener Prozesse, oft 1
      this.wert = wert; }
   public void p() {
      if (wert > 0)
          wert --;
      else { // wert == 0
                sichInDieWarteschlangeEinreihen(); } } // Prozess wird gestoppt
   public void v() {
      wert ++;
      if (!warteschlangeLeer())
                warteschlangeVerlassen(); } } // Prozess läuft weiter

Von entscheidender Wichtigkeit ist die Eigenschaft der Semaphormethoden, dass die Ausführung eines p- oder v-Aufrufs „ununterbrochen“ durchläuft: Nachdem sie angefangen hat, garantiert der nebenläufige Prozess, keine p- oder v-Aufrufe für denselben Semaphor auszuführen; gegebenenfalls muss er auf die Beendigung des laufenden Aufrufs warten. Zusätzlich muss er bei einem p-Aufruf warten, wenn der Semaphor mit p besetzt und noch nicht mit v freigegeben wurde.

Ein Beispiel für die Anwendung eines Semaphors ist die obige Synchronisierungsaufgabe:

Semaphor s = new Semaphor(1);
while (true) {
while (true) {
   s.p();
   s.p();
   kritischerAbschnitt1();
   kritischerAbschnitt2();
   s.v();
   s.v();
   unkritischerAbschnitt1(); }
   unkritischerAbschnitt2(); }

Abb.: Semaphor

Der Semaphor muss immer mit einem int-Wert vorbesetzt werden: Dies ist die Anzahl der zugelassenen Prozesse im kriti­schen Abschnitt, meistens 1.

Das Standardbeispiel für die Verwendung von Semaphoren ist das Problem Er­zeu­ger-Verbraucher  oder Sender-Empfänger . Der Erzeuger produziert (sendet) etwas, z.B. Nachrichten. Diese werden in einem Puffer (in einem Multibehälter) zwischengelagert, bis der Empfänger sie abholt. Der Puffer ist ein kritisches Betriebsmittel. Wir verwenden den Semaphor zur Sicherung der Zugriffe im kritischen Abschnitt:

Semaphor pufferZugriff = new Semaphor(1);
while (true) {
while (true) {
   erzeugeNachricht();
   pufferZugriff.p();
   pufferZugriff.p();
   nachrichtAusDemPuffer();
   nachrichtInDenPuffer();
   pufferZugriff.v();
   pufferZugriff.v(); }
   verarbeiteNachricht(); }

Abb.: Geschützter Puffer

Außer dem gegenseitigen Ausschluss beim Zugriff auf puffer gibt es in diesem Beispiel noch einen weiteren Synchronisierungsbedarf: Der Verbraucher muss warten, bis der Erzeuger etwas in den Puffer gelegt hat. Hierzu benutzen wir einen zweiten Semaphor pufferLeer:

Semaphor pufferLeer = new Semaphor(0), pufferZugriff = new Semaphor(1);
while (true) {
while (true) {
   erzeugeNachricht();
   pufferLeer.p();
   pufferZugriff.p();
   pufferZugriff.p();
   nachrichtInDenPuffer();
   nachrichtAusDemPuffer();
   pufferZugriff.v();
   pufferZugriff.v();
   pufferLeer.v(); }
   verarbeiteNachricht(); }

Abb.: Wartebedingung

Der Vorbesetzungswert des Semaphors pufferLeer ist hier 0; erst nachdem eine Nachricht erzeugt wurde, wird er erhöht, damit sie verarbeitet werden kann. Die obigen Semaphore haben einen Wertebereich 0 bis 1. Sie heißen binäre Semaphore. Allgemein können Semaphore einen beliebigen nichtnegativen Wertebereich 0 bis n haben.

Nachrichtenaustausch

Das obige Beispiel für das Problem Sender-Empfänger setzt voraus, dass im Puffer nur eine Nachricht gelagert werden kann. Wir betrachten nun dieselbe Aufgabe mit einer Pufferlänge GROESSE. Die folgende Lösung basiert auf der Technik des Ringpuffers , den wir aus der Übung 10.6 kennen:

Nachricht[] puffer = new Nachricht[groesse];
int juengstes = 0, aeltestes = groesse-1;
int zaehler = 0;
while (true) {
while (true) {
   if (zaehler <= groesse) {
   if (zaehler != 0) {
      Nachricht nachricht = new Nachricht();
      Nachricht nachricht;
      nachricht.erzeugen();
      nachricht = puffer[aeltestes];
      puffer[juengstes] = nachricht;
      aeltestes = (aeltestes+1) % groesse;
      juengstes = (juengstes+1) % groesse;
      zaehler --;
      zaehler ++; } }
      nachricht.verarbeiten(); } }

Abb.: Nachrichtenaustausch

Hier zählen die Variablen puffer und zaehler zu den kritischen Betriebsmitteln, da sie von beiden Prozessen beschrieben werden.

Prozesse können auf den Puffer und den Zähler auch ohne Sicherung durch Semaphore zugreifen und dadurch in Bedrängnis kommen. Der Semaphor ist ein Synchronisierungswerkzeug, zu dessen Verwendung niemand gezwungen wird. Nur wenn sich alle diszipliniert verhalten, ist ein konfliktfreier Ablauf gewährleistet. Im nächsten Abschnitt werden wir sehen, wie dies erzwungen werden kann.

Monitore

Das zweite klassische Werkzeug, der Monitor, löst das Problem der mangelnden Disziplin. Ein Monitor ist ein durch Semaphore geschütztes Objekt (das Konzept stammt aus der Sprache Simula-67). Er wird häufig mit den zwei Operationen wait und signal implementiert, die genau p und v des Monitors entsprechen. Er funktioniert, wie ein von innen verschließbarer Waschraum: Auf einen Monitor hat nur ein Prozess Zugriff. Er realisiert daher den gegenseitigen Ausschluss.

Das obige Beispiel Ringpuffer  kann also in einer Klasse versteckt werden. Ihr Objekt bildet einen Monitor: Der Puffer selbst ist private, die Zugriffsmethoden realisieren den gegenseitigen Ausschluss über den Zugriff durch den Semaphor zugriff. Außerdem sichern die Semaphore pufferLeer und pufferVoll, dass aus einem leeren Puffer nicht gelesen und in einen vollen Puffer nicht geschrieben wird:

public class Ringpuffer { 
   private Object[] puffer;
   private Semaphor zugriff, pufferLeer, pufferVoll; // binäre Semaphore
   private int juengstes, aeltestes;
   public Ringpuffer(int groesse) {
      puffer = new String[groesse];
      juengstes = puffer.length - 1;
      aeltestes = 0;
      zugriff = new Semaphor(1);
      pufferLeer = new Semaphor(groesse);
      pufferVoll = new Semaphor(groesse); }
   // jetzt können verschiedene Prozesse den Puffer beschreiben und lesen:
   public void eintragen(final Object nachricht) {
      pufferVoll.p(); // bei vollem Puffer warten
      zugriff.p(); // bei Zugriff durch andere warten
       puffer[juengstes] = nachricht; // kritischer Abschnitt
       juengstes = (juengstes+1) % puffer.length; // kritischer Abschnitt
      zugriff.v(); // kritischen Abschnitt freigeben
      pufferLeer.v(); } // Wartende erlösen
   public Object entnehmen() { // unsauber aber zweckmäßig: Mutator als Funktion
      pufferLeer.p(); // bei leerem Puffer warten
      zugriff.p(); // bei Zugriff durch andere warten
       Object nachricht = puffer[aeltestes]; // kritischer Abschnitt
       aeltestes = (aeltestes+1) % puffer.length; // kritischer Abschnitt
      zugriff.v(); // kritischen Abschnitt freigeben
      pufferVoll.v(); // Wartende erlösen
      return nachricht; } }

Java im Internet

In diesem Kapitel fassen wir zusammen, wie man mit Hilfe der Pakete java.net und java.rmi im Netz operieren kann, ohne sich um Systemdetails wie Protokolle und Ports kümmern zu müssen. Auf die Möglichkeiten zu systemnaher Kommunikation wird nur hingewiesen.

Sicherheitskonzepte

Arbeiten im Internet eröffnet viele neue Möglichkeiten und Lösungswege, birgt aber auch Gefahren. Deshalb umfasst das Java-Konzept auch Sicherheitskonzepte für die Ausführung vom Netz geladener Programme.

Für Applets, die häufigste und einfachste Netzanwendung, gelten relativ starke Beschränkungen, die durch Objekte der Klasse AppletSecurityManager überprüft werden. So hat ein Applet keine Möglichkeit, auf das lokale Dateisystem oder lokale Systemeigenschaften zuzugreifen. Es kann weder Prozesse außerhalb seiner eigenen ThreadGroup erzeugen oder bearbeiten noch Programme starten. Es kann keine Methoden in einer anderen Programmiersprache (native methods) aufrufen, deren Sicherheit nicht überprüfbar ist. Darüber hinaus kann es wichtige Systemklassen wie ClassLoader oder SecurityManager nicht überschreiben.

Für den Fernaufruf von Methoden (s. Kapitel 12.3.7.), die auf einem anderen Rechner laufen, muss sowohl auf dem Server als auch auf dem Client ein SecurityManager-Objekt erzeugt werden. Die Klasse RMISecurityManager bietet eine sehr restriktive Vorgabe dafür: Alles außer Klassendefinition und -zugriff ist verboten. Durch Erweiterung der Klasse SecurityManager kann der Benutzer eigene Sicherheitsregeln festlegen. Ist kein Security­Manager-Objekt vorhanden, so sind nur lokale Aufrufe möglich.

Das bedeutet, dass man innerhalb des Java-Konzepts vor böswilligem fremden Code recht gut geschützt ist. Wer allerdings die höheren Konzepte umgeht und z.B. über java.net.URL Binärcode lädt und als native-Methode ausführt, muss sich selbst vor bösen Überraschungen schützen.

Einbinden entfernter Applets in Netzseiten

Die einfachste Möglichkeit, Java-Programme über das Internet zu benutzen, ist das Einbinden eines beliebigen Applets in eine Netzseite. Dazu ist nichts weiter erforderlich, als die oben beschriebene Applet-Klausel um die Netzadresse des Applets (code­base) zu erweitern:

<APPLET CODEBASE="http://www.tfh-berlin.de/~oo-plug/Java/Hypertext/Klasse"
   CODE=MalenOptimal.class, WIDTH=200, HEIGHT=100> </APPLET>

Fehlt die Angabe CODEBASE, so wird hier standardmäßig die DocumentBase eingesetzt, d.h. die Adresse des HTML-Dokuments.

Bei der Verarbeitung dieser Applet-Klausel lädt der Interpreter (z.B. des Browsers) das Applet über das Netz. Mit Hilfe eines Überprüfers (byte code verifier) wird der Code des genannten Programms dann zunächst auf Zulässigkeit geprüft. Danach wird daraus ein .class-Objekt erzeugt und ihm ein eigener Namensraum zugeordnet, sodass es beim Laden gleichnamiger Applets nicht zu Konflikten kommen kann. Dann wird das Applet ausgeführt. Jede vom Applet referenzierte Klasse wird zunächst im lokalen CLASSPATH gesucht, danach im CLASSPATH der im Applet-Klausel angegebenen CODEBASE. Muss die Klasse über das Netz geladen werden, so wird damit erneut wie beschrieben verfahren.

Übung: Schreiben Sie für eines Ihrer Applets eine Applet-Klausel mit vollständiger Internet-Adresse, fügen Sie dies in eine (ggf. leere) HTML-Seite ein und versuchen Sie, das Applet mit Ihrem Internet-Browser auszuführen.

Adressieren entfernter Objekte

Das Standardpaket java.net enthält die Klassen für die Netzkommunika­ti­on, und zwar auf drei verschiedenen Abstraktionsebenen:

· Die Klasse java.net.URL repräsentiert einen Uniform Resource Locator, die im Internet übliche Form der Adresse. URL enthält Methoden, mit denen eine referenzierte Datei sehr einfach komplett oder als Datenstrom vom Netz geladen werden kann.

· java.net.Socket und java.net.ServerSocket ermöglichen den Anschluss an einen bestimm­ten Host und Port und den Datenaustausch über ein java.io.In­put/Out­put­Stream-Protokoll.

· java.net.DatagramSocket schließlich ist eine Möglichkeit, Bytes mit minimalem Protokoll (d.h. schnell und unabgesichert) über das Netz zu senden.

Wir betrachten hier ausschließlich die oberste Abstraktionsebene, die Klasse java.net. URL. Ein URL ist eine Internet-Adresse mit folgenden Bestandteilen:

Protokoll://Host[:Port]/Pfad/Dateiname[#interneReferenz]

Der Inhalt eckiger Klammern kann weggelassen werden. Zum Beispiel ist

http://www.tfh-berlin.de/~oo-plug/Java/Images/Javabuch.gif

die Adresse einer .gif-Datei. Entsprechend hat die Klasse URL verschiedene Kon­struk­toren, denen die Netzreferenz entweder als Ganzes oder als einzelne Bausteine als Parameter mitgegeben wird:

public URL (String url) throws MalformedURLException;
public URL(String protocol, String host, String file) throws MalformedURLException;
public URL(String protocol, String host, int port, String file) throws MalformedURLException;

d.h. für unsere Beispielreferenz lassen sich URL-Objekte durch folgende Konstruktor­aufrufe erzeugen:

try {
   URL u1 = new URL("http://tfh-berlin.de/~oo-plug/Java/Images/Javabuch.gif");
   URL u2 = new URL("http", "tfh-berlin.de", "/~oo-plug/Java/Images/Javabuch.gif");
      // '/' vor Dateinamen erforderlich
   URL u3 = new URL("http", "tfh-berlin.de",80, "/~oo-plug/Java/Images/Javabuch.gif"); }
catch (MalformedURLException ausnahme) { System.err.println(ausnahme); }

Die Ausnahme MalformedURLException tritt auf, wenn der Aufbau des URL syntaktisch falsch ist, oder auch, wenn das angegebene Protokoll vom Java-System nicht unterstützt wird (z.B. auf vielen Systemen ftp, d.h. file transfer protocol).

Die Bestandteile eines URL lassen sich mit den Methoden getProtocol, getHost, getPort, getFile und getRef erfragen: Sie liefern je ein String-Objekt. Die URL-Methode sameFile überprüft, ob verschiedene URL-Objekte dieselbe Netzreferenz darstellen (nicht aber, ob zwei verschiedene Netzreferenzen auf dieselbe Datei verweisen). In unserem Fall müssten alle Vergleiche true ergeben:

(u1.sameFile(u2) && u2.sameFile(u3)) == true

Besonders nützlich ist die Möglichkeit, ein URL relativ zu einem anderen zu erzeugen:

public URL(URL url, String file) throws MalformedURLException;

Dieser Konstruktor nimmt den ersten Parameter ohne den Dateinamen und hängt den zweiten Parameter als Dateinamen daran.

URL u4 = new URL(u1, "Javabuch.gif");
URL u5 = new URL(u1, "../oo-plug/Ooplug.gif");

In den meisten Fällen wird man als ersten Parameter nicht direkt ein URL-Objekt angeben, sondern einen Methodenaufruf wie getCodeBase oder getDocumentBase der Klasse Applet:

URL u6 = new URL(getDocumentBase(), "index.html");

Laden entfernter Textobjekte

Mit Hilfe eines URL-Objekts können Daten direkt vom Netz geladen werden. Die Klasse URL definiert zu diesem Zweck drei Methoden:

public final InputStream openStream() throws IOException;
public URLConnection openConnection() throws IOException;
public final Object getContent() throws IOException;

Die Methode openStream stellt die Verbindung zur URL-Referenz her, erledigt die erforderliche Verbindungsaufnahme (handshake) zwischen dem Kunden, d.h. Benutzer (client) und dem Bediener (server) und liefert ein java.io.InputStream-Objekt, das wie üblich gelesen werden kann:

final static String adresse = "http://www.tfh-berlin.de/~oo-plug/Java/index.html";
InputStream strom;
try {
   strom = new URL(adresse).openStream(); }
catch (Exception ausnahme) { System.err.println(ausnahme); }

Aus dem strom können nun die uninterpretierten Daten der eröffneten Datei gelesen werden – allerdings ohne vorangestellte Protokoll-Steuerinformationen (headers). Mit folgendem Programm lässt sich eine beliebige ASCII-Datei vom Netz lesen und ausdrucken (die Beschränkung auf ASCII ist durch BufferedInputStream ergründet):

import java.net.*; // URL, Ausnahmen
import java.io.*; // BufferedInputStream, Ausnahmen
public class TextLaden {
   public static void main(String args[]) { // URL wird als Aufrufparameter erwartet
      URL url;
      String aktuelleZeile;
      BufferedReader lesestrom;
      System.out.println("Datei " + args[0] + ": ");
      try {
          url = new URL(args[0]); // throws IndexOutOfBounds, MalformedURL
           lesestrom = new BufferedReader(new InputStreamReader(url.openStream()));
          aktuelleZeile = lesestrom.readLine(); // throws IOException
          while (aktuelleZeile != null) { // readLine liefert null am Dateiende
             System.out.println(aktuelleZeile);
             aktuelleZeile = lesestrom.readLine(); } } // throws IOException
      catch (IndexOutOfBoundsException ausnahme) {
          System.out.println("Benutzung: TextLaden URL"); }
      catch (MalformedURLException ausnahme) {
          System.out.println("Falsches URL: " + args[0]); }
      catch (Exception ausnahme) {
          System.out.println(ausnahme); } } }

Übung: Probieren Sie das Programm TextLaden mit der Leitseite der TFH Berlin aus (allerdings vielleicht mit einer Zählschleife, die die Zahl der ausgegebenen Zeilen beschränkt!):

java TextLaden http://www.tfh-berlin.de

oder mit sich selbst:

java TextLaden file:/[Pfad]/TextLaden.java

Laden entfernter Klassenobjekte

Mit den Unterklassen von InputStream gibt es eine Reihe von Möglichkeiten, die byteweise gelesenen Daten als primitive Werte zu interpretieren oder mittels ObjectInput­Stream auch als serialisierte Objekte:

try {
   URL url = new URL("http://www.tfh-berlin.de");
   ObjectInputStream objekt = new ObjectInputStream(url.openStream()); }
catch (Exception ausnahme) { System.err.println(ausnahme); }

Für Bilder funktioniert dieses Verfahren allerdings nicht. Einerseits ist die Klasse Image nicht serialisierbar, und andererseits liegen die interessanten Dateien im Netz zumeist nicht als Java-Objekte, sondern als .gif oder .jpeg-Dateien vor. Der HTTP-Server im Netz stellt zu jeder solchen Datei den MIME-Typ (Multipurpose Internet Mail Ex­tension) zur Verfügung. Anhand dieses Typs kann die URL-Methode getContent entscheiden, ob sie ein InputStream-Objekt oder ein ImageProducer-Objekt erstellen soll. Aus dem letzteren kann die .gif oder .jpeg-Datei in ein Image-Objekt überführt werden. MIME stellt eine weit verbreitete grobe Typisierung von Dateiinhalten nach Typ und Untertyp dar: Die Texttypen text/html und text/plain und die Bildtypen image/gif und image/jpeg können allgemein als bekannt vorausgesetzt werden.

Welche Art von Objekt die Methode getContent geliefert hat, kann an der Aufrufstelle mit dem instanceOf-Operator geprüft werden. Für MIME-Typen, die vom ladenden System nicht unterstützt werden (d.h. dass ContentHandler-Klassen für sie nicht definiert sind), wird die Ausnahme ClassNotFoundException ausgeworfen:

import java.net.*; // URL
import java.io.*; // DataInputStream
public class ObjektLaden {
   public static void main(String args[]) {
      URL url;
      Object objekt; // je nach MIME-Typ für InputStream oder ImageProducer-Objekt
      if (args.length > 0) { // URL wird als Aufrufparameter erwartet
          try {
             url = new URL(args[0]); // throws MalformedUrlException
             obj = url.getContent();// throws ClassNotFoundException
             System.out.println("Es wurde ein " + obj.getClass().getName()
                + "-Objekt erzeugt"); }
          catch (Exception ausnahme) { System.err.println(ausnahme); } } } }

Die Methode openConnection liefert ein URLConnection-Objekt, das seinerseits die Methode getContent zur Verfügung stellt, allerdings zusätzlich eine Reihe von Steuermöglichkeiten über die Netzverbindung ermöglicht. Die eigentliche Verbindung wird durch die Methode connect hergestellt; danach ist der Zugriff auf die Daten mittels get­Content möglich. Zusätzlich besteht Zugriff auf die HTTP-Kopfzeilen durch Methoden wie getHeaderField oder allgemeine Informations­me­tho­den wie getContentType, getContentLength und getExpiration. Vor dem connect-Aufruf bietet URLConnection außerdem die Möglichkeiten, bestimmte Eigenschaften der Verbindung festzulegen und auch abzufragen, z.B. ob während des Datentransfers eine Interaktion mit dem Benutzer stattfinden soll (z.B. zur Passwortabfrage mit setAllowUserInteraction) oder ob die Datei nur geladen werden soll, wenn sie neu genug ist (mit setIfModifiedSince). Wer hier weiterarbeiten möchte, sei besonders auch auf die Unterklasse HttpURLConnection hingewiesen, die für die Arbeit mit dem HTTP-Protokoll noch etliche weitere Möglichkeiten bietet.

Übung: Probieren Sie, ob HTML-Dateien auf ihrem System unterstützt werden: Rufen Sie das Programm (12.11) mit der Kommandozeile

java ObjektLaden url

auf und geben Sie als Parameter url Adressen von verschiedenen Dateitypen an. Mit der Weiterentwicklung von Java-Systemen sollten die ClassNotFoundException-Enttäu­schun­gen seltener werden.

Fernaufruf von Methoden

Das Paket java.rmi realisiert das Java-Konzept der verteilten Programmierung. Methodenaufrufe über das Netz unterscheiden sich dabei sprachlich nicht von lokalen Aufrufen; die entsprechenden Klassen leisten die Kommunikationsarbeit „unter der Oberfläche“. Der Benutzer merkt den Unterschied nur dadurch, dass Fernaufrufe sehr langsam sind. Aber die Geschichte der Informatik hat gezeigt, dass „saubere“, ohne Rücksicht auf Effizienz entwickelte Konzepte ihre Effizienz immer im Nachhinein durch die Hard- und Softwareentwicklung erhalten haben.

Das RMI-Konzept besteht aus der Benutzersicht aus drei Schritten: Der Fernanbieter oder Server stellt in einer Fernschnittstelle (remote interface) zusammen, welche Methoden er über das Netz zur Verfügung stellen will, und trägt das Fernobjekt, dessen Klasse die Fernschnittstelle implementiert, in ein Register ein, in dem ein möglicher Fernbenutzer (client ) danach suchen kann. Der Benutzer stellt die Verbindung zum Server-Rechner her und holt sich mit Hilfe des Registers ein Fernobjekt, das den Typ der Schnittstelle hat und wie ein lokales Objekt benutzt werden kann.

Aus Systemsicht kommen noch zwei Schritte hinzu, die der Programmierer des Servers unterstützen muss: Während es logisch so aussieht, als würden zwei Java-Objekte direkt miteinander kommunizieren, findet der eigentliche Datenaustausch auf einer viel tieferen Schicht statt. Die Datenübermittlung zwischen dem Java-Objekt und der Datentransportschicht leisten zwei spezielle Klassen, die mit Hilfe des Werkzeugs rmic (RMI-Compiler) aus dem übersetzten Fernobjekt erzeugt werden können: Auf der Serverseite ist dies das Skelett -Objekt (skeleton object), auf der Client-Seite das Stumpf-Objekt (stub object), das der Benutzer beim Registeraufruf erhält, d.h. über das Netz lädt.

Wir wollen diese Schritte nun anhand eines einfachen Beispiels untersuchen. Die klassische Anwendung verteilter Programmierung sind Datenbanken; wir realisieren hier eine äußerst primitive Form davon, nämlich das Mitgliederverzeichnis eines Klubs.

Auf der Serverseite definieren wir zunächst die Fernschnittstelle, die eine Erweiterung der Schnittstelle java.rmi.Remote sein muss und die fernaufrufbaren Methoden unserer Klasse spezifiziert. Dabei muss jede fernaufrufbare Methode java.rmi.RemoteException als mögliche Ausnahme spezifizieren, da grundsätzlich mit Kommunikationsproblemen im Netz zu rechnen ist:

import java.rmi.*; 
public interface KlubListe extends Remote { // Fernschnittstelle
   public String name(int mitgliedsnummer) throws NrUngueltigException, RemoteException;
   public int mitgliedsnummer(String name) throws RemoteException;
   public void eintragen(String name) throws RemoteException;
   public void loeschen(String name, int mitgliedsnummer) throws RemoteException; }

Die für die Implementierung der Schnittstelle erforderlichen Klassen befinden sich im Paket java.rmi.server: RemoteObject entspricht der Klasse Object für fernaufrufbare Objekte und exportiert die Standardmethoden equals, clone, toString und hashCode. RemoteServer erweitert RemoteObject und ist die abstrakte Oberklasse für alle Server-Implementierungen. Zurzeit steht nur UnicastRemoteServer als Implementierung zur Verfügung, sodass alle Implementierungen diese erweitern (sollten). Die besondere Bedeutung dieser Klasse liegt in ihren privaten Methoden, die unsichtbar die Kommunikation verwalten, wie etwa das Senden und Empfangen der Parameterdaten. Sie exportiert Methoden zur Identifikation des jeweiligen Fernbenutzers (getClientHost und getClientPort) und zum Anlegen einer Log-Datei (setLog).

import java.rmi.*;
import java.rmi.server.*;
import java.net.*;
public class KlubListeImpl extends UnicastRemoteObject implements KlubListe {
   private String[] liste;
   public KlubListeImpl(int laenge) throws RemoteException {
      super(); // throws RemoteException // erforderlich, um Ausnahme auszulösen
      liste = new String[laenge]; }
   public String name(int nummer) throws Exception {
      try {
          return liste[nummer]; }
      catch (IndexOutOfBoundsException ausnahme) {
          throw new Exception("Nummer ungültig"); } }
   … // die weiteren Methoden aus der Schnittstelle ähnlich

Wie man sieht, unterscheidet sich die Implementierung der fernaufrufbaren Methoden überhaupt nicht von der lokal verfügbarer – alle technischen Details sind in der Klasse UnicastRemoteObject verborgen. Nun ist noch die main-Methode zu schreiben, durch die der Serverprozess gestartet wird. Hier sind zwei Aufgaben zu erfüllen: Zunächst muss (da main eine Klassenmethode ist, also ohne Objekt aufgerufen wird) explizit ein Objekt der Serverklasse erzeugt werden

KlubListeImpl klub = new KlubListeImpl(20);

und in das Register Naming (des Server-Hosts) eingetragen werden. Na­ming bietet fünf Klassenmethoden an:

public static void bind(String url, Remote r) throws RemoteException, AccessException,
   AlreadyBoundException, UnknownHostException;
public static void rebind(String url, Remote r) throws RemoteException, AccessException,
   UnknownHostException;
public static void unbind(String url, Remote r) throws RemoteException, AccessException,
   UnknownHostException, NotBoundException;
public static Remote lookup(String url) throws RemoteException, AccessException,
   NotBoundException, UnknownHostException;
public static String[] list(String url) throws RemoteException, AccessException,
   UnknownHostException;

Die ersten drei Methoden werden vom Server benutzt, um Remote-Objekte bekannt zu machen bzw. zu entfernen, die letzten beiden Methoden benutzen die Fernbenutzer, um Remote-Objekte zu finden. url bezeichnet dabei eine URL-artige Adresse, unter der die Fernbenutzer das Remote-Objekt erreichen können, allerdings mit dem Protokoll rmi und einem (vom Server) frei gewählten Namen als Dateieintrag an Stelle eines Dateinamens. In dieser Adresse können die Einträge Protokoll und Host auf der Server-Seite auch weggelassen werden.

Für unser Beispiel schreiben wir auf der Server-Seite:

final static String serverHost = "nts02.tfh-berlin.de"; // oder eine andere Adresse
Naming.rebind("rmi://" + ServerHost + "/VereinsRegister", klub);
Naming.rebind("VereinsRegister", klub); // gleichwertig

und auf der Fernbenutzer-Seite (mit expliziter Konvertierung zur benötigten Klasse):

KlubListe c = (KlubListe)Naming.lookup("rmi://" + ServerHost + "/VereinsRegister");

Nur diejenigen Remote-Objekte müssen in das Register eingetragen werden, die direkt von einem Fernbenutzer gesucht werden. Kein Eintrag ist erforderlich für Re­mote-Objekte, die dem Fernbenutzer als Ergebnis eines Methodenaufrufs zugänglich gemacht werden. Naming wird aus diesem Grund auch als Bootstrap-Register bezeichnet, da es den ersten Zugang zu Remote-Objekten ermöglicht.

Zweitens muss ein SecurityManager installiert werden, weil sonst keinerlei Fernaufrufe erlaubt sind. Die Klasse RMISecurityManager bietet ein fertiges, restriktives Sicherheitskonzept; eigene Erweiterungen der Klasse SecurityManager sind möglich.

System.setSecurityManager(new RMISecurityManager());

Damit haben wir alle Bestandteile zusammen, um die main-Methode zu schreiben und sie in das Programm (12.13) einzufügen:

public static void main(String args[]) throws Exception {
   System.setSecurityManager(new RMISecurityManager());
   KlubListeImpl klub = new KlubListeImpl(20); // throws RemoteException
   Naming .rebind("VereinsRegister", klub); // throws RemoteException,
      // AccessException, UnknownHostException, MalformedURLException
   System.out.println("Klub-Server ist bereit."); }

Das Fernbenutzer-Programm, das dieses Klubverzeichnis von einem anderen Rechner aus ansprechen möchte, muss den Fernaufruf nur in zwei Schritten vorbereiten und kann dann die Methoden wie lokale Methoden verwenden: Erstens muss auch bei ihm ein SecurityManager installiert werden; zweitens muss das Objekt der Klasse Klub­Liste über das Naming-Register „geholt“ werden (tatsächlich wird ein Stumpf-Objekt geladen, s.o.). KlubListe ist dabei keine Klasse, sondern eine Schnittstelle; es kann davon kein Objekt erzeugt, sondern nur vom Server geholt werden:

import java.rmi.*;
public class KlubAussenstelle {
   final static String serverHost = "nts02.tfh-berlin.de"; // oder eine andere Adresse
   public static void main(String args[]) throws Exception {
      System.setSecurityManager(new RMISecurityManager());
      KlubListe liste = (KlubListe)Naming.lookup("rmi://" + serverHost +
          "/VereinsRegister");
      liste.eintragen("Otto Meierlein"); // eintragen in die entfernte Liste
      int nummer = liste.mitgliedsnummer("Otto Meierlein"); // erhaltene Nummer holen
      String eintrag = liste.name(nummer); // entfernten Eintrag lesen
      System.out.println("Ferneintrag: " + eintrag + " M. Nr.: " + nummer); } }

Auf dem Server-Rechner müssen nun noch zwei Aufgaben erledigt werden, ehe Server und Fernbenutzer (durch den Interpreteraufruf java) gestartet werden und dann mit­ein­ander kommunizieren: Es müssen das Skelett- und das Stumpf-Objekt des Servers erzeugt werden. Hierzu dient das sdk-Programm rmic, dem der Name der übersetzten Server-Klasse mitgegeben wird:

rmic KlubListeImpl

Dieser Aufruf erzeugt die beiden Dateien KlubListeImpl_Skel.class und Klub­Liste­Impl_Stub.class. Dann muss noch das Register gestartet werden. Dies geschieht unter DOS mit der Kommandozeile

start rmiregistry

unter UNIX mit dem Befehl rmiregistry. Nun rufen wir auf dem Server

java KlubListeImpl

auf und auf dem entfernten Rechner

java KlubAussenstelle

und hoffen auf eine gute Verbindung.

Abschließend müssen wir uns noch kurz mit der Parameterübergabe bei Fernaufrufen beschäftigen (s. Kapitel 7.10.). In unserem Beispiel haben wir uns auf einfache Parametertypen beschränkt, nämlich int und String, und angenommen, dass alles genauso ist wie bei lokalen Aufrufen. Zwei Fragen müssen geklärt werden: Werden Parameter bei Fernaufrufen per Referenz übergeben (wie bei loka­len Aufrufen) oder kopiert, und welche Parametertypen sind möglich?

Für die Übergabe gilt: Auf demselben Rechner werden Referenzen übergeben, von Rechner zu Rechner Kopien der Objekte (d.h. Wertübergabe). Das bedeutet natürlich, dass Seiteneffekte auf als Parameter übergebene Objekte bei Fernaufrufen nicht möglich sind: Veränderungen an Objekten sind nach dem Aufruf nicht wahrnehmbar.

Die Umwandlung von Java-Objekten in übertragbare Byteströme benutzt die Serialisierung des Paketes java.io (vgl. ObjectInputStream und ObjectOutputStream im Kapitel 11.1.). Als Parameter von Fernaufrufen sind deshalb außer den primitiven Typen alle serialisierbaren Objekte möglich, d.h. Objekte, deren Klasse durch Implementierung der (leeren, nur zu Markierungszwecken dienenden) Schnittstelle Se­ri­a­lizable gekennzeichnet sind.

Java im Internet

Die Nebenläufigkeit – „quasi-Parallelität“ – von Prozessen, die im selben Rechner abgearbeitet werden, stellt den Programmierer schon vor komplexen Synchronisierungsproblemen. Die echte Parallelität, wenn die Prozesse von verschiedenen Prozessoren abgearbeitet werden, liegt auf einer noch höheren Komplexitätsebene dar. Java – in ihrer Eigenart als Internet-Sprache – bietet hier musterhafte Lösungen an.

Applets

Fast jeder, der von der Sprache Java gehört hat, verbindet damit den Begriff Applet. Applet ist ein englisches Kunstwort, das „kleine Anwendung“ suggeriert. Es steht für (zumeist kleinere) Java-Programme, die direkt in Internet-Seiten eingebunden und von Browser ) ausgeführt werden können.

Applets sind Erweiterungen der Standardklassen java.applet.Applet  oder javax.swing.JApplet . Sie können von einem Applet-Interpreter , z.B. dem appletviewer der Firma Sun, ausgeführt werden. Von Hauptprogrammen unterscheiden sich Applets in drei wesentlichen Punkten:

· Der Applet-Interpreter führt nicht eine main-Methode aus, in der der Programmierer alle Schritte des Programmablaufs nacheinander festgelegt hat. Vielmehr erzeugt er ein Objekt der Applet-Klasse und ruft dessen Standardmethoden zu geeigneten Zeitpunkten auf. Ein Applet wird also durch Überschreiben der Standardmethoden der Klasse Applet programmiert; dies sind

public void init ();
public void start ();
public void stop ();
public void paint (Graphics g); // geerbt von java.awt.Panel

· Damit Applets auf Netzseiten sichtbar werden, ist für sie eine grafische Ausgabe in Form eines (ungerahmten) Fensters vordefiniert, und zwar dadurch, dass JApplet und Applet die Klasse java.awt.Panel erweitern. Damit sind alle wichtigen Panel-Methoden in einem Applet direkt (mit this) aufrufbar (weil sie geerbt wurden).

· Der Aufrufparameter eines Applet-Interpreters ist nicht der Name der übersetzten Klasse, sondern der Name einer Datei, die eine so genannte Applet-Klausel (applet tag) für diese Klasse enthält. Im Wesentlichen ist dies ein Verweis auf die entsprechende .class-Datei in der Sprache HTML (Hyper Text Markup Language), die von Browsern verstanden wird. Für ein Applet namens HalloWelt erzeugt man eine HTML-Datei mit folgender Applet-Klausel:

<APPLET CODE="HalloWelt.class" WIDTH=450 HEIGHT=100 > </APPLET>

wobei WIDTH und HEIGHT die Größe des Applet-Fensters bezeichnen und frei wählbar sind.

Wenn der Name dieser Datei z.B. Klasse.html ist, dann führt die Kommandozeile

appletviewer Klasse.html

die Klasse HalloWelt aus. Der Dateiname und die Dateinamenergänzung spielen hierbei keine Rolle, nur der Dateiinhalt.

Abb.: Interpretieren mit appletviewer

Wenn die Lehrbuch-Bibliothek nicht als Paket (mit .class-Dateien im Unterverzeich­nis), sondern in der Datei lehrbuch.jar vorliegt, muss die Applet-Klausel auch einen Verweis hierauf enthalten:

<APPLET CODE="HalloWelt.class" ARCHIVE="lehrbuch.jar" WIDTH=450 HEIGHT=100>
</APPLET>

Wenn die auszuführende Klasse sich in einem Paket befindet, muss appletviewer aus dem Verzeichnis heraus aufgerufen werden, in dem sich das Paket befindet und die APPLET-Klausel den Pfad der Klasse entweder „im Unix-Stil“ mit / oder „im Java-Stil“ mit . (nicht aber im „DOS-Stil“ mit \) angeben:

<APPLET CODE="k2/p5/HalloWelt.class" ARCHIVE="lehrbuch.jar"
WIDTH=450 HEIGHT=100> </APPLET>

oder:

<APPLET CODE="k2.p5.HalloWelt.class" ARCHIVE="lehrbuch.jar"
WIDTH=450 HEIGHT=100> </APPLET>

Hierarchie der Algorithmen

Algorithmen können in folgende Hierarchie eingeordnet werden:


© APSIS GmbH extern.gif (1249 Byte), Polling, 2008