Programmierung verteilter Anwendungen mit RPC

Ferngesteuert

von Peter Wächtler


Verteilte Anwendungen sind heute keine Seltenheit mehr. In einem Client-Server-Umfeld sind sie sogar unabdingbar. Sei es nun ftp oder das Web - der Dienst besteht aus mindestens zwei Komponenten, die meist auf unterschiedlichen Rechnern laufen. Diese kommunizieren über ein Netzwerk, und die wohl meist eingesetzte Programmierschnittstelle sind die BSD-Sockets. Ein anderes Hilfsmittel ist RPC, die "remote procedure calls". Sie erlauben die Ausführung von Funktionen, die auf einem anderen Rechner implementiert sind.

Erfunden wurden die remote procedure calls (RPC) von Sun Microsystems, und dies bereits in den Achtzigerjahren. Sie sind heute integraler Bestandteil von DCE, dem "Distributed Computing Environment" der Open Group (ehemals Open Software Foundation) - einem Industriestandard, der festlegt, mit welchen Mitteln verteilte Anwendungen realisiert werden und wie sie interagieren. Regen Gebrauch machen aber auch andere Hersteller von Netzwerksoftware, z.B. erfolgt die Anmeldung des NT-Client for Netware mit Hilfe der RPC.

Hauptaufgabe der RPC ist es, dem Anwendungsprogrammierer die fehlerträchtige Programmierung der Kommunikation zwischen Client und Server zu vereinfachen bzw. komplett abzunehmen. Durch den Mechanismus der RPC und Zuhilfenahme einer maschinenunabhängigen Datenrepräsentation (XDR bzw NDR) ergibt sich für den Anwendungsentwickler kein Unterschied zwischen dem lokalen Aufruf einer Funktion im gleichen Adressraum und dem netzwerktransparenten entfernten Aufruf einer Funktion auf einem anderen Rechner durch RPC.

Mittel und Wege: Warum RPC ?

Die wohl meist eingesetzte Transporthilfe für InterNet-Dienste (http, ftp, smtp) sind die BSD-Sockets. Diese stellen eine Voll-Duplex-Verbindung her - vergleichbar mit einer Streams-Pipe unter SysV. Voll-Duplex bedeutet die Möglichkeit des gleichzeitigen Sendens und Empfangens - wie beim Telefonieren. Normale Unix Pipes sind eine Einbahnstraße (simplex): der Datenfluß funktioniert nur in eine Richtung - wie bei einem Telefax. Der Vollständigkeit halber sei noch duplex erklärt: Senden und Empfangen sind möglich, aber nicht gleichzeitig - analog zum Sprechfunkverkehr. Client und Server verwenden ein internes Protokoll um miteinander zu kommunizieren. Für Lehrzwecke gut geeignet ist das Simple Mail Transfer Protokoll SMTP (siehe Abbl. 1):

Abb.1: exemplarische Sitzung mit SMTP

peewee@picklock:/home/peewee > telnet localhost 25 
Trying 127.0.0.1...

Connected to localhost.
Escape character is '^]'.
220 picklock.fido.de Smail3.1.29.1 #1 ready at Fri, 2 Jan 98 13:28 MET

help

250-The following SMTP commands are recognized:
250-
250- HELO hostname - startup and give your hostname
250- MAIL FROM:<sender-address> - start transaction from sender
250- RCPT TO:<recipient-address> - name recipient for message
250- VRFY <address> - verify deliverability of address 
250- EXPN <address> - expand mailing list address
250- DATA - start text of mail message
250- RSET - reset state, drop transaction
250- NOOP - do nothing
250- DEBUG [level] - set debugging level, default 1 
250- HELP - produce this help message
250- QUIT - close SMTP connection
250-
250-The normal sequence of events in sending a message is to state the
250-sender address with a MAIL FROM command, give the recipients with
250-as many RCPT TO commands as are required (one address per command)
250-and then to specify the mail message text after the DATA command.
250 Multiple messages may be specified. End the last one with a QUIT.

VRFY peewee

250 peewee

VRFY otto

550 otto ... not matched: unknown user

quit

221 picklock.fido.de closing connection
Connection closed by foreign host.

Hier wird also jeweils ein Parser programmiert, der die ankommenden Eingaben zeilenweise untersucht und entsprechend reagiert. Üblich ist die Verwendung von lex und yacc (bzw. flex und bison), mit deren Hilfe man die Parser baut, die bei Erkennung von definierten Eingabemustern entsprechende Routinen anspringen. Diese Art der Programmierung ist stark an die bekannte Komandozeile (Shell) angelehnt - ich habe die Kommandos schließlich auch einfach eingetippt. Das dahinterliegende Muster ist aber immer ähnlich: es wird ein Befehl erteilt, der entweder eine Ausgabe, einen internen Statuswechsel oder eine Fehlermeldung erzeugt. Dieses "Programmiermodell" liegt auch den RPCs zu Grunde - mit einem Unterschied: der fehlerträchtige Parser entfällt !

