Programmieren mit CORBA und C++

Martin Kompf

CORBA ist ein Mittel zur relativ komfortablen Realisierung verteilter Software. Dieses Tutorial beschreibt den Einstieg in die Verwendung von CORBA in C++ Programmen. Für die Programmbeispiele (Download) wird die freie CORBA Implementierung MICO verwendet.

Inhalt

Grundlagen

CORBA (Common Object Request Broker Architecture) ist ein Standard, der Mittel zur Kommunikation zwischen verschiedenen Prozessen beschreibt. Im Gegensatz zu den herkömmlichen Interprozess-Kommunikationsmethoden wie Sockets oder Memory Mapped Files enthebt CORBA den Softwareentwickler von der Aufgabe, sich um Details, wie Kommunikationsprotokolle und Datenformate, kümmern zu müssen. Stattdessen bietet CORBA die dem C++ Programmierer gewohnte Sicht auf Objekte, die über Methodenaufrufe miteinander kommunizieren.

Die CORBA Implementierung kümmert sich dann um das Konvertieren und Verpacken der Daten, die an die Methode übergeben worden, um das Verschicken des Methodenaufrufes über das Netzwerk an das Zielobjekt, um das dortige Entpacken und Rückkonvertieren der Daten und den eigentlichen Methodenaufruf auf dem Zielobjekt. Wenn die entsprechende Methode Daten an den Aufrufer zurückliefern soll, dann läuft das ganze nochmal in umgekehrter Richtung ab. Durch diesen - vor dem Anwender versteckten - Ablauf ist es ohne weiteres möglich, zum Beispiel in einem unter Linux laufenden Javaprogramm ein Objekt zu verwenden, welches in C++ implementiert wurde und auf einem Windows NT Rechner residiert.

CORBA ist nur ein Standard - keine Implementierung. Es gibt jedoch diverse Implementierungen dieser Architektur von den unterschiedlichsten Herstellern. Für die in diesem Tutorial enthaltenen Beispielprogramme wurde die Version 2.3.11 von MICO verwendet. MICO ist eine voll funktionsfähige und frei verfügbare CORBA Implementierung unter dem GNU Copyleft.

Die Benutzung von CORBA und MICO soll anhand einer kleinen verteilten Applikation zur Verwaltung von Telefonnummern demonstriert werden. Diese besteht aus einem Server, der Paare von Namen und dazugehöriger Telefonnummer speichert, und aus Clients, die zum Eintragen neuer Namen/Nummern sowie zur Recherche dienen. Als Entwicklungsumgebung dienten MICO 2.3.11 unter Linux mit dem gcc 3 sowie Windows 2000 mit dem Cygwin Toolkit 1.1. Bevor mit den Beispielen gearbeitet werden kann, muss MICO zunächst installiert werden. In vielen Linuxdistributionen wie Debian oder Suse ist eine MICO Entwicklungsumgebung als installierbares Paket enthalten. Alternativ kann man sich den Sourcecode downloaden, übersetzten und installieren.

Interfacedefinition

Der erste Schritt beim Erstellen einer CORBA Anwendung ist die Definition aller benötigten Interfaces. Über diese Interfaces tauschen Client und Server später ihre Daten aus. Die Interfacedefinitionen erfolgen dabei in einer besonderen Sprache, der Interface Definition Language (IDL). Diese ähnelt in ihrer Syntax stark einer C++ Klassendefinition. Die angestrebte Beispielapplikation kommt mit einem einzigen, einfachen Interface namens IPBook aus. Dieses enthält zwei Methoden: Eine zum Hinzufügen eines neuen Telefonbucheintrages und eine zum Suchen nach Namen. Die in IDL geschriebene Interfacedefinition wird in eine Datei IPBook.idl gespeichert und sieht folgendermaßen aus:

#ifndef _IPBook_idl
#define _IPBook_idl

/**
 * Interface for a very simple phone book
 **/
interface IPBook {
    /* Add an entry */
    void addEntry( in string name, in string number);
        
    /* Search for an entry */
    string searchEntry( in string name);
};

#endif

Der wichtigste Unterschied zu einer C++ Klassendefinition ist, dass bei den Methodenparametern die Richtung der Datenübergabe angegeben werden muss. Die verwendete Spezifikation in bedeutet, dass Daten ausschließlich vom Aufrufenden zum Objekt transportiert werden, analog bedeutet out dann einen Datentransport zurück zum Aufrufenden und inout einen bidirektionalen Datentransfer. Ein Returnvalue (wie string bei searchEntry) bedeutet natürlich immer, dass Daten zum Aufrufer zurück übertragen werden, out muss hier nicht angegeben werden.

