oberon index <- ^ -> mail ?
Weiter: Resümee, davor: Erzeugung von Knoten in fremdem Auftrag, darüber: Verteilte Anwendungen.

Verteilte Briefkasten-Objekte

Das Modul PersonalMessages

Im folgenden geht es um den begrifflichen Kern des Mitteilungs-Systems, das Modul PersonalMessages, welches die Sicht der Anwendungsprogramme auf das System realisiert. Es führt als Abstraktion drei Objektklassen ein:

Dieses Codefragment stellt diese Typen in der Form dar, wie sie von anderen Modulen gesehen werden. MessageBoxRec und MessageRec haben keine öffentlichen Komponenten. Ihre Struktur bleibt in der Implementierung verborgen. Bezüge auf Namensräume und ConstStrings werden sogar im ganzen Definitionsteil vermieden, sodaß Anwendungen dieses Moduls nur mit gewöhnlichen Zeichenketten umzugehen brauchen.

Intern handelt es sich beim Versenden von Mitteilungen um nichts anderes als das Einfügen von Kanten in Knoten, die den Briefkasten des jeweiligen Empfängers repräsentieren. Die Kante selbst wird aus einem Präfix, welches Datum und Uhrzeit der Erzeugung sowie den Namen des Absenders enthält, und der eigentlichen Mitteilung gebildet. Durch das Präfix wird bei alphabetisch sortierten Kanten nebenbei die chronologische Reihenfolge beim Abholen der Mitteilungen sichergestellt. Bevor wir exemplarisch auf einige Einzelheiten der Implementierung eingehen, wollen wir die Definitionen der Methoden betrachten.

Dieses Codefragment enthält die Methoden zum Eröffnen und Beenden des Zugangs zu Briefkasten-Objekten. Mit Open wird ein neues Objekt angelegt, falls noch keines dieses Namens vorhanden war, oder versucht, ein vorhandenes wiederzueröffnen. Letzteres hat nur Erfolg, wenn das Paßwort mit demjenigen übereinstimmt, mit dem das Objekt ursprünglich angelegt wurde. Mit Close oder Kill wird die Beziehung zu dem Objekt wieder beendet, wobei Kill zusätzlich das Objekt löscht. Durch das Löschen gehen sämtliche Mitteilungen verloren, die von diesem Briefkasten aus abgeschickt wurden, denn zu ihm führende Kanten werden ja ungültig. Die gewählte Datenrepräsentation sorgt also dafür, daß nur Mitteilungen mit gültigem Absender aufbewahrt werden.

Die Methoden zum Abholen und Löschen von Mitteilungen zeigt dieses Codefragment. Nachdem ein Briefkasten-Objekt eröffnet wurde, stehen die schon vorhandenen Mitteilungen zum sequentiellen Abholen mit Get zur Verfügung. Das Resultat von Get ist FALSE, wenn schon alle Nachrichten gelesen worden sind. Der Lesezyklus kann jederzeit mit Reopen von vorn begonnen werden. Nicht mehr gebrauchte Mitteilungen können mit Remove aus dem Briefkasten entfernt werden. Falls auf das Eintreffen neuer Mitteilungen direkt reagiert werden soll, können mit Watch entsprechende Ereignisse angefordert werden.

Zum Umwandeln von Zeichenketten in die abstrakte Form der PersonalMessages.Message und wieder zurück dienen die in diesem Codefragment dargestellten Methoden Create und Extract. Das Präfix mit dem Zeitstempel usw. wird bei Create automatisch erzeugt; Extract liefert es in header getrennt vom Rest der Mitteilung zurück.

Für das Versenden einer Mitteilung gibt es zwei Varianten. Die Methoden dafür zeigt dieses Codefragment. Wenn es sich um eine Rückantwort handelt, braucht nur die zu beantwortende Mitteilung als zweites Argument von Reply angegeben zu werden. Den allgemeinen Fall mit einem anhand seines Namens zu suchenden Empfänger realisiert Send.

Vor dem Anlegen eines neuen Briefkastens und dem Senden einer Nachricht kann es interessant sein, die bereits bekannten Namen abzufragen. Dazu dienen die drei Methoden aus diesem Codefragment. Known testet, ob ein angegebener Name bekannt ist. Users fordert einen Iterator an, der mit GetUser der Reihe nach die Namen der bekannten Benutzer liefert.