rsh, rcp und rdist: etwa alles Mist?

Die sogenannten r-commands (rsh, rlogin, rcp ) von BSD haben nichts mit RPC zu tun - sie bedienen sich auch der BSD-Sockets. Die Authorisierung erfolgt entweder interaktiv und somit über die Dateien /etc/passwd bzw. /etc/shadow, über entsprechende Einträge in der ~/.rhosts oder auch über MIT-Kerberos (ab BSD 4.3-Reno) einem ticket-basiertem sicherem Authentisierungsverfahren. Weiterhin wird der Zugriff oft über den tcpd geregelt. Dieser sitzt zwischen inetd und dem angeforderten Service und überprüft anhand von Einträgen in /etc/hosts.deny und /etc/hosts.allow ob ein Zugriff erlaubt ist oder nicht. Da bei der interaktiven Authorisierung die Paßwörter im Klartext über das Netz geschickt werden, und IPv4 über keinerlei Verschlüsselungsmechanismen verfügt, ist das Ausspähen von Paßwörtern in einer solchen Umgebung extrem leicht - wie auch bei telnet, ftp und herkömmlichem pop3. Wer nicht gleich zu Kerberos greifen möchte hat mit ssh eine anerkannt sichere Alternative. Die Secure Shell [16] verschlüsselt den Datenstrom zwischen Client und Server. Ein Ausspähen von Paßwörtern (Schlüsseln) ist nahezu unmöglich. Dateien sollten mit scp übertragen werden, selbst X11-Sessions lassen sich mittels ssh verschlüsseln - ganz zu Schweigen von der Möglichkeit beliebige TCP/IP-Ports über ssh verschlüsseln zu lassen!

Komponenten einer verteilten Anwendung

Ein verteiltes RPC-Programmsystem besteht aus folgenden Komponenten:

  1. portmap: registriert RPC-Programmnummern und ordnet sie Portnummern/Transportdiensten zu (rpcbind bei TI-RPC, rpcd oder dced bei DCE oder RPC Service Locator bei eNTe )
  2. rpc-server: stellt über den portmap Prozeduren netzwerkweit zur Verfügung
  3. rpc-client: ruft mit Hilfe des portmap registrierte Prozeduren eines RPC-Servers auf

Der wohl bekannteste RPC-Dienst dürfte NFS (Network File System - ebenfalls von Sun) sein - daneben sind noch Dienste wie rstat, rusers und rwall häufig anzutreffen. Die Serverdienste setzen beim Start einen laufenden portmap vorraus, ohne den ein Client die augenblickliche Portnummer für diesen Dienst nicht ermitteln kann und mit einer Fehlermeldung abbricht - ein "beliebter" Fehler beim Aufsetzen von NFS (mount clntudp_create: RPC: Program not registered).

Ein RPC-Server oder Client beinhaltet den Anwendungscode, einen oder mehrere RPC-Rümpfe und die RPC-Laufzeitumgebung. Der Anwendungscode ist speziell für eine Anwendung vom Anwendungsprogrammierer entwickelt worden. Der Anwendungscode implementiert und ruft entfernte Prozeduren sowie evtl. weitere benötigte RPC-Laufzeitroutinen auf. Ein RPC-Rumpf ist ein schnittstellenabhängiges Modul, der die RPC Laufzeitumgebung nutzt um Argumente und Rückgabewerte auszutauschen. Server und Client haben sich ergänzende RPC Rümpfe für eine Schnittstelle, die sie selbst gemeinsam bilden. Die RPC-Laufzeitumgebung handhabt die Kommunikation für Anwendungen. Zusätzlich stellt sie eine Programmierschnittstelle (API) zur Verfügung, anhand derer sich Kommunikationsparameter einstellen, Informationen über Server manipulieren und weitere Aufgaben erledigen lassen - z.B. die entfernte Wartung von Servern oder Zugriff auf sicherheitsrelevante Informationen.


Abb.2: Schichtenmodell der RPC

Beim Start des Clients kontaktiert dieser erstmal den portmap auf dem angegebenen Rechner (im Falle von rwall sogar jeden Rechner im Subnetz) auf Port 111 (siehe /etc/services) und erfragt die Portnummer für ein bestimmtes Programm. So liefert rpcinfo auf dem Server Informationen über registrierte RPC-Programme:

Abb.3: Ausgabe von rpcinfo -p

$ rpcinfo -p 
program vers proto port
100000 2 tcp 111 portmapper
100000 2 udp 111 portmapper
100002 2 udp 1025 rusersd
100002 3 udp 1025 rusersd
100008 1 udp 1027 walld
100005 1 udp 1016 mountd
100005 2 udp 1016 mountd
100005 1 tcp 1019 mountd
100005 2 tcp 1019 mountd
100003 2 udp 2049 nfs
100003 2 tcp 2049 nfs
150001 1 udp 663 pcnfsd
150001 2 udp 663 pcnfsd
150001 1 tcp 666 pcnfsd
150001 2 tcp 666 pcnfsd
788585389 1 udp 622

In Abhängigkeit von bereits laufenden Diensten sieht die Zuordnung zwischen Programmnummer und Portnummer anders aus. Analog zu den "assigned numbers" für die IP-Dienste (/etc/services ) sind die RPC-Programmnummern unterteilt und können bei Sun - bei allgemeinem Interesse - registriert werden . Die Nummern sind in Gruppen von 0x20000000 unterteilt:

Tabelle 1: Aufteilung der RPC-Programmnummern
00000000 - 1fffffff von Sun vergeben
20000000 - 3fffffff von Benutzern zu vergeben
40000000 - 5fffffff vorrübergehend - für Benutzerprogramme
60000000 - 7fffffff reserviert
80000000 ... ffffffff reserviert

Varianten und Einsatzgebiete - Streifzug DCE

Im Laufe der Zeit haben die ursprünglichen "Sun-RPC" Erweiterungen erfahren. So sind für System V in Zusammenarbeit mit AT&T die TI (Transport Independant) RPC entwickelt worden, die das Transport Level Interface (TLI) von System V ausnutzen und somit in ihrer Wahl des Transportmittels flexibler sind - die ursprünglichen RPC-Programme setzen ihrerseits auf Sockets auf und müssen für ein neues Transportmittel neu übersetzt werden. Für DCE wurden die RPC-Services in den DCE-Namensdienst integriert bei dem sich die Server registrieren, damit der Client "von alleine" herausbekommt, welchen Rechner er zu kontaktieren hat. Dies geschieht hier über das Name Service Interface (NSI) innerhalb der Cell Directory Services (CDS).

DCE stellt Dienste und Werkzeuge zur Verfügung, die die Erstellung, Benutzung und Wartung von verteilten Anwendungen in heterogenen Umgebungen ermöglichen. Zu diesen Werkzeugen gehören DCE Threads, DCE Directory Services, DCE Distributed Time Service (DTS), DCE Security Service, DCE Distributed File Service (DFS) und als zugrundeliegendes Transportmittel die DCE-RPC. Der RPC-Dienst gliedert sich in Entwicklungswerkzeug (Sprache und Compiler) und Laufzeitumgebung (dced/rpcbind/portmapper sowie der Client-/Serverprogramme). Fast sämtliche Dienste von DCE bedienen sich des Mittlers RPC. Zur Programmierung existiert eine spezielle IDL (Interface Definition Language) und ein Compiler, der Rümpfe für Client und Server generiert.

Es sind also drei Abkömmlinge zu unterscheiden:

In Tabelle 2 ist der zeitliche Ablauf der einzelnen Abarbeitungsschritte in einer DCE-Umgebung dargestellt. Es fällt auf, daß der Aufruf bzw. die Ausführung der entfernten Prozedur nur einen Bruchteil der Arbeitsschritte ausmachen. Schritte 4 und 6 sind DCE-spezifisch. Bei den Sun-RPC, die ja transportabhängig sind, entfällt auch Schritt 1.

Tabelle 2: Grundlegende Aufgaben einer RPC Applikation

Client Tasks

Server Tasks


1. Auswahl des Netzwerkprotokolls

2. RPC Schnittstelle registrieren

3. Registration von Endpunkten in endpoint map

4 . Bekanntgabe der RPC Schnittstelle im Namensraum

5. Auf Anrufe warten (listen)
6. Suche nach kompatiblen Server, der gesuchte Dienste anbietet
7. Aufruf der entfernten Prozedur
8. Bindung zum Server etablieren
9. Konvertierung der Eingangsargumente in network data
10. Übertragung der Argumente zum Server

11. Anruf erhalten

12 . Aufbereitung der Argumente in lokale Datenrepräsentation

13. Ermittlung und Aufruf der geforderten Prozedur

14. Ausführung der entfernten Prozedur

15. Konvertierung der Ausgangsargumente und Rückgabewerte in network data