Noch ein Wort zum generellen Design von CORBA Interfaces: Hier sollte man sich immer vor Augen halten, dass jeder Methodenaufruf eine Netzwerkübertragung ist! Gerade wenn nur wenige Bytes an Nutzdaten übertragen werden, entsteht ein beträchtlicher Overhead. Hüten muss man sich an dieser Stelle vor Theoretikern des objektorientierten Entwurfs, die alles und jedes in Objekte verpacken.

So wäre es in unserer Beispielapplikation vom objektorientierten Standpunkt durchaus sinnvoll, ein zusätzliches Interface Entry mit Methoden wie setName(), getName(), setNumber() und so weiter zu definieren. Die Funktion addEntry() in IPBook würde dann ein Entry Objekt als Parameter bekommen. Resultat dieses Designs wäre die dreifache Anzahl von entfernten Methodenaufrufen, die alle über das Netz gehen. Ein absoluter Performancekiller!

Beim Entwurf von CORBA Interfaces gilt also:

Implementierung des Serverobjektes

Nach der Erstellung der Interfacedefinition ist der nächste Schritt die Implementierung der Serverobjekte (Servants). Ein Serverobjekt muss einerseits die Applikationslogik implementieren - die Funktionen addEntry() und searchEntry() müssen mit Leben gefüllt werde. Andererseits muss das Serverobjekt auch die Verbindung zur CORBA Implementierung, genauer gesagt dem Object Request Broker (ORB), der für die Vermittlung der verteilten Funktionsaufrufe verantwortlich ist, herstellen. Für diese Verbindung gibt es verschiedene Objekt Adapter. Der ältere ist der Basic Object Adapter (BOA). Seit dem CORBA Standard 2.2 gibt es den umfangreicheren und flexibleren Portable Object Adapter POA. Obwohl MICO in der aktuellen Version beide Adapter zur Verfügung stellt, soll im folgenden ausschließlich auf die Verwendung des modernen POA eingegangen werden.

Bei der Implementierung der Serverobjekte nimmt der in MICO enthaltene IDL Prozessor dem Programmierer eine Menge Arbeit ab. Der Aufruf des IDL Prozessors erfolgt an der Kommandozeile, zuerst sollte man hier mittels

. /usr/local/lib/mico-setup.sh

die korrekte Umgebung einstellen (falls MICO im Verzeichnis /usr/local installiert wurde und die bash verwendet wird - ansonsten muss diese Zeile entsprechend modifiziert werden). Der Aufruf

idl --poa --c++-suffix cpp IPBook.idl

erzeugt aus der Interfacedefinitionsdatei IPBook.idl die C++ Dateien IPBook.h und IPBook.cpp.

Diese Dateien enthalten bereits einen großen Teil der Implementierung des Serverobjektes. Unter anderem stellen diese Dateien eine Klasse POA_IPBook zur Verfügung, die als Basisklasse für das Serverobjekt dient. Öffnet man mit einem Texteditor die Datei IPBook.h und sucht in ihr nach der Definition der Klasse POA_IPBook, so findet man unter anderem die Deklaration der pur virtuellen Funktionen

    virtual void addEntry( const char* name, const char* number ) = 0;
    virtual char* searchEntry( const char* name ) = 0;

Dies sind die Methoden, die in der Interfacedefinition spezifiziert wurden. Aus dem IDL Datentyp string ist jetzt nur der C++ Datentyp char* geworden, das IDL Attribut in findet seine Entsprechung in der const Spezifikation des Funktionsparameters.

Die vom Programmierer noch zu leistende Arbeit beschränkt sich nun darauf, eine neue Klasse zu schreiben, die von POA_IPBook abgeleitet ist und mindestens die beiden pur virtuellen Funktionen addEntry() und searchEntry() implementiert. Es hat sich eingebürgert, diese Klasse mit dem Nachsatz _impl zu benennen, im Beispiel also IPBook_impl. Die Definition der Klasse steht also in der Datei IPBook_impl.h:

#ifndef IPBook_impl_h
#define IPBook_impl_h 1

#include "IPBook.h"

#include <map>
#include <string>

using namespace std;

class IPBook_impl : virtual public POA_IPBook
{
public:
    // implement pure virtual functions from POA_IPBook
    virtual void addEntry( const char* name, const char* number );
    virtual char* searchEntry( const char* name );
    
private:
    map <string, string, less<string> > _numbers;
};

#endif

Neben den notwendigen Funktionen addEntry() und searchEntry() hat die Klasse noch die private Membervariable _numbers, die zur Aufnahme der Daten des Telefonbuches dient. Üblich sind an dieser Stelle auch die Definitionen eigener Konstruktoren und des Destruktors. Dieses relativ einfache Beispiel kommt jedoch mit den vom C++ Compiler zur Verfügung gestellten Standard-Konstruktor und -Destruktor aus.