Die bisher genannten Methoden setzen voraus, daß stets derselbe Serverknoten anzusprechen ist, sonst wäre ein zusätzlicher Parameter erforderlich. In einem weit verteilten System sollte dieser Fall allerdings nicht ausgeschlossen werden, denn es ist dann sicherlich zweckmäßig, mehrere Server zu integrieren. Dieses Codefragment zeigt, für welche Methoden eine Variante mit einem Server-Parameter existieren sollte. Um im Schema der gewöhnlichen Zeichenketten zu bleiben, empfiehlt sich die Bezeichnung des Servers nach der Syntax des Moduls Paths. Die Varianten ohne Server-Parameter könnten einen mit Default einstellbaren Server benutzen.

Anmerkungen zur Realisierung

Zur Realisierung und internen Datenstruktur des Moduls PersonalMessages ist folgendes anzumerken:

Die internen Definitionen von PersonalMessages.MessageBox und den dazu gehörenden Datenstrukturen sehen wie folgt aus:

TYPE
   BoxesList = POINTER TO BoxesListRec;
   Discipline = POINTER TO DisciplineRec;

   MessageBox = POINTER TO MessageBoxRec;
   MessageBoxRec =
      RECORD
         (Disciplines.ObjectRec)
         node:  Names.Node;
         name:  Names.Name;
         auth:  Shards.Lid;
         msgs:  Iterators.Iterator;
         etype: Events.EventType;
         disc:  Discipline;
         entry: BoxesList;
      END;

   BoxesListRec =
      RECORD
         box: MessageBox;
         prev, next: BoxesList;
      END;

   DisciplineRec =
      RECORD
         (Disciplines.DisciplineRec)
         boxes: BoxesList;
      END;

VAR
   default: Names.Node;
   defaultKey: Resources.Key;
   discId: Disciplines.Identifier;

Daraus ergibt sich, wie in PersonalMessages.Open vorzugehen ist: Ein neues Briefkasten-Objekt anlegen und Name und Autorisierungsobjekt initialisieren. Wenn das entsprechende Knotenobjekt nicht erhältlich ist (was darauf hindeutet, daß es den Knoten noch nicht gibt), einen solchen mit passender Berechtigung vom Server anfordern. Bei Mißerfolg aufgeben. Wenn von dem alten (oder neuen) Knoten die Kantenmenge nicht erhältlich ist, deutet das auf ein unkorrektes Paßwort hin, was wiederum Mißerfolg bedeutet. Resources wird veranlaßt, (Terminations-)Ereignisse, die den Knoten betreffen, an das Briefkasten-Objekt weiterzugeben. Ferner wird ein Bearbeiter für diese Ereignisse vereinbart, der dafür zuständig ist, terminierte Briefkasten-Objekte aus der Liste der Interessenten an einem Ereignistyp wieder herauszunehmen:

PROCEDURE Open(VAR box: MessageBox;
                  name, password: ARRAY OF CHAR): BOOLEAN;
   VAR
      pw: Names.Name;
      perm: Shards.Pot;
      queue: RelatedEvents.Queue;
      etype: Events.EventType;
BEGIN
   NEW(box);
   ConstStrings.Create(box.name, name);
   ConstStrings.Create(pw, password);
   PasswordShards.CreateLid(box.auth, pw);
   IF ~Names.GetNode(default, box.name, NIL, box.node) THEN
      (* create new node for message box *)
      PasswordShards.CreatePot(perm, pw);
      IF ~PersMesgMaintenance.SupplyBox(default,
            box.name, perm, box.node) THEN
         Conclusions.Conclude(default, Errors.error, name);
         box := NIL;
         RETURN FALSE
      END;
   END;
   IF ~Names.GetMembers(box.node, box.auth, box.msgs) THEN
      RelatedEvents.GetQueue(box.node, queue);      (* ignore *)
      box := NIL;
      RETURN FALSE
   END;
   box.etype := NIL;
   box.disc := NIL;
   box.entry := NIL;
   Resources.DependsOn(box, box.node);
   Resources.TakeInterest(box, etype);
   Events.Handler(etype, ResourcesEventHandler);
   RETURN TRUE
END Open;

Die Methoden Close und Kill sind fast trivial:

PROCEDURE Close(box: MessageBox);
BEGIN
   Resources.Notify(box, Resources.terminated);
END Close;

PROCEDURE Kill(box: MessageBox);
BEGIN
   IF ~Names.Destroy(box.node, box.auth) THEN
      Conclusions.Conclude(box.node, Errors.error, "destroy box");
   END;
END Kill;

Ein wenig interessanter ist die Methode Watch. Es muß auf jeden Fall darauf geachtet werden, daß der Bearbeiter für Names-Ereignisse pro Ereignistyp höchstens ein Mal vereinbart wird. Wir stellen dies sicher, indem wir diesen Vorgang mit dem Neuanlegen der Disziplin für den Ereignistyp verbinden. Unabhängig davon wird die Aufnahme des Knotens in die Datenstruktur der Disziplin, eine doppelt verkettete Liste, gehandhabt. Das Briefkasten-Objekt gilt genau dann bereits als Glied dieser Liste, wenn die Komponente entry nicht gleich NIL ist. Sie zeigt dann auf den entsprechenden Eintrag.

