Inhalt


1. Einleitung

2. Anwendungsarchitektur

3. Kommunikationstechniken

4. Evaluierung von MDA-Tools

5. Ergebnis

6. Diskussion und Ausblick

7. Literaturverzeichnis

Anhänge



3. Kommunikationstechniken


Das folgende Kapitel beschäftigt sich mit den für die Messenger-Applikationen möglichen Kommunikationstypen. Nachdem diese im Kapitel 3.1 vorgestellt werden, folgen in Kapitel 3.2 mögliche und realisierte Implementierungen. Insbesondere werden die Techniken Sockets, JXTA, RMI und JINI ausführlich erläutert.


3.1 Theoretische Grundlagen



3.1.1 Sockets


Sockets sind das grundlegende Programmierkonzept für Netzwerkkommunikation. Sie werden vom Betriebssystem angeboten und stellen aus Programmierersicht die Softwareschnittstelle zum Netzwerk dar.

Java bietet eine API zum Zugriff auf die Sockets des Betriebssystems an. Im Package java.net sind einige Klassen vorhanden, die umfangreiche Socketfunktionalität - wie normale TCP/IP-Sockets, UDP-Sockets oder Multicast-Sockets - zur Verfügung stellen. Im Folgenden werden wir nur die Standard-Sockets behandeln. Es handelt sich hierbei um verlässliche TCP/IP-Verbindungen im Unicast-Verfahren. Das bedeutet, ein Socket stellt eine Verbindung mit je genau einem Empfänger und einem Sender dar.

 

Für eine grundlegende Socketimplementierung werden die Klassen Socket und ServerSocket benötigt. Mit ihnen erfolgt ein asymmetrischer Aufbau der Verbindung. Der ServerSocket wartet mit der Methode accept() an einem festgelegten Port darauf, dass ein Verbindungswunsch eintrifft. Dieser wird auf Clientseite mit dem Konstruktor von der Klasse Socket unter Angabe von IP-Adresse und Port des ServerSockets eingeleitet. Ist der Verbindungsaufbau erfolgreich verlaufen, gibt der ServerSocket ebenfalls einen Socket zurück und es steht eine zuverlässige Netzwerkverbindung zwischen den beiden Rechnern, auf die über die Sockets zugegriffen werden kann.

 

Der Transport von Daten erfolgt über Datenströme oder Streams. Jeder Socket hat die Methoden getInputStream() und getOutputStream(), die Ströme zurückgeben, auf die erweiterte Ströme aufgesetzt werden können. Ein Beispiel sind Objektströme, über die komplette Objekte transportiert werden können. Voraussetzung hierfür ist lediglich, dass die zu versendenden Objekte das Interface Serializable implementieren.

Die Erzeugung eines solchen Objektstroms erfolgt mit ObjectInputStream() und ObjectOutputStream(), wobei als Übergabeparameter jeweils der entsprechende Strom des Sockets angegeben werden muss. Unter anderem um sich über die verwendete JDK-Version zu einigen, blockieren die Konstruktoren jeweils, bis das Gegenstück vorhanden ist. Daher muss beim Anlegen der Objektströme darauf geachtet werden, dass die Reihenfolge der Erzeugung von ObjectInputStream und ObjectOutputStream auf Server- und Clientseite vertauscht ist. Da Unstimmigkeiten in der Reihenfolge der Stromerzeugung zu Deadlocks führen können, sollte hierzu eine Hilfsklasse verwendet werden. Sehr gut geeignet ist der SocketChannel, der in [kredel, Seite 106ff] erarbeitet wird.


SocketChannel


Die Klasse SocketChannel verfügt über einen Konstruktur SocketChannel(Socket s), der den Socket entgegennimmt, über den die Kommunikation verlaufen soll. Es wird nun intern die Reihenfolge festgelegt, in der die Ströme deadlockfrei erzeugt werden. Als Kriterium dient der Vergleich zwischen lokaler und entfernter IP-Adresse und Port. Nachdem der SocketChannel auf beiden Seiten erfolgreich erstellt wurde, können über die Methoden send(Object o) und receive() Objekte in beide Richtungen transportiert werden.


ChannelFactory


Es besteht hierbei eine weitere Quelle für Deadlocks. Und zwar beim Erstellen der Sockets durch die Tatsache, dass die Methoden accept() des ServerSockets und Socket() auf Clientseite die weitere Programmausführung blockieren. Dies ist vor allem dann problematisch, wenn ? wie beip2p notwendig ? mehrere Rechner gleichzeitig gegenseitig Verbindungen aufbauen wollen und dabei die Rolle des Servers nicht konkret festgelegt ist. Unter Umständen kann es dann vorkommen, dass zwei Rechner einen ServerSocket benutzen anstatt je einen ServerSocket und einen Socket. Somit warten beide auf einen eingehenden Verbindungswunsch und der Programmablauf wird blockiert. Eine Lösung bietet die ChannelFactory, die ebenfalls in [kredel, S. 112ff] erarbeitet wird.

Das Konzept der ChannelFactory beruht darauf, dass der Aufruf von ServerSocket.accept() auf Serverseite in einen Thread ausgelagert wird und somit der eigentliche Programmablauf nicht mehr blockiert werden kann. Dieser Thread wird bereits im Konstruktor erzeugt und gestartet. Sobald ein Verbindungswunsch eintrifft, gibt accept() einen Socket zurück, aus welchem nun ein SocketChannel erstellt wird. Dieser SocketChannel wird zunächst in einem Puffer zwischengespeichert. Anschließend wird auf weitere eingehende Verbindungswünsche gewartet.

Es gibt zwei Methoden getChannel. Einerseits getChannel() ohne Übergabeparameter, welche auf Serverseite verwendet wird. Sie gibt einfach einen zwischengespeicherten SocketChannel aus dem Puffer zurück. Andererseits getChannel(host, port), welche in einer Schleife versucht eine Socketverbindung mit dem als "host" (entspricht dem Server) angegeben Rechner aufzubauen. Sobald dies erfolgt ist, wird ein SocketChannel erzeugt und zurückgegeben. Die Angabe des Ports im Aufruf von getChannel(host, port) kann ignoriert werden, wenn der in der ChannelFactory definierte Standardport verwendet werden soll.

Nach oben


3.1.2 Multicast


Im Gegensatz zu den oben vorgestellten Sockets hat ein MulticastSocket jeweils mehrere Empfänger. Ein Multicastpaket muss nur einmal an eine Gruppe versendet werden, um von allen Empfängern in dieser Gruppe empfangen werden zu können. Jede Gruppe wird durch eine Gruppen-ID bestimmt, welche genauer betrachtet eine Klasse D IP-Adresse ist. Klasse D IP-Adressen sind alle Adressen im Bereich von 224.0.0.1 bis 239.255.255.255 und für Multicasting reserviert.

Jedes Multicastpaket erhält eine Time-to-Live (TTL), also eine Angabe, wie lange das Paket im Netzwerk weitergeleitet werden soll. Die TTL ist eine Zahl, die beim Senden festgelegt und von jedem Router, der das Paket weiterleitet, um eins dekrementiert wird. Ein Paket mit einer TTL von null wird nicht mehr weitergeleitet und somit verworfen.

Multicasting würde mit einer entsprechend hohen TTL der Pakete theoretisch auch über das ganze Internet funktionieren und vor allem Streaminganwendungen deutlich vereinfachen. Allerdings sind die meisten Internetrouter entweder nicht multicastfähig oder diese Technik ist deaktiviert (siehe [wiki1]).

Java unterstützt Multicasting mit sogenannten MulticastSockets. Nach Erzeugen eines MulticastSockets kann man mit der Methode joinGroup(InetAddress groupAddr) einer Gruppe beitreten. Mit der Methode send(DatagramPacket packet) kann ein UDP-Paket über den Socket versendet und bei jedem Empfänger in der Gruppe mit der Methode receive(DatagramPacket packet) wieder empfangen werden. Der Sender muss nicht zwingend Mitglied der Empfängergruppe sein. Da es sich hierbei im Gegensatz zu den normalen Sockets um UDP-Sockets handelt, über die nur UDP-Datagramme versendet werden können, besteht keine Garantie, dass das Paket bei den Empfängern ankommt. Fehler im Übertragungskanal werden nicht erkannt und die Übertragung wird nicht wiederholt. Diese Begebenheit müsste von der Applikationslogik erkannt und das Senden von ihr wiederholt werden.

Nach oben


3.1.3 JXTA