Die eigentliche Implementierung der Klasse in der Datei IPBook_impl.cpp sollte nun komplikationslos über die Bühne gehen:

#include <CORBA.h>
#include "IPBook_impl.h"

using namespace std;

void IPBook_impl::addEntry( const char* name, const char* number)
{
    string nam = name;
    string num = number;

    _numbers[nam] = num;
}

char* IPBook_impl::searchEntry( const char* name )
{
    map <string, string, less<string> >::iterator r;
    r = _numbers.find( name);
    if (r != _numbers.end()) {
        return CORBA::string_dup( (*r).second.c_str());
    }
    else {
        return CORBA::string_dup( "???");
    }
}

Wichtig ist, zweierlei zu bedenken (bzw. zu wissen!): Die Übergabeparameter vom Typ char* belegen Speicherplatz auf dem Heap. Dieser wird vom Aufrufer der Funktion, also dem ORB, allokiert. Nach Beendigung der Funktion wird dieser Speicherbereich vom ORB wieder freigegeben. Dann dürfen im Serverobjekt keinerlei Referenzen mehr auf diesen Speicher existieren! Deshalb werden die Parameter name und number zunächst in die C++ Strings nam und num kopiert, bevor sie in die Map _numbers eingefügt werden (dabei wird dann zwar nochmal kopiert, jedoch liegen nam und num ja auf dem Stack und werden beim Verlassen der Funktion zerstört).

Zweitens gibt searchEntry() einen Zeiger auf einen Speicherbereich vom Typ char* an den ORB zurück. Damit hier keine Speicherlecks entstehen können, wurde im CORBA Standard festgelegt, dass der ORB für die Freigabe dieses Speicherbereiches verantwortlich ist. Somit muss der Programmierer beachten, dass er auf dem Heap liegenden Speicher zurückliefert. Dafür stellt CORBA die Funktion CORBA::string_dup() zur Verfügung.

Nun können die bisher erstellten Module kompiliert werden:

mico-c++ -c IPBook.cpp
mico-c++ -c IPBook_impl.cpp

erzeugt die Objektmodule IPBook.o und IPBook_impl.o. Um diese zum Leben zur erwecken, bedarf es noch der entsprechenden Server- und Clientapplikationen.

Die Serverapplikation

Zur Erstellung eines kompletten CORBA-Serverprogrammes fehlt nur noch die entsprechende main() Funktion. Diese wird sinnvollerweise in ein eigenes Sourcefile, zum Beispiel mit dem Namen cabsrv_co.cpp geschrieben. Zunächst werden die Headerfiles inkludiert - absolut notwendig sind hier das von MICO gelieferte CORBA.h sowie die Deklaration des Servants in IPBook_impl.h:

#include <CORBA.h>
#include "IPBook_impl.h"

#include <fstream>

using namespace std;

int main( int argc, char **argv)
{
    int rc = 0;

Als nächstes müssen der ORB (Object Request Broker) und ein POA-Manager (POA: Portable Object Adapter) erzeugt und initialisiert werden. Für dieses einfache Beispiel reicht es, den vom System zur Verfügung gestellten Root-POA-Manager zu verwenden. Alle CORBA Systemaufrufe werden in einem try-catch Block gekapselt:

    try {
        // init ORB and POA Manager
        CORBA::ORB_var orb = CORBA::ORB_init( argc, argv);
        CORBA::Object_var poaobj = orb -> resolve_initial_references("RootPOA");
        PortableServer::POA_var poa = PortableServer::POA::_narrow( poaobj);
        PortableServer::POAManager_var mgr = poa -> the_POAManager();

Nun kann der Servant erzeugt und aktiviert werden. CORBA stellt mehrere Aktivierungsmethoden zur Verfügung, hier wird die implizite Aktivierung verwendet:

        // create a new instance of the servant
        IPBook_impl *impl = new IPBook_impl;
        // activate the servant
        IPBook_var f = impl -> _this();

Jedes CORBA-Serverobjekt benötigt natürlich eine eindeutige Identikation, damit ein Client auch den »richtigen« Servant finden kann. Diese Identifikation wird als IOR (Interoperable Object Reference) bezeichnet. Um diese IOR vom Server zum Client transportieren zu können, gibt es mehrere Möglichkeiten. Das einfachste (und einzig portable) Verfahren ist, die IOR in eine Zeichenkette umzuwandeln und diese in einer Datei zu speichern, auf die Server und Client gleichermaßen Zugriff haben (in den nächsten Kapiteln wird noch ausführlich auf Alternativen dazu eingegangen):

        // save the stringified object reference to file
        CORBA::String_var s = orb -> object_to_string( f);
        ofstream out( "IPBook.ref");
        out << s << endl;
        out.close();

Nun sind alle notwendigen Vorbereitungen abgeschlossen, jetzt kann der POA Manager aktiviert und der ORB gestartet werden. Damit wird der Servant endgültig zum Leben erweckt:

        // activate POA manager
        mgr -> activate();
        // run the ORB
        orb -> run();

Aus diesem Funktionsaufruf kehrt das Programm normalerweise nicht zurück, es sei denn, dass ein explizites Shutdown des ORBs ausgeführt wird. In diesem Falle sollten dann noch POA-Manager und Servant korrekt aufgeräumt werden:

        poa -> destroy( TRUE, TRUE);
        delete impl;
        rc = 0;
    }

Der Rest der main() Funktion befasst sich nun noch mit dem Auffangen von Exceptions aus dem CORBA System und der Fehlerausgabe:

    catch(CORBA::SystemException_catch& ex)
    {
        ex -> _print(cerr);
        cerr << endl;
        rc = 1;
    }
    return rc;
}

