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:
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.
Zur Realisierung und internen Datenstruktur des Moduls PersonalMessages ist folgendes anzumerken:
Wenn ein Ereignistyp für ankommende Nachrichten angefordert wird, wird für den Knoten Names.TakeInterest aufgerufen. Da im Prinzip von mehreren Seiten auf denselben Knoten zugegriffen werden kann, Ereignisse aber individuell für einzelne dieser MessageBox-Zuordnungen gelten sollen, wird für jede offene Briefkasten-Verbindung ggf. ein eigener Ereignistyp eingesetzt. Ein Bearbeiter des originalen Names-Ereignisses filtert die insert-Fälle heraus und leitet sie genau an die interessierten MessageBox-Objekte in Form ihres individuellen Ereignistyps weiter. Welche Objekte interessiert sind, wird durch eine dem Names-Ereignistyp zugeordnete Disziplin festgehalten. Wenn ein MessageBox-Objekt terminiert, muß es aus dieser Menge wieder entfernt werden. Mit einem Verweis beim Objekt auf die Disziplin und seinen dortigen Listeneintrag kann die Verwaltung der Liste mit konstantem Aufwand erfolgen.
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;
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.