JXTA (ausgesprochen "jux-ta", von englisch "juxtapose" = "nebeneinander stellen") wurde 2001 bei Sun Microsystems als Forschungsprojekt unter Führung von Bill Joy und Mike Clary ins Leben gerufen. Das Ziel war es, die Vision von verteiltem Rechnen in einer peer-to-peer-Topologie zu ermöglichen.

JXTA mit seinen Komponenten, Diensten und Funktionsweisen ist ein sehr weitreichendes Thema, daher wird im Folgenden nur auf die wichtigsten Aspekte eingegangen.

 

JXTA an sich ist völlig plattformunabhängig. Es definiert nur die Vorgehensweisen und Protokolle, unabhängig von der Programmiersprache und Transportprotokollen. JXTA kann daher z.B. in Java, C++, Perl oder ähnlichem implementiert werden und IP, Bluetooth oder beliebige Anbindungen an ein Netzwerk verwenden. Die Voraussetzung ist, dass das benutzte Gerät einen "digital heartbeat" ([leuf]) haben muss, auf dem der JXTA-Core ausgeführt werden kann. Sinnvollerweise sollte auch eine Anbindung an irgendeine Art von Netzwerk vorhanden sein.




Abb.11 (aus [jxta])

Durch die JXTA-Protokolle wird eine virtuelle Netzwerkschicht definiert. Dieses Ad-Hoc-Netzwerk verbindet alle Clients miteinander, unabhängig von ihrer Systemplattform, ihrem physikalischen Standort und dem verwendeten Transportprotokoll. Auf diese Weise wird ein echtes Peer-to-Peer-Netz geschaffen, das darunterliegende Topologien einschließlich Firewalls und Verbindungsbrücken zwischen unterschiedlichen Techniken versteckt hält.

Da JXTA von Sun entwickelt und beworben wurde, ist die erste Implementierung (die so genannte Referenzimplementierung) in Java geschrieben. Ein weiterer bedeutender Grund hierfür ist die Plattformunabhängigkeit, die mit Java erreicht wird. Aufgrund der Größe des Vorhabens wurde JXTA als Open Source deklariert. So sorgen Entwickler auf der ganzen Welt für ein stetiges Wachsen des Projekts und vieler Tochterprojekte. Beispiele für Projekte sind im Bereich des JXTA-Cores Implementierungen in C, Perl oder J2ME. Zu den Applikationen gehören Kommunikationsanwendungen, eine Spieleplattform, verteilte Dateiaufbewahrung, Handelsprogramme und vieles mehr.


Die wichtigsten JXTA Konzepte


Im Folgenden sind die wichtigsten Konzepte von JXTA erläutert. Wie diese in der Implementierung des Peer-to-Peer-Messengers angewandt wurden, ist in Kapitel 3.2.3 beschrieben.

Jedes Gerät, das mit dem JXTA-Netz verbunden ist, wird Endpunkt ("Endpoint") genannt. Dies kann ein PC, ein Server, ein Handy oder jedes andere Gerät sein, das die Voraussetzungen erfüllt. Ein Beispiel für einen Endpunkt in einem IP-Netz wäre die Angabe einer IP-Adresse mit Port.

Ein Peer ist ein Teilnehmer des JXTA-Netzes. Er kann als die Person angesehen werden, die mit dem Netzwerk interagiert. Denn ein Peer ist nicht an ein bestimmtes Gerät gebunden, sondern kann im JXTA-Netzwerk wandern. Die Zuordnung zwischen Endpunkten und Peers kann sich dynamisch ändern. Durch seine Abstraktionsebenen regelt JXTA transparent die Zuordnung zwischen Netzwerkendpunkt und Peer. Um ihn identifizieren zu können, hat jeder Peer eine eindeutige Peer-ID. Der Name des Peers muss nicht zwingend eindeutig sein.

Peers können in Peergruppen (= "Peergroups") zusammengefasst werden. Dies ermöglicht es, gesicherte Domänen zu bilden, in denen über Bewerbungs- und Mitgliedschaftskonzepte bestimmt werden kann, wer dazugehört. Dadurch können Dienste oder Ressourcen ausgewählten Peers zur Verfügung gestellt und anderen verweigert werden. Zusätzlich dienen Gruppen zu Partitionierung des JXTA-Netzes, um die Kommunikation nicht unnötig auf das ganze JXTA-Netzwerk auszubreiten. Jeder Peer kann Gruppen erstellen, Gruppen finden und Gruppen beitreten (falls das Sicherheitssystem der Gruppe dies zulässt). Gruppen sind hierarchisch als Baum aufgebaut. Das bedeutet, jede Gruppe hat eine Übergruppe und beliebig viele Untergruppen.

Per Definition sind alle JXTA-Peers Mitglied in der "world peer group", welche die einzige Peergroup ohne Übergruppe ist. Zusätzlich erstellt JXTA beim Starten eine Untergruppe der Worldpeergroup, die sogenannte Netpeergroup. In ihr sind alle Peers enthalten, die in irgendeiner Weise über das Netzwerk gefunden werden können.

Ein Advertisement beschreibt je eine bestimmte JXTA-Ressource wie einen Peer oder eine Peergroup. Typische Eigenschaften, die hierin gespeichert werden sind eine eindeutige ID und der Name. JXTA-Advertisements sind XML-Dokumente, die von den Peers untereinander ausgetauscht und je in einem lokalen Cache der Applikation gespeichert werden.

Messages sind all das, was zwischen den Peers ausgetauscht wird. Sie können sowohl aus Text (XML-Format) als auch aus Binärdaten bestehen. Advertisements sind eine spezielle Art von Messages.

Der Kommunikationskanal, über den Peers beliebige Formen von Informationen austauschen können, heißt Pipe. Eine Pipe ist nicht an einen physikalischen Netzwerkendpunkt gebunden, d.h. unabhängig von Port, IP-Adresse, sogar unabhängig vom verwendeten Protokoll. Eine solche Bindung besteht nur in dem Moment, in dem tatsächlich über sie kommuniziert wird.




Abb. 12