16. Übertragung der Ergebnisse zum Client
17. Erhalt der Ergebnisse
18. Aufbereitung der Rückgabewerte in lokale Datenrepräsentation
19. Rückgabe der Ergebnisse und Kontrolle an aufrufenden Programmkode

Zurück zur Praxis unter Linux: rpcgen

Rpcgen ist der Compiler, der aus der RPC Programmier Sprache (RPCL - bei DCE: IDL) C-Code für Client und Server generiert. Mit Hilfe der RPCL läßt sich das "Protokoll" zwischen Client und Server festlegen, dessen Funktionalität in anderen Quellcodemodulen implementiert wird. Eine vollständige Beschreibung der RPCL ist unter [1] zu finden. Mittels der RPCL werden entfernte Funktionen und deren Parameter deklariert. Dabei sorgt der RPC-Compiler für die Kodierung der Daten in eine unabhängige Datenrepräsentation (XDR bzw NDR) und erzeugt ein Modul, das die Umsetzung implementiert. Die Funktionalität von rpcgen geht also über einen reinen Präprozessor hinaus - auch im Vergleich zu einem Präprozessor für Embedded SQL.


Abb. 3: Allgemeine RPC Architektur

einfaches Sun-RPC Beispiel

Anhand eines simplen RPC-Programms, das ein Verzeichnis eines entfernten Rechners anzeigt ( rsh host ls ) kann man sich schon einen guten Eindruck über die Vorgehensweise machen. Listing 1 zeigt die Datei xdir.x , die mit rpcgen übersetzt werden muss. Rpcgen generiert daraus einen Stub (Rumpf) für Server und Client (dir_svc.c und dir_clnt.c ) , die Header-Datei dir.h und ein Modul dir_xdr.c, das für die Verpackung der zu transferierenden Daten zuständig ist. Die Programmiererin muss dann noch die eigentliche Programmfunktion für den Server ( dir_proc.c in Listing 2 ) und den Client ( rls.c in Listing 3 ) implementieren.Übersetzt wird das Ganze wie folgt:

$ rpcgen dir.x
$ cc -c dir_xdr.c
$ cc rls.c dir_clnt.c dir_xdr.o -o rls
$ cc dir_svc.c dir_proc.c dir_xdr.o -o dir_svc

Sind die Programmdateien dir_svc und rls auf zwei vernetzte Rechner kopiert worden, startet man den Server einfach mit

picklock:/\> dir_svc &

und kann von einem anderen Rechner den Inhalt von Verzeichnissen abrufen, z.B.:

jimmy:/\> rls picklock /tmp

Listing 1: dir.x - RPCL Definition

/* dir.x: Remote directory listing protocol (vereinfacht) */

const MAXNAMELEN = 255; /* maximale Dateinamenlaenge */ 
typedef string nametype<MAXNAMELEN>; /* ein Verzeichniseintrag*/ 
typedef struct namenode *namelist; /* Zeiger einer verketteten Liste */

/* Ein Eintrag im Verzeichnis */
struct namenode {
  nametype name; /* Name des Eintrages */ 
  namelist next; /* naechster Eintrag */ 
};

union readdir_res switch (int errno) { 
  case 0:
    namelist list; /* kein Fehler: Rueckgabe der Eintragsliste */ 
  default:
    void; /* Fehlerzustand: keine weitere Rueckgabe */ 
};

/* Programm Definition */

program DIRPROG {
  version DIRVERS {
      readdir_res READDIR(nametype) = 1;
  } = 1;
} = 0x20000076;

Listing 2: dir_proc.c - Definition der entfernt aufzurufenden Funktion

/* dir_proc.c: entfernte "readdir" Implementation */

#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <dirent.h>
#include "dir.h" /* wird von rpcgen generiert */ 

readdir_res * readdir_1_svc(nametype * dirname, struct svc_req *req) {
  DIR *dirp;
  struct dirent *d;
  namelist nl;
  namelist *nlp;
  static readdir_res res; /* muss statisch sein! */
  dirp = opendir(*dirname); /* Verzeichnis oeffnen */ 

  if (dirp != (DIR *)NULL) {
    /* voriges Ergebnis freigegeben (sonst: memory leaks) */ 
    xdr_free(xdr_readdir_res, &res);
    nlp = &res.readdir_res_u.list;
    res.errno = 0;
    while (d = readdir(dirp)) {
      nl = *nlp = (namenode *) malloc(sizeof(namenode)); 
      if (nl == (namenode *) NULL) {
        res.errno = EAGAIN;
        break;
      }
      nl->name = strdup(d->d_name);
      nlp = &nl->next;
    }
    *nlp = (namelist)NULL;
    closedir(dirp);
  } else
    res.errno = errno;

  return (&res);
}