Damit ist unsere erste CORBA-Serverapplikation fertig programmiert, sie kann nun mittels

mico-c++ -c cabsrv_co.cpp

übersetzt werden. Abschließend müssen die bisher entstandenen Objektmodule zusammen mit der MICO-Library gelinkt werden, um ein ausführbares Programm zu erhalten:

mico-ld -o cabsrv_co cabsrv_co.o IPBook.o IPBook_impl.o -lmico2.3.11

Jetzt kann cabsrv_co über die Kommandozeile gestartet werden. Wenn kein Fehler auftritt, läuft das Programm im Vordergrund ohne weitere Ausgabe. Um überhaupt etwas sinnvolles tun zu können, benötigt man noch Programme, die zumindest das Neuanlegen von Telefonbucheinträgen und eine Recherche darin erlauben.

CORBA Clients

Als erstes soll ein Programm erstellt werden, welches das Anlegen von Einträgen im Telefonbuch erlaubt. Eine Eingabe an der Kommandozeile in der Form

cabadd_co name nummer

soll einen Eintrag mit dem Namen name und der Nummer nummer dem Telefonbuch hinzufügen. Dazu wird die Programmdatei cabadd_co.cpp erstellt. Hier muss zunächst auch wieder CORBA.h inkludiert werden. Im Unterschied zum Serverprogramm darf hier aber die Deklaration des Servants nicht inkludiert werden, stattdessen wird das vom IDL-Preprozessor generierte Stubfile IPBook.h verwendet:

#include <CORBA.h>
#include "IPBook.h"

#include <fstream> 

using namespace std;

Im Hauptprogramm wird als erstes der ORB initialisiert und dann ein Test auf das Vorhandensein der Kommandozeilenargumente durchgeführt. Diese Reihenfolge ist sinnvoll, da es so möglich ist, zusätzliche Parameter für den ORB mit auf der Kommandozeile zu übergeben (einige dieser Parameter werden wir in den nächsten Kapitel noch kennenlernen). Die Funktion CORBA::ORB_init( argc, argv) wertet diese ORB-spezifischen Parameter aus und passt argc und argv entsprechend an. Der darauffolgende Test kann sich dann auf die Abfrage der programmspezifischen Kommandozeilenargumente beschränken:

int main( int argc, char **argv)
{
    CORBA::ORB_var orb = CORBA::ORB_init( argc, argv);

    int rc = 0;
    if (argc != 3) {
        cerr << "usage: " << argv[0] << " name number\n";
        exit(1);
    }

Das Clientprogramm benötigt außerdem die Information, welchen CORBA-Server es verwenden soll. Schließlich können in einer umfangreichen CORBA-Applikation durchaus mehrere hundert Servants ihren Dienst tun! Wir erinnern uns: Bei der Programmierung des Servers im vorhergehenden Kapitel wurde veranlasst, dass die IOR des Servants in eine Zeichenkette umgewandelt und in die Datei IPBook.ref geschrieben wird. Diese Datei wird nun vom Clientprogramm eingelesen:

    ifstream in( "IPBook.ref");
    char s[1000];
    in >> s;
    in.close(); 

Die »stringifizierte« IOR steht jetzt also in der Variablen s. Diese kann nun zur Erstellung eines Objektes vom Typ IPBook_var verwendet werden. Zum Abfangen eventueller Fehler bei diesem Vorgang wird ein try-catch Block geöffnet:

    try {
        CORBA::Object_var obj = orb -> string_to_object( s);
        IPBook_var f = IPBook::_narrow( obj); 

Mit dem nun vorliegenden Objekt f vom Typ IPBook_var können sämtliche Operationen durchgeführt werden, die ursprünglich in der Interfacedefinition IPBook.idl festgelegt worden sind. Das CORBA-Laufzeitsystem und unsere Vorarbeiten bewirken nun, dass alle auf dem Objekt f (dem Clientstub, Stub = Stummel) ausgeführten Operationen automatisch auf den Servant, das heißt auf das im Adressbereich des Serverprogramms cabsrv_co liegende Objekt vom Typ IPBook_impl weitergeleitet werden. Logischerweise muss das Serverprogramm also schon gestartet sein, bevor der Client aufgerufen werden kann (es gibt zwar die Möglichkeit, per Implementation-Repository eine Serveraktivierung on demand auszuführen, jedoch geht dies über die Thematik dieser »Einführung« in CORBA hinaus).

Wir rufen jetzt also die Methode add mit den beiden Argumenten name und nummer von der Kommandozeile auf:

        f -> addEntry( argv[1], argv[2]); 

Der Rest des Programmmes ist dem Auffangen von Exceptions und der Ausgabe ihrer Ursache gewidmet:

    }
    catch(CORBA::SystemException_catch& ex)
    {
        ex -> _print(cerr);
        cerr << endl;
        rc = 1;
    }

    return rc;
}

Als zweites benötigen wir noch ein Programm, um nach Namen im Telefonbuch suchen zu können. Die Eingabe

cabsearch_co name

soll die zu name gehörende Telefonnummer vom Server holen. Der Quelltext des Programmes (cabsearch_co.cpp) sei hier kommentarlos angegeben, mit den bisher gewonnenen Erkenntnissen sollte seine Interpretation problemlos möglich sein:

#include <CORBA.h>
#include "IPBook.h"

#include <iostream>
#include <fstream>

using namespace std;

int main( int argc, char **argv)
{
    CORBA::ORB_var orb = CORBA::ORB_init( argc, argv);

    int rc = 0;
    if (argc != 2) {
        cerr << "usage: " << argv[0] << " name\n";
        exit(1);
    }

    ifstream in( "IPBook.ref");
    char s[1000];
    in >> s;
    in.close();
    
    try {
        CORBA::Object_var obj = orb -> string_to_object( s);
        IPBook_var f = IPBook::_narrow( obj);

        cout << f -> searchEntry( argv[1]) << endl;
    }
    catch(CORBA::SystemException_catch& ex)
    {
        ex -> _print(cerr);
        cerr << endl;
        rc = 1;
    }

    return rc;
} 

Jetzt können die Clientprogramme übersetzt und gelinkt werden:

mico-c++ -c cabadd_co.cpp
mico-c++ -c cabsearch_co.cpp

mico-ld -o cabsearch_co cabsearch_co.o IPBook.o -lmico2.3.11
mico-ld -o cabadd_co cabadd_co.o IPBook.o -lmico2.3.11

Logischerweise braucht das Servant-Objekt IPBook_impl.o im Gegensatz zum Serverprogramm hier nicht dazugelinkt werden.

Nun können wir unsere erste vollständige CORBA-Applikation testen:

$ cabsrv_co &
$ cabadd_co Kurt 123
$ cabadd_co Heinz 445-566
$ cabsearch_co Kurt
123
$ cabsearch_co Moni
???
$ iordump < IPBook.ref
...

Ein Wermutstropfen bleibt: Damit Client und Server sich finden können, musste die Objektreferenz (IOR) des Servants in einer Datei gespeichert werden. Sowohl Client- als auch Serverprogramm benötigen Zugriff auf diese Datei. Solange beide Programme auf einem Rechner laufen, sollte dies kein Problem sein. Was aber, wenn dabei Rechnergrenzen zu überwinden sind? Eventuell könnte man sich noch mit der Einrichtung von Netzlaufwerken (z.B. SAMBA- oder NFS-Shares) behelfen, jedoch wird man hier bald an administrative Grenzen stoßen. Daher werden in den folgenden zwei Kapiteln Methoden vorgestellt, um Objektreferenzen auf andere Art und Weise zu transportieren: Die Verwendung des properitären MICO-Binders und die Einbeziehung des CORBA Naming Service.

Der MICO-Binder