PROCEDURE Watch(box: MessageBox; VAR eventType: Events.EventType);
   VAR
      namesEventType: Events.EventType;
      this: BoxesList;
BEGIN
   IF box.etype = NIL THEN
      Events.Define(box.etype);
   END;
   IF box.disc = NIL THEN
      IF Names.TakeInterest(box.node, box.auth,
            namesEventType) THEN
         IF ~Disciplines.Seek(namesEventType, discId,
               box.disc) THEN
            NEW(box.disc);
            box.disc.id := discId;
            box.disc.boxes := NIL;
            Disciplines.Add(namesEventType, box.disc);
            Events.Handler(namesEventType, NamesEventHandler);
         END;
      ELSE
         Conclusions.Conclude(box.node, Errors.error, "watch");
      END;
   END;
   IF (box.disc # NIL) & (box.entry = NIL) THEN
      NEW(this);
      this.box := box;
      this.prev := NIL;
      this.next := box.disc.boxes;
      box.disc.boxes := this;
      IF this.next # NIL THEN
         this.next.prev := this;
      END;
      box.entry := this;
   END;
   eventType := box.etype;
END Watch;

Die übrigen Methoden des Moduls sind relativ unproblematisch aus den Definitionen abzuleiten. Exemplarisch seien hier die Methoden Send und Reply herausgegriffen. Zunächst die interne Typdefinition von PersonalMessages.Message:

TYPE
   Message = POINTER TO MessageRec;
   MessageRec =
      RECORD
         (Disciplines.ObjectRec)
         from:    Names.Node;
         content: Names.Name;
      END;

PROCEDURE Send(msg: Message; to: ARRAY OF CHAR): BOOLEAN;
   VAR
      name: Names.Name;
      node: Names.Node;
BEGIN
   ConstStrings.Create(name, to);
   IF ~Names.GetNode(default, name, NIL, node) THEN
      Conclusions.Conclude(default, Errors.error, "recipient");
      RETURN FALSE
   END;
   IF ~Names.Insert(node, msg.from, msg.content, NIL) THEN
      Conclusions.Conclude(node, Errors.error, "send");
      RETURN FALSE
   END;
   RETURN TRUE
END Send;

PROCEDURE Reply(msg, ref: Message): BOOLEAN;
BEGIN
   IF ~Names.Insert(ref.from, msg.from, msg.content, NIL) THEN
      Conclusions.Conclude(ref.from, Errors.error, "send");
      RETURN FALSE
   END;
   RETURN TRUE
END Reply;

Es sei noch einmal daran erinnert, daß die Fehlerbehandlung hier nur angedeutet wird. Anstatt an Ort und Stelle Fehlermeldungen auszugeben, sollten spezifische Ereignisse generiert und zusammen mit denen aus verschachtelten Aufrufen an die Umgebung zurückgegeben werden.

Zum Abschluß der zitierten Codefragmente sei noch ein Beispiel für den korrekten Umgang mit "gewichtigen" Referenzen angeführt. Die globale Variable default stellt hier eine solche Referenz dar. Das hat die Methode, mit der sie bestimmt wird, zu berücksichtigen:

PROCEDURE Default(server: ARRAY OF CHAR);
   VAR
      errors: RelatedEvents.Object;
BEGIN
   NEW(errors);
   RelatedEvents.QueueEvents(errors);
   IF default # NIL THEN
      Resources.Detach(default, defaultKey);
   END;
   IF ~Paths.GetNode(server, NIL, errors, default) THEN
      Conclusions.Conclude(errors, Errors.error, server);
   END;
   Resources.Attach(default, defaultKey);
END Default;

Benutzerschnittstelle

Ein Blick zurück auf die Modulhierarchie zeigt uns, daß die Benutzerschnittstelle PersMesgClient nur Methoden aus der Abstraktion PersonalMessages benutzt. Namensräume oder gar entfernte Objekte im Allgemeinen sind auf dieser Ebene völlig transparent. Mithin beschränkt sich die Aufgabe dieses Moduls auf die Ein- und Ausgabe von Zeichenketten und einen herkömmlichen Dialog zur Steuerung der verschiedenen Funktionen. Beispiele hierzu sind allenthalben zu finden und sollen daher an dieser Stelle keinen breiteren Raum einnehmen.


oberon index <- ^ -> mail ?
Weiter: Resümee, davor: Erzeugung von Knoten in fremdem Auftrag, darüber: Verteilte Anwendungen.
Martin Hasch, Oct 1996