Listing 3: Definition des Clients

/* rls.c: Remote directory listing client */

#include <stdio.h>
#include <errno.h>
#include "dir.h" /* wird von rpcgen generiert */ 

main(int argc, char *argv[]) {

  CLIENT *clnt;
  char *server;
  char *dir;
  readdir_res *result;
  namelist nl;
  if (argc != 3) {
    fprintf(stderr, "usage: %s host directory\n",argv[0]); 
    exit(1);
  }

  server = argv[1];
  dir = argv[2];
  clnt = clnt_create(server, DIRPROG, DIRVERS, "tcp"); 
  if (clnt == (CLIENT *)NULL) {
    clnt_pcreateerror(server);
    exit(1);
  }

  result = readdir_1(&dir, clnt);
  if (result == (readdir_res *)NULL) {
    clnt_perror(clnt, server); /* RPC schlug fehl */
    exit(1);
  }

  if (result->errno != 0) {
    errno = result->errno; /* Fehler beim Server */ 
    perror(dir);
    exit(1);
  }

  for (nl = result->readdir_res_u.list;nl != NULL; nl = nl->next) {
    printf("%s\n", nl->name);
  }

  xdr_free(xdr_readdir_res, result);
  clnt_destroy(clnt);
  exit(0);
}

Das Ganze mit DCE-RPC

Eher zur Anschauung denn als praktische Programmiereinführung folgt eine Implementierung des obigen Beispiels mit dem DCE-RPC Laufzeitsystem. Für Linux ist (noch) keine vollständige Portierung von DCE verfügbar. Unter [3] erhält man aber eine teilweise Portierung des DCE-RPC-Subsystems. Hier funktioniert u.a. der Transport über UDP nicht. Mir ist es außerdem nicht gelungen ein endpoint mapping über den rpcd vorzunehmen. In diesem Falle kann man das Beispiel aber dennoch über eine explizit angegebene Bindung testen. Außerdem soll noch erwähnt werden, daß die Programme auch einfacher über eine automatische Bindung zueinander gefunden hätten, einen funktionierenden Name Service vorrausgesetzt ... leider auch (noch) nicht für Linux verfügbar. Die ganzen Aufrufe um eine Bindung herzustellen kann komplett vom Laufzeitsystem übernommen werden. Hat man das Paket von [3] erfolgreich installiert, kann das Ganze mit

$ make

übersetzt werden. Über die Datei dir.acl lassen sich weitere Attribute des Interface verändern - in diesem Beispiel wird nur angegeben, daß Fehler über die Variable status zurückgegeben werden. Um einen eindeutigen Bezeichner für das entwickelte Interface zu generieren, benutzt man das Werkzeug uuidgen, das den Part vor interface in Listing 5 erstellt. Der Compiler idl erzeugt aus dir.idl und dir.acf die Dateien dir.h, dir_sstub.c , dir_cstub.c und kann diese auch gleich übersetzen lassen.

Würde der rpcd korrekt funktionieren und bereitsseinen Dienst verrichten, reichte ein Aufruf

$ ./dir_server
$ ./dir_client -s localhost /tmp

Leider muß ein explizites Binding angegeben werden. Dazu veranlaßt man den Server, sich nicht in der endpoint map einzutragen:

$ ./dir_server -e
dir RPC Server Starting...
BINDING: ncacn_ip_tcp:192.168.227.47[1206]
BINDING: ncadg_ip_udp:192.168.227.47[1072]

Jetzt kann man den Client mit folgenden Informationen zur Bindung aufrufen:

$ ./dir_client -p ncacn_ip_tcp -s 192.168.227.47 -# 1206 /tmp

Und tatsächlich erhält man eine Liste der Dateien im Verzeichnis /tmp.

Listing 4: DCE-Interface Attribut-Beschreibung dir.acf

/* Simple (dir) RPC application*/

/* Die ACF-Datei veranlaßt die RPC Laufzeitumgebung Fehler 
    in der Kommunikation und auf Seite des
    Servers ueber die status Variable anzuzeigen */

interface dir {
  dir_readdir([comm_status] status);
}

Listing 5: DCE-IDL Version dir.idl

/*
uuidgen -io dir.idl

Simple (dir) RPC application
*/

[
uuid(f9d944b6-a536-11d1-99f1-000000301490),
version(1.0)
]