Die CORBA-Implementierung MICO stellt als Hilfe bei der Lokalisation von Serverobjekten den MICO-Binder zur Verfügung. Diesem liegt folgende Überlegung zugrunde: Um einen Servant in einem Netzwerk eindeutig indentifizieren und auffinden zu können, bedarf es nicht unbedingt einer IOR. Alternativ ist es möglich, einen Servant durch seine Netzwerkadresse zu bezeichnen. Netzwerkadressen bestehen in einem TCP/IP Netzwerk aus der IP-Adresse des Rechners (oder seinem Namen) und einer Portnummer. Da IP-Adressen und Hostnamen zentral vergeben werden, ist damit die weltweite Eindeutigkeit der Kombination IP-Adresse:Portnummer gewährleistet. Falls ein CORBA-Serverprogramm mehrere Objekte der Außenwelt zur Verfügung stellt, können diese durch den Typ des Servants (genauer gesagt durch die Repository-Id der Interfacedefinition) unterschieden werden. Wenn dann noch mehrere Servants des gleichen Typs existieren, dann muss der Programmierer diesen noch einen (je Serverprogramm) eindeutigen Namen verpassen.

Da unser in im vorletzten Kapitel erstellter Telefonbuchserver cabsrv_co nur ein ein einziges CORBA-Objekt noch außen liefert, kann das Programm ohne Veränderung für die Benutzung des MICO-Binders verwendet werden. Es muss lediglich sichergestellt werden, dass der Servant beim Starten immer die gleiche Portnummer benutzt. Dies erfolgt durch Angabe der Kommandozeilenoption -ORBIIOPAddr inet:hostname:port. Ohne diese Option würde sich der Server bei jedem Start an irgendeine freie Portnummer binden. Der Aufruf

cabsrv_co -ORBIIOPAddr inet:munzel:4500

bewirkt also, dass der Server auf Port 4500 des Rechners munzel auf Verbindungen wartet. Wir müssen natürlich munzel mit dem Hostnamen des Rechners ersetzen, auf dem unser Server wirklich läuft. Der Hostname lässt sich mit dem Kommando

hostname

bestimmen.

Um mit unseren beiden Clients den MICO-Binder verwenden zu können, sind noch einige Änderungen an deren Sourcecode nötig. Im wesentlichen muss der Aufruf von string_to_object(stringified ior) durch bind(repository id, address) ersetzt werden. Die Adresse sollte dabei als Kommandozeilenargument übergeben werden, um einfach zwischen verschiedenen Servern umschalten zu können. Exemplarisch sei im folgenden das modifizierte Programm cabadd_co.cpp, welches wir jetzt cabadd_mi.cpp nennen, vorgestellt.

Zunächst erfolgt wieder das Inkludieren der notwendigen Header und der Test auf das Vorhandensein der Kommandozeilenargumente. Es muss jetzt als viertes Argument die Adresse des CORBA-Servers mit übergeben werden:

#include <CORBA.h>
#include "IPBook.h"

#include <iostream>

using namespace std;

int main( int argc, char **argv)
{
    CORBA::ORB_var orb = CORBA::ORB_init( argc, argv);

    int rc = 0;
    if (argc != 4) {
        cerr << "usage: " << argv[0] << " name number server_address \n";
        exit(1);
    } 

Nun kommt die entscheidende Stelle: Anstelle der Verwendung von string_to_object() wird direkt bind() aufgerufen. Der erste Parameter ist die Repository-ID unseres Servants, der zweite die per Kommandozeile übergebene Netzwerkadresse des Servers:

    try {
        CORBA::Object_var obj = orb -> bind( "IDL:IPBook:1.0", argv[3]);
        if (CORBA::is_nil( obj)) {
            cerr << "no object at " << argv[3] << " found.\n";
            exit(1);
        }
        IPBook_var f = IPBook::_narrow( obj); 

Falls alles gut gegangen ist, haben wir jetzt wieder ein IPBook_var Objekt, mit dem alle Methoden des Interfaces IPBook ausgeführt werden können. Der verbleibende Rest des Programmes bringt daher nicht Neues:

        f -> addEntry( argv[1], argv[2]);
    }
    catch(CORBA::SystemException_catch& ex)
    {
        ex -> _print(cerr);
        cerr << endl;
        rc = 1;
    }

    return rc;
} 

Die Änderungen am Programm cabsearch sind analog und daher ohne besondere Beschreibung nachzuvollziehen.

Die Programme werden wie bekannt übersetzt und gelinkt. Falls der Server wie oben angegeben an Port 4500 gebunden wurde, so sollte jetzt der Aufruf von

$ cabadd_mi Bimbo 123 inet:munzel:4500
$ cabsearch_mi Bimbo inet:munzel:4500

die erwartete Ausgabe 123 bringen.

Wer in der glücklichen Lage ist, mehrere über TCP/IP verbundene Rechner zu besitzen, kann jetzt auch Server und Client auf verschiedenen Rechnern starten. Dabei muss nur gewährleistet sein, dass die Auflösung des Hostnamen munzel auf allen beteiligten Rechnern funktioniert.