Um über eine Pipe zu kommunizieren, muss der Empfänger sich als Inputpipe und der Sender als Outputpipe an die Pipe binden. Hierfür wird auf beiden Seiten das gleiche Pipe-Advertisement benötigt. Pipes sind unidirektional, unzuverlässig und asynchron. Das bedeutet, dass Messages über sie nur in eine Richtung versendet werden können und es keine Garantie dafür gibt, dass sie beim Empfänger in der richtigen Reihenfolge oder überhaupt ankommen. Es gibt zwei Ausführungen von Pipes: ?Unidirectional Pipes?, die mehrere Sender und nur einen Empfänger haben können und ?Propagate Pipes?, die mehrere Sender und auch mehrere Empfänger haben können (siehe Abb. 12 nach [sili].

Ein Service oder Dienst wird von einem Peer angeboten und von anderen Peers genutzt. Welchen Nutzen der Service im Detail anbietet und wie die Kommunikation mit ihm abläuft, wird von JXTA nicht festgelegt. Um das konventionelle Server-Prinzip mit dem Server als Single Point of Failure zu umgehen, ist es in einer Peer-to-peer-Umgebung sinnvoll, den gleichen Service auf mehreren Peers anzubieten. In JXTA gibt es hierfür Peergroup-Services. Das bedeutet, dass mehrere Peers in einer Peergroup den gleichen Service anbieten. Im Idealfall gleichen sich diese Peers untereinander so ab, dass ein Nutzer des Services durch transparentes Binding nicht erkennt, von welchem konkreten Peer er den Dienst nutzt.

 

Der Discovery-Service findet durch eine verteilte Anfrage andere Peers, Peergroups, Pipes und weitere Komponenten, die durch Advertisements beschrieben werden. Er schickt Suchanfragen unter anderem per Multicast über das Netzwerk und wertet die erhaltenen Antworten aus. Zusätzlich ist er dafür verantwortlich, auf Anfragen anderer Peers mit passenden Ergebnissen zu antworten.

Für das Binden von Peers an Pipe-Endpunkte ist der Pipe-Service zuständig. Er wird auch verwendet, um Messages über die Pipes zu versenden.

Um Verbindungen in einem großen Netzwerk wie dem Internet verwalten zu können, wird der Router-Service benötigt. Eine Applikation mit Routerservice weiß, wie er andere Endpunkte durch das JXTA-Netz erreichen kann und leitet Messages an entsprechende andere Router weiter. Zusätzlich handeln Router untereinander neue Routen aus, die bisher nicht benötigt wurden oder die alte, nicht mehr verfügbare, ersetzen.

Ein häufiges Problem sind Rechner, die sich hinter einer Firewall befinden. Der Relay-Service bietet auch solchen Peers die Möglichkeit, mit dem JXTA-Netzwerk zu kommunizieren. Dazu speichert der Relay-Service alle Messages, die an den Peer hinter der Firewall gerichtet sind. Der Peer verbindet sich nun regelmäßig mit seinem Relay per HTTP, um diese eingehenden und ausgehende Messages auszutauschen.

 

Mit JXTA können sowohl serverbasierte als auch serverlose Peernetze realisiert werden. Der Hauptvorteil von JXTA bei einem serverbasierten System ist sicher der Mechanismus zum Entdecken anderer Peers, der hierbei allerdings ausschließlich zum Auffinden des Servers verwendet wird. Die Stärken von JXTA kommen allerdings nur in einem reinen Peer-to-Peer-Netz zur Geltung.

Nach oben


3.1.4 RMI


Die Remote Method Invocation (RMI) ermöglicht das Aufrufen von Methoden eines Objektes in einer anderen Java Virtual Machine (JVM), die auf einem anderen Rechner läuft. Sie gehört, wie auch CORBA oder RPCs, zu den klassischen Techniken, die eine vereinfachte Kommunikation von Rechnern in einem Netzwerk ermöglichen.

Ein Server zeichnet sich bei RMI dadurch aus, dass auf ihm die so genannte RMI-Registry, die Bestandteil von Java ist, ausgeführt wird. Sie kann per Hand über die Kommandozeile mit dem Befehl rmiregistry gestartet werden. In diese Registry können über die Klasse java.rmi.Naming Remote-Objekte eingetragen und einem Namen zugeordnet werden. Alternativ kann über die Klasse java.rmi.registry.LocateRegistry mit der Methode createRegistry() innerhalb einer Java-Applikation eine Referenz auf ein Registry-Objekt geholt werden, das eine bind() Methode anbietet, um Remote-Objekte zu registrieren.

Damit ein Client auf einem Remote-Objekt operieren kann, benötigt er bereits zur Kompilierungszeit das Wissen, welche Operationen er auf diesem Objekt ausführen kann. Die Operationen entsprechen in Java Methoden eines Objekts, die in einem Interface, welches das java.rmi.RemoteInterface erweitert, festgelegt werden. Um die Remote-Objekte auf Serverseite zu definieren, ist die gebräuchlichste Variante, die Klasse java.rmi.server.UnicastRemoteObjekt zu erweitern und das eben beschriebene Interface zu implementieren.

Der Client gelangt über die Methode lookup() der Klasse java.rmi.Naming unter Angabe der URL oder über das Interface java.rmi.registry unter Angabe des Namens des gewünschten Remote-Objekts an dieses. Die URL enthält die IP und den Port des Servers sowie den Namen, an den das Remote-Objekt gebunden ist. Mit diesen Maßnahmen ist es möglich, dass ein Client auf einem Remote-Objekt, genauso wie bei einem lokalen Objekt, Methoden aufrufen kann. Die Ausführung der Methode erfolgt aber auf der JVM des Servers. Die nötige Kommunikation zwischen Client und Server übernimmt RMI. Es werden die Übergabeparameter der Methode zusammengestellt und vom Client an den Server geschickt. Der Rückgabewert oder eine eventuell auftretende Exception wird von RMI verpackt und an den Client zurückgegeben. Die Methodenaufrufe sind dabei synchron, d.h. der Thread des aufrufenden Clients blockiert bis er eine Rückmeldung vom Server erhält.

So ist es möglich eine entfernte Methode aufzurufen, ohne dass man selbst, weder auf Client- noch auf Serverseite, Code für die Netzwerkkommunikation schreiben muss. Dieser Vorgang wird von RMI verdeckt. Was bei der Implementierung auf der Serverseite allerdings beachtet werden muss, ist, dass mehrere Clients gleichzeitig dieselbe Methode auf dem Server nebenläufig aufrufen können. Das heißt es müssen möglicherweise Synchronisierungsanforderungen beachtet werden. Der einzige Unterschied zwischen einem lokalem und einem entfernten Aufrufs ist, dass jeder Aufruf eine RemoteException werfen kann, wenn etwas bei der Kommunikation zwischen Client und Server fehlgeschlagen ist.

Aus technischer Sicht unterscheidet sich aber ein entfernter Methodenaufruf erheblich von einem lokalen Aufruf. Gewöhnlich werden bei Java Objekte stets als Referenz (by reference) und nicht als Wert (by value) übergeben. Ein Server könnte aber nichts mit einer Referenz auf ein Objekt anfangen, weil dieses im physischen Speicher des Clients steht, auf den der Server keinen Zugriff hat. Deshalb werden bei RMI Objekte als Wert an den Server übergeben, was allerdings voraussetzt, dass das Objekt kopiert werden kann. Dies wird erreicht, indem ein Objekt in ein Byte-Array serialisiert wird. Beim Deserialisieren des Arrays erhält man dann eine Kopie des Objekts. Allerdings lassen sich in Java nur diejenigen Objekte serialisieren, die mindestens das Interface java.io.Serializable implementieren. Da das Serialisieren ein rekursiver Vorgang ist, d.h. alle Objekte, die von einem zu serialisierenden Objekt referenziert sind, auch serialisiert werden, müssen alle enthaltenden Objekte serialisierbar oder als transient gekennzeichnet sein. Letzteres schließt das Objekt von der Serialisierung aus. Wird zur Laufzeit versucht ein nicht serialisierbares Objekt zu kopieren, kommt es zu einer NotSerializableException, was bereits bei der Entwicklung vermieden werden sollte.

Eine Ausnahme bei der Objektübergabe bilden die Remote-Objekte. Für diese wird eine Objektübergabe by reference simuliert. Um dies zu erreichen, muss für jede Klasse, die ein Remote-Interface implementiert, ein so genannter Stub (Rumpf) erzeugt werden. Dieser Stub wird serialisiert und an den Client geschickt. Ruft der Client jetzt eine Methode des Remote-Objekts auf, so übernimmt der Stub, für den Entwickler verborgen, die Kommunikation mit dem Server, der dann Operationen auf dem Orginalobjekt ausführen kann. Das Ergebnis wird vom Stub auf Serverseite serialisiert und an den Stub auf Clientseite übergeben. Die Erzeugung der Stubs erfolgt über den von Java mitgelieferten RMI Compiler, der in einer Kommandozeile mit rmic unter Angabe der Remote-Klasse gestartet wird.

Nach oben


3.1.5 Jini




Abb. 13

Die von Sun Microsystems entwickelte Systemarchitektur Jini (Java Intelligent Network Infrastructure) soll das Zusammenspiel von beliebigen Elektrogeräten (Computer, Drucker, Stereoanlage, Handys, etc.) erleichtern. Diese Vernetzung der Geräte kann mit Jini von einem lokalen bis hin zu einem weltweit verbreiteten Netzwerk erreicht werden.

Aus Anwendersicht sollen sich die Geräte ohne Installation von Zusatzsoftware, wie beispielsweise Treibern, verstehen können. Dies wird dadurch erreicht, dass die Jini Technologie bei der Software ansetzt und nicht versucht, die Standardisierung mit Blick auf die Hardware zu realisieren. Bisher benötigte man für eine Hardware-Komponente je nach Betriebssystem und dessen Version einen anderen Treiber. In eine Jini-Komponente ist ein vom Hersteller entwickelter Treiber für eine Java-Plattform bereits integriert. Wegen der Plattformunabhängigkeit von Java ist also nur noch ein auf Jini basierender Treiber notwendig, der sich an die Definitionen von Jini bezüglich dem Aussehen der Dienstleistungen (Services) und dem Austausch von Informationen (Protokoll) hält.

Durch den Softwareansatz ergibt sich zusätzlich der Vorteil, dass nicht nur Hardware, sondern auch Applikationen ihre Dienste mit Hilfe der Jini Technologie in einem Netzwerk bereitstellen können.

Zusammenfassend kann also gesagt werden, dass Jini einen Verbindungsaufbau und die Kommunikation zwischen Netzkomponenten ermöglicht, auf denen jeweils ein Java-Programm ausgeführt werden kann. Die Netzkomponenten können dabei Hard- aber auch Software sein, so dass diese Unterscheidung von Jini "verwischt" wird.

Nach [oaks, Seite 3] ist Jini eine Menge von Spezifikationen, mit deren Hilfe Dienste einander im Netz finden können und die einen Rahmen bieten, innerhalb dessen diese Dienste an bestimmten Arten von Operationen beteiligt sein können. Diese Menge von Diensten auf einem bestimmten Netzwerk nennt man Jini-Gemeinschaft.

Folgende Abbildung zeigt eine solche Jini-Gemeinschaft:


Funktionsweise


Um die Arbeitsweise von Jini besser verstehen zu können, gehen wir von einem Netzwerk aus, in dem bereits der Jini-Lookup-Service zur Verfügung steht (1). Beim Starten eines weiteren Dienstes findet dieser, ohne Wissen über die physikalische Lokation auf dem der Lookup-Service läuft, diesen und registriert sich bei ihm (2+3). So hat der Lookup-Service zu jedem Zeitpunkt Kenntnis über alle Jini-Dienste in diesem Netzwerk. Hinzu kommt ein Konsument (Client), der die beim Lookup-Service registrierten Dienste in Anspruch nehmen kann. Um die Dynamik der Jini-Gemeinschaft möglichst flexibel zu gestalten, stehen dem Konsumenten verschiedene Arten, wie er sich bei einem Lookup-Service anmelden kann, zur Verfügung. So ist es ihm möglich zu Beginn alle beim Lookup-Service registrierten Dienste zu sehen oder lediglich bei Bedarf einen Dienst anzufordern (4).

Der Konsument kann sich auch dergestalt registrieren, dass er eine Benachrichtigung bekommt, wenn ein bestimmter Dienst im Netzwerk neu verfügbar ist oder wenn ein Dienst vom Netz genommen wird.

Will ein Client einen Dienst nutzen, bezieht er beim Lookup-Service einen so genannten Proxy, der vorher vom Service registriert wurde. Über diesen Proxy kann der Client dann direkt mit dem Service kommunizieren (5).


Architektur und Technologie




Abb. 14: Statische JINI-Struktur

Bei Jini handelt es sich um eine dynamisch verteilte Systemarchitektur. Es werden Dienste über das gesamte Netzwerk verteilt angeboten und es können ständig Dienste neu registriert oder entfernt werden [bader, Seite 3]. Die verteilte Architektur steigert deren Robustheit in Bezug auf den Ausfall einzelner Dienste. Diese Ausfälle haben nur Auswirkung auf die aktuellen Nutzer dieses Dienstes und nicht etwa auf das gesamte Netz. Die Dienste, bei Jini auch als Services bezeichnet, werden jeweils durch ein Java-Objekt dargestellt.

Jini wurde in Java realisiert und benötigt eine Java Virtual Machine ab Version 1.2, da es auf bestehende Technologien aus dem Java-Umfeld zurückgreift (RMI, JavaSpaces, Java-Security-Mechanismus, JavaBeans, etc.). Deshalb ist bei der in Abbildung 14 dargestellten Architektur auch keine klare Trennung zwischen Jini und Java zu erkennen.

 

Möchte ein Client einen Service in Anspruch nehmen, erfolgt die logische Kommunikation unter Ausnutzung von Jini. Jini selbst gibt über Java-Spaces die Anforderung an RMI weiter, welches wiederum über die JVM die Netzwerkdienste des entsprechenden Betriebssystems nutzt.

 

Nachfolgend wird erläutert, welche Technologie hinter der oben beschriebenen Funktionsweise steht. Ganz einfach betrachtet besteht das Netzwerk aus Clients und Servern. Ein Server implementiert ein Interface, das nach außen hin bekannt ist und als Dienst bezeichnet wird. Diese Dienste können ohne Vorwissen über deren Implementierung konsumiert werden. Klassen des Servers können vom Client zur Laufzeit dynamisch geladen werden. Der Konsument benötigt also zur Kompilierungszeit lediglich die Interfaces der im Netz angebotenen Dienste.

Eine Jini-Anwendung kann gleichzeitig als Client und Server agieren. Nimmt ein Server Dienste von einem anderen Server in Anspruch, ist er in diesem Moment ein Client. Genau genommen ist jeder Jini-Dienst auch ein Jini-Client, weil er mindestens den im Folgenden beschriebenen Lookup-Service in Anspruch nimmt.

 

Der Jini-Lookup-Service ist die zentrale Komponente einer Jini-Gemeinschaft. Bei ihm registrieren sich Services, die im Netz verfügbar sind und an ihn wenden sich Clients, die einen bestimmten Service benötigen. Er ist daher vergleichbar mit einem Namensserver, wie er zum Beispiel bei RMI verwendet wird.

Die registrierten Dienste werden jeweils als ein Objekt der Klasse ServiceItem beim Lookup-Service verwaltet. Dieses Objekt besteht aus einer eindeutigen Service-ID, dem eigentlichen Service-Objekt und einer Menge von Attributen, die den Service genauer charakterisieren.

Innerhalb einer Jini-Gemeinschaft kann der Lookup-Service als ein weiterer Dienst betrachtet werden, der von den anderen Services und Clients im Netzwerk genutzt wird. Das Interface dieses Dienstes ist durch die Jini Lookup Service Specification definiert. Jini stellt mit der Datei reggie.jar eine Implementierung dieses Interfaces zur Verfügung. Es legt alle Operationen, die mit dem Lookup-Service möglich sind, fest, wie beispielsweise das Lokalisieren eines Dienstes innerhalb eines Netzwerks.

Damit Clients und Server Lookup-Services in der Jini-Gemeinschaft ausfindig machen können, existiert das Discovery-Protokoll. Man unterscheidet zwischen Multicast-Discovery, das vorhandene Lookup-Services über Multicasts ausfindig macht, und Unicast-Discovery, das die Verbindung mit einem Lookup-Service an einem durch Host und Portnummer bestimmten Ort ermöglicht. Das Multicast-Discovery-Protokoll entdeckt nur Lookup-Services, die durch Multicasts erreichbar sind (siehe Kapitel 3.1.2).

Um sicherzustellen, dass der Lookup-Service keine Dienste verwaltet, die nicht mehr zur Verfügung stehen, vergibt er bei der Anmeldung einen so genannten Lease, der in regelmäßigen Abständen vom angemeldeten Dienst erneuert werden muss. Läuft ein Lease ab, deregistriert der Lookup-Service den dem Lease zugeordneten Dienst. Gründe für ein nicht ordnungsgemäßes Deregistrieren eines Dienstes können beispielsweise der Absturz des Geräts, auf dem der Dienst läuft, oder ein Zusammenbruch des Netzwerks sein. Ist letzteres der Fall, meldet sich der deregistrierte Dienst automatisch wieder beim Lookup-Service an, sobald er bemerkt, dass das Netzwerk wieder funktionstüchtig ist. Man kann also sagen, dass Jini Selbstheilungskräfte [oaks] besitzt. Für dieses Leasing-Konzept bietet Jini unterschiedliche Interfaces und Hilfsklassen an. Das eigentliche Leasing-Protokoll muss aber selbst implementiert werden.

Für das Finden eines bestimmten Dienstes bei einem Lookup-Service muss der Client bei einer Anforderung das Interface des gewünschten Dienstes angeben. Dienste können mit Hilfe von Attributen genauer spezifiziert werden, so dass ein Client auch zusätzlich die Dienstauswahl durch Angabe von gewünschten Attributen einschränken kann.

Damit der Lookup-Service, wie weiter oben in diesem Kapitel unter "Funktionsweise" beschrieben, auch Clients darüber informieren kann, wenn ein neuer Dienst registriert wurde oder ein Dienst nicht mehr zur Verfügung steht, erweitert Jini den Event-Mechanismus von Java. Es werden von Jini diverse Interfaces für die Behandlung von verteilten Events zur Verfügung gestellt, die Events auf entfernten Rechnern behandeln.

Wie in Abbildung 13 zu sehen, können in einem Netzwerk auch mehrere Lookup-Services parallel laufen, um eine bessere Ausfallsicherheit zu gewährleisten.


Abgrenzung zu klassischen verteilten Systemtechnologien


Vergleicht man Jini mit anderer Client-Server-Middleware für verteilte Systeme wie beispielsweise RMI, CORBA, RPC oder DCOM, lassen sich eine Vielzahl von Unterschieden feststellen.

Bei Jini geht es im Wesentlichen darum, wie sich Services und Clients untereinander finden und nicht um die konkrete Implementierung eines Services [jini]. Es spielt also keine Rolle, welches Protokoll ein Service für die Kommunikation mit einem Client verwendet. So kann sich zum Beispiel ein CORBA-Dienst genauso wie ein anderer Dienst mit einem proprietären Protokoll beim Jini-Lookup-Service registrieren. Clients, die einen Dienst in Anspruch nehmen, kommunizieren dann über das entsprechende Protokoll mit dem Service. Es besteht nicht einmal die Notwendigkeit, dass der Service in Java implementiert wurde. Lediglich der Mechanismus, den Jini nutzt, um andere Jini-Komponenten zu finden, muss in Java geschrieben werden.

Des Weiteren realisiert Jini das Konzept der Ortstransparenz. Bei den klassischen Technologien benötigt der Client die Information auf welcher Maschine der Server genau läuft, auf dem eine Aufgabe erledigt werden soll. Bei Jini braucht ein Client nicht zu wissen, wo sich ein bestimmter Dienst befindet. Er erlangt Zugriff auf den Service über den Lookup-Service, der mit der Lookup-Discovery gefunden werden kann.

Ein weiterer Vorteil ist der bereits oben beschriebene Selbstheilungsprozess. Jini setzt also keine durchgehende Verfügbarkeit des Netzwerks voraus und kompensiert auch Softwarefehler bei Komponenten in der Jini-Gemeinschaft, ohne dass der Nutzer es bemerkt oder eingreifen muss.

Durch das Konzept des Leasings kann sichergestellt werden, dass die durch einen Client belegten Ressourcen bei einem Service wieder freigegeben werden, wenn der Client sein Lease nicht verlängert.

Daraus lässt sich ableiten, dass Jini kein Ersatz für die oben genannten klassischen Client-Server-Technologien ist, sondern eine Erweiterung für das Verwalten von dynamischen Netzwerkumgebungen darstellt.

Nach oben


3.6.1 MPI


Bei MPI, dem Message Passing Interface, handelt es sich um die Spezifikation eines Kommunikationsmodells, welches ab 1994 in der Version MPI 1.0 und seit 1997 in der Version 2.0 spezifiziert ist. MPI ist als Standard für high performance Rechensysteme gedacht. Es ermöglicht die parallele Berechnung auf verteilten, heterogenen, lose-gekoppelten Systemen, welche quasi-paralleles Rechnen unterstützen [wiki2]. Durch MPI ist eine gemeinsame Problemabarbeitung auf mehreren Systemen möglich. Der Datenaustausch zwischen den verschiedenen Systemen geschieht mittels Nachrichten, welche über Rechnergrenzen hinaus gesendet werden können. Dazu werden zu Beginn von einem Masterprozess auf dem Mastersystem die verteilten Prozesse auf den Arbeitssystemen parallel gestartet.

Für die Anwendung von MPI stellen verschiedene Hersteller ihre eigenen Implementierungen bereit. Alle diese Implementierungen definieren Funktionen, mit denen eine typische Punkt-zu-Punkt-Kommunikation, sowohl synchron wie auch asynchron, betrieben werden kann.

 

Ein zentrales Konzept von MPI sind die Kommunikatoren (Communicators). Hierbei handelt es sich um virtuelle Kanäle zwischen den Kommunikationspartnern, mit deren Hilfe parallele Programmbibliotheken auf MPI-Basis möglich sind. Ebenfalls ist es möglich Untergruppen von Prozessen zu definieren, welche nur untereinander kommunizieren können.

 

Allerdings besitzt MPI auch ein paar Schwachpunkte. Diese liegen im Erkennen von Abstürzen, dem Aufräumen von Ressourcen und der Behandlung nicht abgeholter Nachrichten. Ebenfalls ist es mittels MPI aufwändig, Daten auf die einzelnen Prozesse zu verteilen und deren Ergebnisse wieder einzusammeln. Des Weiteren ist in MPI 1.0 nicht spezifiziert wie Prozesse gestartet, überwacht und beendet werden sollen. Dies bleibt den verschiedenen Implementierungen von MPI überlassen. [kredel]

 

Für die Kombination von MPI und Java gibt es zwei mögliche Konzepte. Das eine Konzept besteht aus der Implementierung von MPI mittels JAVA. Dies hätte den Vorteil, dass RMI, JMS und JINI für MPI weiter benutzbar wären. Allerdings gab es Stand 2001 noch keine zufrieden stellende Implementierung.

Das andere Konzept sieht eine Anbindung von Java an ein existierendes MPI mittels JNI vor. Der Vorteil wäre eine ausgereifte und angepasste MPI-Implementierung als Grundlage, wobei dies einer zweistufigen Implementierung aus Javasicht entsprechen würde. [kredel, S.241]


MPI und die Messengeranwendung


Trotz der Möglichkeit mit MPI über Rechnergrenzen hinweg mittels eines virtuellen Kanals direkte Kommunikation zwischen den Systemen zu betreiben ist MPI nicht für die Messengeranwendung geeinget, da eine zentrale Masterinstanz benötigt wird und es von der Konzeption her eher für High Performance Clustersysteme gedacht ist.

Ein weiteres Hindernis von MPI für die Anwendung in der Chatapplikation besteht darin, dass MPI ein statisches Prozessmodel besitzt. Dass heißt alle Prozesse, die in einer MPI-Laufzeitumgebung existieren, betreten und verlassen die Umgebung quasi gleichzeitig. [mpi1, Idee von MPI].

 

Da also MPI für die Implementierung nicht in Frage kommt, wird im Kapitel 3.2 - Implementierungen auch nicht mehr auf MPI eingegangen.

Nach oben


3.2 Praktische Umsetzungen


In den folgenden Unterkapiteln wird auf die möglichen Implementierungen der in obigem Kapitel beschriebenen Kommunikationstechniken eingegangen. Insbesondere werden die in der Messenger-Anwendung implementierten Techniken JXTA und RMI näher erläutert, sowie die aus RMI hervorgegangene Umgebung Jini, welche jedoch praktisch nicht umgesetzt wurde.


3.2.1 Sockets


Bei Verwendung der oben beschriebenen Hilfsklassen ChannelFactory und SocketChannel wäre es ohne Gefahr von Deadlocks möglich, die Verbindungen zwischen den Peers zu erstellen und zu nutzen. Sobald eine Verbindung zu einem anderen Peer benötigt wird, wird sie damit aufgebaut und anschließend entweder zur weiteren Verwendung aufrechterhalten oder wieder abgebaut. Über diese Verbindungen können sowohl Chat- als auch diverse Steuerungsnachrichten ausgetauscht werden.

 

Die größte Herausforderung ergibt sich jedoch bei der Frage, wie die Liste aller derzeit aktuellen Peers verwaltet wird. Grundsätzlich gäbe es zwei Möglichkeiten:

A) Ein Peer im Netzwerk ist der Master, der die aktuelle Liste verwaltet und bei Bedarf verteilt.