interface dir
{
  const short MAXNAMELEN = 255;
  typedef [string] char nametype[MAXNAMELEN];
  typedef struct namenode_tag {
    nametype name;
    [ptr] struct namenode_tag *next; 
  } namenode;
  typedef [ptr] namenode *namelist;
  typedef union switch (long error) readdir_res_u {
    case 0:
      namelist list;
    case 1:
      [ptr] long *dummy;
  } readdir_res;

void dir_readdir(
  [in] handle_t h,
  [in] nametype dirname,
  [out] readdir_res **dir,
  [out] error_status_t *status
);

}

Listing 6: DCE-Version von dir_proc.c

/* dir_proc.c: entfernte "readdir" Implementation */

#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <dirent.h>
#include "dir.h" /* wird von idl generiert */ 

void dir_readdir(handle,dirname,dirlist,status) 

handle_t handle;
nametype dirname;
readdir_res **dirlist;
error_status_t *status;

{
  DIR *dirp;
  struct dirent *d;
  namelist nl;
  namelist *nlp;
  static readdir_res res; /* muss statisch sein! */
  dirp = opendir(dirname); /* Verzeichnis oeffnen */ 
  if (dirp != (DIR *)NULL) {
    *dirlist=NULL;
    nlp = &res.readdir_res_u.list; 
    res.error = 0;
    while (d = readdir(dirp)) {
      /* rpc runtime gibt den Speicher 
         nach der Uebertragung autom. frei */

      nl = *nlp = (namenode *) rpc_ss_allocate(sizeof(namenode)); 
      if (nl == (namenode *) NULL) {
        res.error = EAGAIN;
        break;
      }

      strncpy(nl->name,d->d_name,sizeof(nl->name)); 
      nlp = &nl->next;
    }

    *nlp = (namelist)NULL;
    closedir(dirp);
    *dirlist=&res;
  } else
    res.error = errno;
  return;
}

Listing 7: DCE-Server Code dir_server.c

/* Simple (dir) RPC application - server */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <dce/rpc.h>
#include <dce/dce_error.h>
#include "dir.h"

void print_error(char *caller, error_status_t status) {
  dce_error_string_t error_string;
  int print_status;
  dce_error_inq_text(status, error_string, &print_status); 
  fprintf(stderr, "%s: %s\n", caller, error_string); 
}

void usage(char *name) {
  printf("USAGE: %s [-d -f -e -h]\n"
  "\t-d = Turn ON debugging\n"
  "\t-f = Run in foreground\n"
  "\t-e = Turn OFF endpoint mapping\n" 
  "\t-h = this help message\n",name);
  exit(1);
}

int main(int argc, char **argv) {
  unsigned32 status;
  rpc_binding_vector_t *binding_vector;
  int i, foreground = 0, debug = 0, register_endpoint = 1; 
  unsigned_char_t *string_binding;
  extern int optind;
  extern char *optarg;
  while ((i=getopt(argc,argv,"dfeh"))!=EOF) {
    switch (i){
      case 'd': foreground++; debug++;break;
      case 'f': foreground++;break;
      case 'e': register_endpoint=0;break;
      default : usage(argv[0]);
    }
  }
  printf("dir RPC Server Starting...\n"); 

  /* INTERFACE registrieren */
  rpc_server_register_if(dir_v1_0_s_ifspec, NULL, NULL, &status); 
  if(status!=error_status_ok) {
    print_error("server main() calling rpc_server_register_if()", 
                 status);
    /* return 1; */
  }

/* Alle verfuegbaren Protokolle verwenden */

  rpc_server_use_all_protseqs(rpc_c_protseq_max_reqs_default, &status);
  if(status!=error_status_ok) {
    print_error("server main() calling rpc_server_use_all_protseqs()\n", 
                 status);
    return 1;
  }

/* Binding Handle erfragen: */

  rpc_server_inq_bindings(&binding_vector, &status); 
  if(status!=error_status_ok) {
    print_error("server main() calling rpc_server_inq_bindings()\n", 
                 status);
    return 1;
  }

/* Beim Endpoint mapper registrieren, falls gewuenscht */

  if(register_endpoint) {
    rpc_ep_register(dir_v1_0_s_ifspec, binding_vector, NULL, 
                    (unsigned_char_p_t)"dir Logging Server version 1.0", 
                    &status);
    if(status!=error_status_ok)
      print_error("server main() calling rpc_ep_register()", status);
  }

/* Normalerweise koennte man die Bindung an das CDS exportieren */

/* Bindung ausgeben */

  for(i=0; i < binding_vector->count; i++) {
    rpc_binding_to_string_binding(binding_vector->binding_h[i], 
                                  &string_binding, &status);
    if(status!=error_status_ok)
      print_error("server main() calling rpc_binding_to_string_binding()", 
                   status);
    printf("BINDING: %s\n", string_binding); 
    rpc_string_free(&string_binding, &status);
    if(status!=error_status_ok)
      print_error("server main() calling rpc_string_free()", status);
  }

/* falls gewuenscht, ab in den Hintergrund */

  if(!foreground) {
    setpgrp();
    if(fork()) exit(0);
  }

/* Auf reinkommende Rufe warten */

  rpc_server_listen(rpc_c_listen_max_calls_default, &status); 
  if(status!=error_status_ok) {
    print_error("server main() calling rpc_server_listen()", status);
    return 1;
  }

  printf("Server Shutting Down\n");
  return 0;
}