Die Verwendung des MICO-Binders ermöglicht es dem Anwender, auf den Transport von Objektreferenzen zu verzichten und stattdessen Netzwerkadressen zur Lokalisierung eines CORBA-Servers zu verwenden. Diese Methode dürfte für die meisten Anwendungen vollkommen ausreichend sein; jedoch hat sie noch zwei Nachteile:

In diesen Situationen hilft die Verwendung des CORBA-Naming-Service weiter.

Der CORBA Naming Service

Der Naming Service ist ein Standard CORBA Dienst, der für die einfache Verwaltung einer Vielzahl von Objektreferenzen geeignet ist. Er dürfte in den meisten Implementierungen, wie auch im MICO, zur Verfügung stehen. Der Naming Service ist ein hierachisches Verzeichnis, in dem es Namenskontexte und Namenseinträge gibt. Einen Namenskontext kann man sich als eine Art Unterverzeichnis vorstellen. Ein Namenseintrag enthält die IOR eines konkreten CORBA-Objektes. Sowohl Namenskontexte als auch Namenseinträge haben einen symbolischen Namen, mittels dessen das Objekt eindeutig lokalisiert werden kann:

Die Clientprogramme brauchen somit nur noch den Namen des Objektes, welches sie benutzen wollen, sowie die Adresse des Naming Service zu kennen. Letztere kann per Kommandozeilenparameter -ORBNamingAddr inet:hostname:port an die Anwendung übergeben werden.

Allerdings muss der Softwareentwickler in Server- und Clientapplikation einige Modifizierungen vornehmen, damit die Objektreferenzen über den Naming Service aufgelöst werden können. Schauen wir uns zunächst den Serverquellcode cabserv_ns.cpp an. Die ersten Zeilen sind bis zur Aktivierung des Servants sind fast identisch mit dem bereits erstellten Programm cabserv_co.cpp, es muss nur zusätzlich die Headerdatei CosNaming.h inkludiert werden:

#include <CORBA.h>
#include <coss/CosNaming.h>

#include "IPBook_impl.h"

using namespace std;

int main( int argc, char **argv)
{
    int rc = 0;

    try {
        // init ORB and POA Manager
        CORBA::ORB_var orb = CORBA::ORB_init( argc, argv);
        CORBA::Object_var poaobj = orb->resolve_initial_references("RootPOA");
        PortableServer::POA_var poa = PortableServer::POA::_narrow( poaobj);
        PortableServer::POAManager_var mgr = poa->the_POAManager();

        // create a new instance of the servant
        IPBook_impl *impl = new IPBook_impl;
        // activate the servant
        IPBook_var f = impl->_this(); 

Nun muss der Rootkontext des Naming Service aufgelöst werden. Dazu wird zuerst mittels resolve_initial_references die per Kommandozeilenoption -ORBNamingAddr übergebene Objektreferenz auf den Naming Service besorgt. Anschliessend wird diese auf einen NamingContext »eingeengt«:

         // resolve the naming service
        CORBA::Object_var nsobj =
          orb->resolve_initial_references ("NameService");
        if (CORBA::is_nil( nsobj)) {
            cerr << "can't resolve NameService\n";
            exit(1);
        }
  
        // narrow the root naming context
        CosNaming::NamingContext_var nc =
          CosNaming::NamingContext::_narrow (nsobj); 

Ausgehend von diesem NamingContext (der ja der RootNamingContext ist), kann nun die gesamte Hierarchie des Naming Service durchlaufen werden. In diesem einfachen Beispiel wollen wir uns darauf beschränken, direkt den einfachen Namenseintrag »AddressBook« anzulegen. Dies erfolgt mittels der Funktion bind(). Diese wirft allerdings eine Exception, falls der Eintrag schon (von einem vorherigen Lauf des Programms) existiert. In diesem Fall muss dann rebind() verwendet werden:

        // create a name entry
        CosNaming::Name name;
        name.length (1);
        name[0].id = CORBA::string_dup ("AddressBook");
        name[0].kind = CORBA::string_dup ("");

        // bind or rebind the servant to the naming service
        try {
            nc->bind (name, f);
        }
        catch (CosNaming::NamingContext::AlreadyBound_catch &ex) {
            nc->rebind (name, f);
        } 

Der Rest des Programmes läuft wie gehabt, lediglich der Code zur Behandlung von Exceptions ist erweitert worden, damit Fehler bei der Angabe der Adresse des Naming Service abgefangen werden:

        // activate POA manager
        mgr->activate();
        // run the ORB
        orb->run();

        poa->destroy( TRUE, TRUE);
        delete impl;
    }
    catch(CORBA::ORB::InvalidName_catch& ex)
    {
        ex->_print(cerr);
        cerr << endl;
        cerr << "possible cause: can't locate Naming Service\n";
        rc = 1;
    }
    catch(CORBA::SystemException_catch& ex)
    {
        ex->_print(cerr);
        cerr << endl;
        rc = 1;
    }
    return rc;
} 