B) Es gibt keinen Master, die Liste wird zu jedem Zeitpunkt auf allen Peers möglichst aktuell gehalten.

Wenn ein neuer Peer in das Netzwerk hinzukommt, muss er zunächst die IP-Adresse des Masters oder eines Peers herausfinden, um sich die Liste mit IP-Adressen aller anderen Peers geben zu lassen. Dies lässt sich durch Anwendung von Multicasting (siehe Kapitel 3.2.2) bewerkstelligen.

Schwieriger dürfte es sich gestalten, die Liste der Peers immer aktuell zu halten. Wenn ein Peer sich kontrolliert abmeldet, kann er vorher eine Meldung an den Master (Variante A) oder alle anderen Peers (Variante A) verschicken. Sollte dies aber aufgrund eines Programmabsturzes, Netzwerkfehlers oder ähnlichem nicht mehr möglich sein, müssen Mechanismen implementiert werden, die solche Ereignisse erkennen und entsprechend darauf reagieren.


Einige weitere zu bedenkende Aspekte


Variante A: Bei Verwendung eines Masters ist es ein gravierendes Problem, wenn dieser nicht mehr erreichbar ist. Dann muss ein neuer Master bestimmt und dies an alle Peers kommuniziert werden. Hier müssen Spezialfälle berücksichtigt werden, wie beispielsweise eine lediglich kurze Unterbrechung der Netzwerkverbindung zu dem Master. In diesem Fall kann es vorkommen, dass ein neuer Master bestimmt wird und der alte seinen Dienst weiterführt. Eine Überlegung wäre die Verwendung eines zweiten Masters, der mit dem ersten dauerhaft in Verbindung steht und bei Bedarf den Dienst übernimmt.

Variante B: Ohne Verwendung eines Masters wäre es am naheliegendsten, dass ein Peer, der eine Änderung der Liste festgestellt hat, die neue Liste an alle anderen Peers verbreitet. Aber angenommen, zwei Peers löschen unabhängig voneinander je einen Peer aus der Liste, weil dieser nicht mehr erreichbar ist, und verbreiten ihre neue Liste. Ein weiterer Peer, der diese beiden erhält, kann nicht entscheiden, welche der Listen die richtige ist oder wie diese vereint werden müssen. Die Alternative wäre es, nur die Änderungen anstatt der ganzen Liste an die bestehenden Peers zu kommunizieren. Aber auch hier besteht in Einzelfällen Potential zu Differenzen zwischen den Listen.

 

Zusammenfassend lässt sich sagen, dass es definitiv möglich ist, den Peer-to-Peer-Chat unter Verwendung reiner Sockets zu implementieren. Allerdings müssen sehr viele Spezialfälle berücksichtigt werden, sicher auch einige, die hier noch nicht bedacht wurden. Alle Eventualitäten abzufangen und die Kommunikation in der Applikation robust zu gestalten, wäre nur mit erheblichem Aufwand möglich. Zuverlässige Entscheidungen, wie der Kommunikationsablauf und die Listenverwaltung aussehen, können nur nach gründlichen Performance- und Robustheitstest getroffen werden.

Diese Arbeit wurde von den Machern von JXTA abgenommen, welches auf Sockets basiert und dem Entwickler einen Großteil der oben erwähnten Schwierigkeiten abnimmt.

Nach oben


3.2.2 Multicasting


Aufgrund der Unzuverlässigkeit des UDP-Transports ist Multicasting alleine keine Technik, mit der das Vorhaben des Peer-to-Peer-Messengers umgesetzt werden kann. Allerdings ist es eine große Hilfe, wenn ein neuer Peer dem Netzwerk beitreten möchte.

Da die Applikation unabhängig von der IP-Adressenstruktur im verwendeten Netzwerk funktionieren soll, kennt eine neue Instanz weder die IP-Adresse eines eventuell vorhandenen Servers / Masters, noch die irgendeines anderen Peers. Das manuelle Eingeben der Adresse eines bereits vorhandenen und bekannten Teilnehmers wäre sehr unkomfortabel. Mit Multicasting kann ein solcher automatisch und für den User transparent erkannt werden.

Der Server / Master / jeder Peer hat immer einen MulticastSocket auf einem fest definierten Port geöffnet und ist Mitglied einer ebenfalls festgelegten Multicast-Gruppe, deren Nachrichten er dadurch empfängt. Ein neuer, unbekannter Peer müsste nun lediglich an diese Multicastgruppe eine Nachricht senden, dass er an dem Netzwerk teilnehmen möchte. Der Master / Server / ein Peer kann ihm nun je nach verwendeter Technik antworten, eine Verbindung aufbauen und ihn in die Peerliste aufnehmen.

Nach oben


3.2.3 JXTA


Im folgenden Kapitel wird erläutert, welche Aspekte von JXTA bei der Umsetzung des Peer-to-Peer-Messengers in welcher Art verwendet wurden.

 

Verwendet wurde die aktuellste verfügbare Version JXTA 2.3.3. Die dazugehörigen Bibliothekdateien können unter [jxta] heruntergeladen werden. Sie sind auch in die endgültige Version des Messengers integriert.

 

Beim ersten Start einer JXTA- Applikation auf einem System wird üblicherweise ein komplexer Konfigurationsdialog angezeigt, in dem Peername, ein Passwort, zu verwendete Transportprotokolle und einige Einstellungen der Core-Services angegeben werden können. Dies wird hier umgangen durch Verwendung der Klasse Configurator aus einem JXTA-Erweiterungspaket. Es müssen lediglich ein Peername und ein Passwort angegeben werden, die in diesem Anwendungsfall jedoch keine weitere Rolle spielen. Es werden durch den Configurator automatisch die besten Einstellungen gesetzt. So wird dafür gesorgt, dass genügend Rendevouz- und Relaydienste vorhanden sind, um eine optimale Kommunikation im JXTA- Netz zu gewährleisten, auch über Firewalls hinweg. Um bei jedem Programmstart die gleichen Voraussetzungen zu schaffen, löscht die Messengerapplikation vor der Initialisierung von JXTA dessen Cacheverzeichnis, soweit vorhanden.

Als nächster Schritt in der Initialisierung wird eine Peergroup unterhalb der NetpeerGroup erstellt, genannt p2pGroup. Alle Kommunikation des Messengers läuft innerhalb dieser Gruppe ab. Dafür werden mit getDiscoveryService() und getPipeService() Referenzen auf die benötigten Dienste dieser Gruppe erstellt.

Schließlich werden mehrere aufeinander folgende Suchfragen nach bereits angemeldeten Usern in dieser Gruppe gestartet. Die Antworten landen im lokalen Cache, aus dessen Inhalt nach kurzer Wartezeit die Liste der bereits angemeldeten User erstellt wird. Da es keine Garantie dafür gibt, dass alle Peers auf diese Anfragen antworten, ist auch nicht gewährleistet, dass der Cache zu diesem Zeitpunkt alle Peers enthält. Dadurch lässt es sich vor allem in einem relativ weit verstreuten Netzwerk nicht vermeiden, dass die Peerliste zu diesem Zeitpunkt noch nicht hundertprozentig vollständig ist. Dies alles erledigt die Methode initJXTA(), welche durch den Konstruktor des JxtaCommunicator ausgeführt wird.

 

Die Methode initComm() bekommt den gewählten Nickname übergeben. Falls dieser schon in der vorhandenen Peerliste steht, also von einem anderen Peer verwendet ist, bricht sie mit Rückgabewert false ab. Ansonsten erstellt sie zunächst die Inputpipe. Hierfür wird ein neues PipeAdvertisement erstellt, diesem der gewünschte eigene Nickname zugewiesen und es daraufhin in der Gruppe veröffentlicht. Da dieses Advertisement den Nickname trägt, reicht es in diesem Falle aus, nur das PipeAdvertisement zu veröffentlichen. PeerAdvertisements werden nicht genutzt. Alle anderen Peers können nun das veröffentlichte PipeAdvertisment finden und nutzen, um eine Pipe zu diesem neuen Peer aufzubauen.

 

Es wird schließlich ein Thread der Klasse PeerChecker gestartet. Dieser übernimmt mehrere Aufgaben. Zum einen führt er regelmäßig die Methode checkPeer() für alle bekannten Peers aus, um deren Verfügbarkeit zu überprüfen. Zweitens sendet er regelmäßig über den Discoveryservice eine Suchanfrage an das Netzwerk, um mit der Peerliste auf dem aktuellen Stand zu bleiben. Bei eingehenden Antworten wird die Methode discoveryEvent() ausgeführt, die zur Schnittstelle des implementierten Interfaces DiscoveryListener gehört. Falls in der Antwortmenge ein noch unbekannter Peer erscheint, wird dieser in die Peerliste aufgenommen.

 

Die Peerliste ist eine Instanz der Klasse Peers. Sie verwaltet zu jedem Peernamen dessen PipeAdvertisement und die OutputPipe, welche die Verbindung zu dem Peer darstellt. Wenn ein neuer Peer aufgenommen wird, wird gleich diese OutputPipe geöffnet, um bei Bedarf schnell zu Verfügung zu stehen.

 

Das Überprüfen der Verfügbarkeit eines Peers wird dadurch erreicht, dass eine Statusanfrage an den Peer gesendet wird. Diese wird im Normalfall mit einer Antwort quittiert. Um die Implementierung zu vereinfachen, wird gleichzeitig nur auf die Antwort jeweils eines Peers gewartet. Erst wenn die Antwort von diesem eingetroffen oder die Wartezeit abgelaufen ist, wird eine Anfrage an den nächsten Peer geschickt. Gewährleistet wird dieses Verhalten durch Variablen, in denen die Namen des befragten und antwortenden Peers gehalten werden und durch Threadlocking. Falls ein Peer nicht innerhalb der gegebenen Wartezeit antwortet, wird er durch die Methode removePeer() aus der Liste entfernt und alle anderen Peers darüber benachrichtigt. Vor dem Versand jeder Message in der Methode sendOutMessage() wird die eigene IP-Adresse abgefragt. Hiermit wird festgestellt, ob man selbst noch eine Netzwerkverbindung hat. Somit kann der Anwender über eine fehlerhafte Verbindung informiert werden und Probleme werden vermieden. Schließlich darf es nicht vorkommen, dass ein Peer, dessen Netzwerkverbindung kurz gestört war, aufgrund fehlender Rückmeldungen andere Peers als offline deklariert, diese aus der Peerliste entfernt und entsprechende Informationsmeldungen verschickt.

 

Für die versendeten Messages wurde ein simples Protokoll entwickelt. Jede Message besteht aus mehreren MessageElements. Auf jeden Fall vorhanden ist das Element mit Bezeichnung "Type". Aufgrund dieser Typbezeichnung ergeben sich die weiteren benötigten Elemente der Message und die Art, wie mit der Message beim Empfänger umgegangen werden muss. Eine Auflistung der Typen, der Elemente und deren Bedeutung befindet sich im Anhang A3.

 

JXTA bietet deutlich mehr Möglichkeiten, die für den Messenger in Frage kommen würden, jedoch in dieser Version aus zwei Gründen nicht eingesetzt wurden. Einerseits erhöhen manche Funktionalitäten die Komplexität der Applikation enorm. Andererseits wurden viele Aspekte nicht genutzt oder sogar unterdrückt, um die gleichen Funktionalitäten wie die RMI- Umsetzung zu erreichen.

 

Ohne ein Löschen des Caches beim Programmstart wäre es möglich, eine Liste aller Peers zu erhalten, die je an dem Messenger-Netzwerk teilgenommen hatten. Somit wären auch User aufgelistet, die im Moment offline sind, was zur Folge hätte, dass eine persistente Zuordnung von Nickname zu User möglich wäre.

Durch Verwenden bekannter Rendevousserver im Internet wäre es auf Basis des bestehenden weltweiten JXTA-Netzwerkes möglich, die Applikation über das lokale Netzwerk hinaus zu betreiben.

Eine manuelle Einstellung der verwendeten Transportprotokolle und der JXTA-Core-Services ließe feinere Konfigurationen zu, durch die die Kommunikation gegebenenfalls noch optimiert werden könnte.

JXTA bietet weiter ein Lifetime-Konzept, mit dem die Gültigkeit veröffentlichter Advertisements festgelegt werden kann. Es müsste untersucht werden, ob dessen Einsatz Vorteile bringen würde.

Eine intensivere Nutzung der Peergroups böte die Möglichkeit, geschlossene Gruppen mit eingeschränktem Zutritt zu nutzen. In Kombination hierzu würde der Einsatz eines verschlüsselten Nachrichtenaustausches Sinn machen.

Nach oben


3.2.4 RMI


Als zweite Möglichkeit wurde die Netzwerkkommunikation des oben vorgestellten p2p-Chats mittels RMI umgesetzt. Obwohl RMI vom Aufbau her mehr auf die Kommunikation über einen oder mehrere zentrale Server ausgelegt ist, ließ sich die Kommunikationsschicht auch ohne eine dauerhafte zentrale Instanz umsetzen.

Zunächst wurde der Ansatz verfolgt, dass immer ein Peer, der sich gerade im Netzwerk befindet, die Rolle des zentralen Servers übernimmt. Meldet sich dieser Peer aus dem Netzwerk ab, muss ein neuer Peer gewählt werden, der die Serverfunktionen übernimmt. Dabei stellt sich aber die Frage, was passiert wenn dem Peer mit Serverfunktion, aus welchen Gründen auch immer, keine Netzwerkverbindung mehr zur Verfügung steht und dieser sich nicht abmelden kann? Versuche den Server zu kontaktieren würden dann ins Leere laufen oder Fehler verursachen. Um dieses Problem zu lösen, müsste immer mindestens ein Ersatzserver existieren, der aktiv wird, wenn der Ausfall des Primärservers festgestellt wird. Es wäre also ein Ausfallsmechanismus zu implementieren, der festlegt, wer der Ersatzserver ist und wann er ihn aktiviert. Außerdem muss ein neuer Ersatzserver gewählt werden, alle Peers davon in Kenntnis gesetzt werden und es muss gewährleistet sein, dass der Ersatzserver zu jedem Zeitpunkt die gleichen Informationen wie der Primärserver zur Verfügung hat. Da durch diese Variante aber alle Peers zu potentiellen Servern werden, kann man die Implementierung durch den im Folgenden beschriebenen Ansatz einsparen.

Die zweite und auch umgesetzte Variante setzt alle im Netzwerk befindlichen Peers gleich. Das heißt jeder Peer besitzt nach dessen Initialisierung alle benötigten Informationen, um mit allen im Netzwerk befindlichen Peers direkt, also tatsächlich peer-to-peer, zu kommunizieren. Um an diese Informationen zu gelangen, muss ein Peer während seiner Initialisierung einen anderen, bereits initialisierten Peer ausfindig machen und die benötigten Daten anfordern.

In der im letzten Abschnitt zusammenfassenden Darstellung der Initialisierung ist sehr abstrakt von benötigten Informationen oder auch Daten die Rede. Konkret handelt es sich um jeweils ein Server-Objekt pro im Netzwerk vorhandenen Peer, über das die direkte Kommunikation mit dem entsprechendem Peer ermöglicht wird. Dieses Objekt entspricht bei der Implementierung einer Instanz der Klasse Client (im Folgenden Client-Objekt genannt), die das ClientInterface implementiert, welches das Interface Remote erweitert und in dem alle benötigten Methoden für die Kommunikation deklariert sind.

Wie kommt aber ein neuer Peer an diese Client-Objekte? Hierfür wird beim Start der Applikation auch die RMI-Registry auf einem festgelegten Port aus dem Programm heraus gestartet. Dort wird ein Remote-Objekt registriert, das später genutzt wird, um an alle Client-Objekte zu gelangen. Die zu diesem Remote-Objekt gehörende Klasse heißt Server und bietet eine Remote-Methode getClients(), die eine Map zurück gibt, in der jeder Nickname seinem Client-Objekt zugeordnet ist.

Da RMI aber keine Möglichkeit bietet, ohne Angabe einer IP-Adresse eine im Netzwerk laufende Registry ausfindig zu machen, muss hierfür auf die oben beschriebene Socket- und Multicasttechnik zurück gegriffen werden. Der suchende Peer sendet an eine fest definierte Multicastgruppe seine IP-Adresse und wartet über einen ServerSocket auf eine Antwort. Fertig initialisierte Peers warten in einem separaten Thread auf ankommende Nachrichten der Multicastgruppe und senden ihre IP-Adresse an die empfangene IP-Adresse über einen Socket zurück. Diejenige IP-Adresse, die der suchende Peer als erstes empfängt, nutzt er, um wie im Theorieteil beschrieben ein Registry-Objekt des gefundenen Peers zu bekommen und sich dort eine Referenz auf das registrierte Server-Objekt zu holen. Da Multicasts keine zuverlässige Kommunikation bieten (siehe Kapitel 3.2.2) werden über einen bestimmten Zeitraum mehrere Multicasts gesendet. Erhält der Peer in diesem Zeitraum keine Antwort auf seine Multicasts, ist davon auszugehen, dass er der erste Peer im Netzwerk ist und somit auch keine Client-Objekte benötigt. Die Socketkommunikation erfolgt über die in [kredel] ausgearbeiteten Klassen SocketChannel und ChannelFactory.

Nachdem sich der Peer über das Server-Objekt alle Client-Objekte geholt hat, trägt er bei jedem Peer über das entsprechende Client-Objekt sein eigenes ein, dass auch die anderen Peers mit ihm kommunizieren können. Damit ist die Initialisierung eines Peers abgeschlossen.

Bei der Initialisierung ist besonders der Fall zu betrachten, wenn sich zwei Peers gleichzeitig oder sehr zeitnah anmelden. Handelt es sich hierbei um die ersten beiden Peers im Netzwerk, besteht die Gefahr, dass zwei getrennte Systeme entstehen könnten, weil beide keine Antwort auf ihre Multicasts bekommen. Ist mindestens schon ein initialisierter Peer im Netzwerk vorhanden, würde dieser zwar die Liste mit allen Client-Objekten an die neuen Peers schicken, aber die anmeldenden Peers wären noch nicht enthalten, weil sich diese erst, nachdem sie die Client-Objekte haben, bei diesen Clients eintragen können. Das hieße, dass jeweils die sich neu anmeldeten Peers nichts voneinander wissen würden. Dieses Problem wurde gelöst, indem ein Peer, nachdem er sich bei den anderen Peers eingetragen hat, erneut einen Server sucht und die dann erhaltenen Client-Objekte mit seinen bereits gefundenen abgleicht. Durch den Abgleich wird auch gewährleistet, dass sich nicht zwei Peers gleichzeitig mit demselben Nickname anmelden können.

 

Wie in Kapitel 2.2.2 beschrieben, nutzt die Applikationslogik ein definiertes Interface um mit der Kommunikationsschicht zu kommunizieren. Dieses Interface wird von der Klasse RmiCommunicator implementiert, die im Konstruktor eine Instanz der Klasse Client erzeugt. Diese Klasse stellt dem RmiCommunicator Funktionalitäten zur Verfügung, die er für die Implementierung des Communicator-Interfaces benutzt. Im Folgenden wird aber nicht detailliert auf die Funktionen und Implementierung der einzelnen Methoden eingegangen (diese sind bzw. ist dem Javadoc bzw. dem Quellcode einschließlich Kommentaren zu entnehmen), sondern nur auf grundsätzliche Überlegungen bei der Implementierung.

 

Schließt ein Peer seine Applikation, regelt die shutDown() Methode, dass er bei allen anderen Peers gelöscht und aus der Peerliste genommen wird. Stürzt aber ein Peer ab oder hat keine Netzwerkverbindung mehr, bekommen dies die anderen Peers nicht mit. Sie würden dann unnötige Client-Objekte verwalten und der User würde nicht mitbekommen, dass ein bestimmter Peer nicht mehr vorhanden ist. Dieses Problem wurde mit Hilfe der RemoteException gelöst. Diese tritt immer dann auf, wenn bei einem entfernten Client eine Remote-Methode nicht aufgerufen werden kann. Ist dies der Fall, wird die Exception in einem catch-Block abgefangen und ein Vorgang gestartet, der den kontaktierten Client aus der eigenen und aus der Peerliste aller noch im Netz befindlichen Clients löscht. Um sicherzustellen, dass keine RemoteException ausgelöst wird, weil der aufrufende Peer der Remote-Methode keine Netzwerkverbindung hat, wird diese vor jedem Aufruf getestet und gegebenenfalls die Methode nicht aufgerufen.

Ein weiterer Punkt ist, dass aufgerufene entfernte Methoden sehr lange blockieren können bis sie eine RemoteException werfen und dieser Zeitraum nicht festgelegt werden kann. Um die Ausführungszeit von Methoden, in denen eine Remote-Methode aufgerufen wird, zu verkürzen, wird dieser Aufruf generell in einen separaten Thread ausgelagert.

 

Bei der Implementierung der Kommunikation mittels RMI wurde davon ausgegangen, dass innerhalb des Netzwerks jegliche Kommunikation der Applikation zugelassen wird. Das heißt ins besondere, auf den Clients installierte Firewalls müssen so konfiguriert werden, dass sie weder ausgehende noch ankommende Pakete des Programms blocken. Des Weiteren wird ein stabiles Netzwerk vorausgesetzt, wenn auch bei der Implementierung kurzfristige Ausfälle des Netzwerks teilweise berücksichtigt wurden.

Nach oben


3.2.5 Jini


Im Folgenden wird auf eine theoretisch realisierbare Implementierung eingegangen und anschließend erörtert, warum sich Jini in der Praxis für eine reine p2p-Umgebung (siehe Kapitel 2.1.2) nicht eignet.

Eine Implementierung unter Ausnutzung von Jini würde vom Aufbau der Umsetzung mit RMI sehr ähnlich sein, weil beide grundsätzlich einen oder mehrere Server verwenden, um dort Objekte zu registrieren. Als Pendant zur RMI-Registry verwendet Jini den oben beschriebenen Lookup-Service. Dieser wird analog zur Umsetzung mit RMI aus dort beschriebenen Gründen zu Beginn auf jedem Peer gestartet.

 

Jeder Peer muss seinen Dienst bei allen Lookup-Services im Netz registrieren. Hierfür wird eine Klasse geschrieben, die einen DiscoveryListener implementiert. Dieser Listener stellt zwei Methoden bereit. Eine wird aufgerufen, wenn ein Lookup-Service gefunden wird, die andere, wenn ein Lookup-Service entfernt werden soll. Diese Klasse wird einem LookupDiscovery-Objekt hinzugefügt, welches das Aufrufen der Listener-Methoden übernimmt. Die Methoden werden so implementiert, dass der Dienst sich bei neu gefundenen Lookup-Services registriert und zu entfernende Lookup-Services aus seiner internen Liste nimmt. Letzteres wird benötigt, um eine aktuelle Liste mit Lookup-Services zu haben, bei denen der Dienst in regelmäßigen Abständen seinen Lease erneuern muss.

Die Spezifikation des Messengerdienst-Interfaces könnte genauso wie das ClientInterface bei RMI aussehen, mit der Ausnahme, dass dieses nicht die Klasse Remote erweitert. Dadurch ist das Interface generisch und legt nicht die auf Serverseite zu implementierende Kommunikationstechnik fest. Es wäre aber durchaus denkbar, dass die durch das Interface vorgegebenen Methoden genauso wie bei RMI implementiert werden.

 

Damit ein Peer auch in der Rolle als Konsument mit anderen Peers kommunizieren kann, benötigt er alle im Netz vorhandenen Dienste, die das oben genannte Dienstinterface implementieren. Um dies zu realisieren wird auch an dieser Stelle der DiscoveryListener verwendet. Diesmal wird die Methode, mit der neue Lookup-Services registriert werden, so implementiert, dass bei diesen der gewünschte Dienst angefordert wird. Es werden alle Server zurück geliefert, die den entsprechenden Messengerdienst implementieren. Die zweite Methode bleibt in diesem Fall leer.

 

Gegenüber RMI würde eine Implementierung mittels Jini zwei grundlegende Vorteile bringen. Jini unterstützt über sein Multicast-Discovery-Protokoll die Suche nach Lookup-Services innerhalb eines lokalen Netzes, ohne die Lokation angeben zu müssen. Bei RMI musste das Suchen nach dem Ort eines Servers selbst über Multicast- und Socketkommunikation implementiert werden. Der zweite Vorteil besteht darin, dass Jini durch seine Selbstheilungskräfte wesentlich besser mit einer schwankenden Netzwerkverbindung zurecht kommt. Musste unter RMI mit Hilfe von geworfenen Exceptions bei Remote-Aufrufen entschieden werden, ob ein Peer aus der Liste genommen wird, kann dies unter Jini sehr elegant über das Leasing-Konzept und über verteilte Events geregelt werden. Verlängert ein Peer bei einem Lookup-Service seinen Lease nicht rechtzeitig, wird sein Dienst entfernt. Durch den Mechanismus der verteilten Events kann jeder Peer von einem nicht mehr verfügbaren Dienst in Kenntnis gesetzt werden und entsprechende Maßnahmen einleiten. Dieses Prinzip funktioniert natürlich auch in umgekehrter Richtung, wenn ein Dienst neu hinzu kommt oder sich erneut nach einer etwaigen Netzwerkstörung anmeldet.

 

Das Problem bei einer Implementierung mit Jini ist das Starten des Lookup-Services. Jini definiert nicht welche Kommunikationstechnik ein Client verwendet um einen Dienst zu kontaktieren, sondern übergibt nur einen Proxy, der den Dienst repräsentiert. Deshalb ist es nötig einen http-Server zu starten, dass sich Clients eines Lookup-Services zur Laufzeit benötigte Klassen für die Kommunikation herunterladen können. Des Weiteren handelt es sich bei einem Lookup-Service um einen aktivierbaren Dienst, der sich bei einem RMI-Aktivierungungsdämon anmeldet. Das heißt, dass ebenfalls rmid gestartet sein muss. Bezogen auf die zu realisierende Applikation müssten also bei jedem Peer mindestens diese Voraussetzungen gegeben sein. Bei RMI konnte die Registry aus der Applikation heraus gestartet werden, ohne dass der Anwender dafür etwas tun musste. Bei Jini müsste alles über die Konsole oder über jeweils eine Batch-Datei in der richtigen Reihenfolge gestartet werden. Eine Batch-Datei zum Starten der gesamten Applikation wäre nicht möglich, weil die zu startenden Programme endlos laufen und aufeinander aufbauen. Aus letzterem Grund ist das Starten solcher Batch-Dateien aus Java heraus auch nicht möglich, weil keine Möglichkeit besteht, eine Rückmeldung zu bekommen, wenn zum Beispiel der Startvorgang des http-Servers abgeschlossen ist. Außerdem würde das starten über Batch-Dateien die durch Java erreichte Plattformunabhängigkeit einschränken.

Nach oben