Listing 8: DCE-Client dir_client.c

/* Simple (dir) RPC application - client */

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <dce/rpc.h>
#include <dce/dce_error.h>
#include <dce/pthread_exc.h>
#include "dir.h"

void print_error(char *caller, error_status_t status) {
  dce_error_string_t error_string;
  int print_status;
  dce_error_inq_text(status, error_string, &print_status);
  fprintf(stderr, "%s: %s\n", caller, error_string);
}

void usage(char *name) {
  printf("USAGE: %s [-p -s -# -h]\n"
  "\t-p protocol_sequence\n"
  "\t-s server_ip_address\n"
  "\t-# port\n\t-h = this help message\n",name);
 exit(1);
}

int main(int argc, char **argv) {
  int i;
  handle_t binding_handle;
  unsigned_char_t *actual;
  error_status_t status;
  char *protocol = "ncacn_ip_tcp";
  char *serverip = "192.168.227.47";
  char *port = 0;
  readdir_res *dirlist;
  namelist nlp;
  extern int optind;
  extern char *optarg;
  setbuf(stdout, NULL);
  while ((i=getopt(argc,argv,"hp:s:#:"))!=EOF)
    switch (i){
      case 'p': protocol = optarg;break;
      case 's': serverip = optarg;break;
      case '#': port = optarg;break;
      default : usage(argv[0]);
  }

/* binding handle erzeugen */

  rpc_string_binding_compose(NULL, protocol, serverip,
  port, NULL, &actual, &status);
  if(status!=error_status_ok) {
    print_error("client main() calling rpc_string_binding_compose()\n",
                 status);
    return 1;
  }
  printf("Composed Binding: %s\n", actual);


/* binding handle umwandeln: */

  rpc_binding_from_string_binding(actual, &binding_handle, &status);
  if(status!=error_status_ok) {
    print_error("client main() calling rpc_binding_from_string_binding()",
                 status);
    return 1;
  }

/* string binding freigeben: */
  
  rpc_string_free(&actual, &status);
  if(status!=error_status_ok) {
    print_error("client main() calling rpc_string_free()", status);
    return 1;
  }

/* endpoint suchen, falls kein port angegeben wurde: */

  if(port==NULL) {
    printf("Finding PORT in the Endpoint map\n");
    rpc_ep_resolve_binding(binding_handle, dir_v1_0_c_ifspec, &status); 
    if(status!=error_status_ok) {
      print_error("client main() calling rpc_ep_resolve_binding()",
                   status);
      return 1;
    }

    /* in String zurueckwandeln, um endgueltige Bindung zu zeigen: */

    rpc_binding_to_string_binding(binding_handle, &actual,&status); 
    if(status!=error_status_ok) {
      print_error("client main() calling rpc_binding_to_string_binding()",
                   status);
      return 1;
    }

    printf("Final Binding: %s\n", actual);
    rpc_string_free(&actual, &status);
    if(status!=error_status_ok)
      print_error("client main() calling rpc_string_free()", status);
  }

/* Verzeichniseintraege lesen */

  while (optind < argc){
    TRY
    printf ("readdir: %s\n",argv[optind]); 
    dir_readdir(binding_handle,argv[optind],&dirlist,&status);
    if(status!=error_status_ok)
      print_error("client main() doing RPC call dir_readdir()",
                   status);
    else {
      printf ("\n");
      nlp=(*dirlist).readdir_res_u.list; 
      while (nlp){
        printf("%s\n",nlp->name); 
        nlp=nlp->next;
      }
    }
    optind++;
    CATCH_ALL { 
      printf("Caught Something!\n");
    }
    ENDTRY;
  }
}

Listing 9: Makefile für dir.idl

# Makefile fuer dir -- DCE Example
#

CC = gcc
IFNAME = dir
INCDIR = /opt/dcelocal/include 
INCFLAGS =-I. -I$(INCDIR)
IDLFLAGS =-keep c_source -I$(INCDIR) -cc_cmd "gcc \ 
          -D_BSD -DLINUX -D_REENTRANT -c"
IDL = /opt/dcelocal/bin/idl 
CFLAGS = $(INCFLAGS) -DLINUX -D_REENTRANT
LIBDIR = /opt/dcelocal/lib
LIBS = $(LIBDIR)/libnck.a \
       $(LIBDIR)/libidl.a \
       $(LIBDIR)/libpthreads-ext.a -lpthread -lm 
SSTUB_SRC = ${IFNAME}_sstub.c
SSTUB = ${IFNAME}_sstub.o
CSTUB_SRC = ${IFNAME}_cstub.c
CSTUB = ${IFNAME}_cstub.o

all: dir_client dir_server

#---------------------------------------------------------------------
# CLIENT BUILD 
#

dir_client: dir_client.c ${IFNAME}.h ${CSTUB} ${SSTUB}
  @echo + building client program 
  $(CC) $(CFLAGS) dir_client.c dir_cstub.o -o dir_client $(LIBS)

#---------------------------------------------------------------------
# SERVER BUILD 
#

dir_server: dir_server.c dir_proc.c ${IFNAME}.h ${CSTUB} ${SSTUB} 
  @echo + building server program 
  $(CC) $(CFLAGS) dir_server.c dir_proc.c dir_sstub.o -o dir_server $(LIBS)

#---------------------------------------------------------------------
# INTERFACE BUILD 
#

${IFNAME}.h ${SSTUB_SRC} ${CSTUB_SRC}: ${IFNAME}.idl
  @echo + generating interface header and stub sources
  $(IDL) $(IDLFLAGS) ${IFNAME}.idl 

clean:
  rm -f dir_client dir_server *~
  rm -f *.o *_?stub* ${IFNAME}.h

Infos
[1] http://docs.sun.com Sun Microsystems Online Documentation
[2] http://www.transarc.com/afs/transarc.com/public/www/Public/Documentation/dce/1.1/index.html
[3] http://www.aa.net/~mtp/ DCE-RPC for Linux
[4] http://www.camb.opengroup.org/dce/ OpenGroup DCE
[5] http://www.camb.opengroup.org/dce/info/faq-mauney.html DCE FAQ
[6] UNIX Network Programming, W. Richard Stevens (Prentice Hall Software Series, 1990)
[7] Power Programming with RPC, John Bloomer (O'Reilly & Associates, Inc, 1992)
[8] RFC-1014 XDR: External Data Representation Standard
[9] RFC-1050 RPC: Remote Procedure Call Protocol Specification
[10] RFC-1057 RPC: Remote Procedure Call Protocol specification version 2 (obsoletes RFC1050)
[11] RFC-1790 An Agreement between the Internet Society and Sun Microsystems, Inc. in the Matter of ONC RPC and XDR Protocols
[12] RFC-1831 RPC: Remote Procedure Call Protocol SpecificationVersion 2
[13] RFC-1832 XDR: External Data Representation Standard
[14] RFC-1833 Binding Protocols for ONC RPC Version 2
[15] ftp://ftp.gcom.com/pub/linux/src/streams-1-15-98 SystemV Streams Implementation für Linux
[16] http://www.cs.hut.fi/ssh Secure Shell - verschlüsselte Verbindungen

Glossar
DCE: Distributed Computing Environment - ein Industriestandard der Open Group, der festlegt, mit welchen Mitteln und Methoden verteilte Anwendungen zu implementieren sind
NDR: Network Data Representation - hardwareunabhängige Datenbeschreibung; verwendet von DCE
OSF: Open Software Foundation - Vorgänger Konsortium der Open Group
RPC: Remote Procedure Call - Mechanismus zum Aufruf entfernter Prozeduren in einem anderen Adreßraum
Sun-RPC: siehe TS-RPC
TI-RPC: Transport Independent RPC - nicht auf einen bestimmten Transportdient angewiesen, setzt auf Transport-Level-Interface (TLI) von SystemV auf; auch ONC-RPC genannt: Open Network Computing (Sun Terminus)
TS-RPC: Transport Specific RPC - direkte Kopplung an TCP/IP als Transportmittel; auch Sun-RPC genannt
XDR: External Data Representation: hardwareunabhängige Datenbeschreibung; verwendet bei TS- und TI-RPC

Der Autor

Peter Wächtler arbeitet als Software-Entwickler bei L.I.Consulting, Hildesheim. Er entwickelt dort kundenspezifische Lösungen im Client/Server-Umfeld unter Unix und MS-Windows/Delphi. Der "erste Kontakt" mit Linux fand vor mehr als 4 Jahren mit 0.99pl12 statt.

Copyright © 1998 Linux-Magazin Verlag