Beim Linken des Programmes muss mittels -lmicocoss2.3.11 die Library mit dem Zugriffsroutinen auf den Naming Service dazugebunden werden.

Der Code des Clients cabadd_ns.cpp ist analog an die Verwendung des Naming Service anzupassen, anstelle von bind() oder rebind() wird hier resolve() aufgerufen, um an den vom Server angelegten Namenseintrag heranzukommen:

#include <CORBA.h>
#include <coss/CosNaming.h>
#include "IPBook.h"

#include <iostream>

using namespace std;

int main( int argc, char **argv)
{
    // init ORB
    CORBA::ORB_var orb = CORBA::ORB_init( argc, argv);

    int rc = 0;
    if (argc != 3) {
        cerr << "usage: " << argv[0] << " name number\n";
        exit(1);
    }

    try {
        // resolve the naming service
        CORBA::Object_var nsobj =
            orb->resolve_initial_references ("NameService");
        if (CORBA::is_nil( nsobj)) {
            cerr << "can't resolve NameService\n";
            exit(1);
        }
        // narrow the root naming context
        CosNaming::NamingContext_var nc =
            CosNaming::NamingContext::_narrow (nsobj);

        // create a name component
        CosNaming::Name name;
        name.length (1);
        name[0].id = CORBA::string_dup ("AddressBook");
        name[0].kind = CORBA::string_dup ("");
  
        // resolve the name component with the naming service
        CORBA::Object_var obj = nc->resolve( name);

        // narrow this object to IPBook 
        IPBook_var f = IPBook::_narrow( obj);
        
        // work with IPBook
        f->addEntry( argv[1], argv[2]);
    }
    catch(CORBA::ORB::InvalidName_catch& ex)
    {
        ex->_print(cerr);
        cerr << endl;
        cerr << "possible cause: can't locate Naming Service\n";
        rc = 1;
    }
    catch(CosNaming::NamingContext::NotFound_catch& ex)
    {
        cerr << "Name not found at Naming Service\n";
        rc = 1;
    }
    catch(CORBA::SystemException_catch& ex)
    {
        ex->_print(cerr);
        cerr << endl;
        rc = 1;
    }

    return rc;
} 

Bevor die Programme nun ausprobiert werden können, muss als erstes natürlich der CORBA (bzw. MICO) Naming Service nsd gestartet werden. Damit dieser sich an einen bestimmten Netzwerkport bindet, wird dabei die bekannte Option -ORBIIOPAddr verwendet. Als offizielle Portnummer für den CORBA Namingserver hat die IANA 2809 festgelegt.

$ nsd -ORBIIOPAddr inet:munzel:2809 &
# munzel ist wieder mit dem korrekten Hostnamen zu ersetzen!

Nun können unsere Server- und Clientprogramme in Aktion treten; sie benötigen jedoch nun immer die Angabe der Adresse des (korrekten) Naming Service:

$ cabsrv_ns -ORBNamingAddr inet:munzel:2809 &
$ cabadd_ns Bimbo 123 -ORBNamingAddr inet:munzel:2809
$ cabsearch_ns Bimbo -ORBNamingAddr inet:munzel:2809
123

Zur Administration des Naming Service gibt es noch das Konsolenprogramm nsadmin:

$ nsadmin -ORBNamingAddr inet:munzel:2809
CosNaming administration tool for MICO(tm)
nsadmin> ls
        AddressBook

nsadmin> iordump AddressBook

    Repo Id:  IDL:IPBook:1.0
    ...

nsadmin> exit
$

Der Vorteil der Verwendung des Naming Service kommt erst dann voll zur Wirkung, wenn eine Vielzahl von CORBA Objekten eines »Frameworks« verwaltet werden sollen. Statt dann mit einer Unmenge von stringified IORs oder Netzwerkadressen zu arbeiten, reicht es nun aus, allen Anwendungskomponenten lediglich die Adresse des Namingservice mitzuteilen. Bei geschickter Strukturierung unter Verwendung von Namenskontexten kann eine Vielzahl von Objekten verwaltet werden.

Damit sind wir am Ende des CORBA Tutorials angelangt. Selbstverständlich konnte hier vieles nur angerissen werden oder wurde komplett weggelassen. Zur Vertiefung sei auch auf folgende Bücher verwiesen: