R P C - Remote Procedure Calls
|
Autor:
Zbigniew Lisiecki
Version 1.4, 21 Jan 2004 |
RPC (Remote Procedure Call - Aufruf entfernter Proceduren) ist eine Programmiertechnik, die es erlaubt, innerhalb eines Programmes Daten an einen anderen Rechner zu verschicken. Im anderen Rechner wird eine dort abgelegte Prozedur mit den empfangenen Daten als Parameter aufgerufen und die Ergebnis-Daten werden wieder an den lokalen Rechner zurueck uebermittelt. RPC stuetzt sich auf das Client/Server-Modell: der lokal agierende Client-Rechner nutzt das Programm des entfernten Servers.
...................... ................................. . . . . . Computer A . . Computer B . . . . . . Client Programm . . Server Daemon . . . . . . aktiv | RPC-Aufruf . . . . V . . . . -----------------------------------> Initialisierung . . . . Datenuebertragung . | des Dienstes . . . . . | . . . . . | Aufruf der Dienstroutine . . . . V . . Programm . . ----------> . . wartet . . | die Dienst- . . . . | routine . . . . . Antwort V . . . . . <----------- . . . . | . . Rueckkehr aus . . | Abschluss des Dienstes . . dem RPC-Aufruf . . V . . <------------------------------------ . . | . Datenuebertragung . . . | Programm- . . . . | fortsetzung. . . . V . . . ...................... ................................. Bild 1Die Objekte des Serverdienstes sind primaer Programme und CPU Resourcen. Dabei muessen Start- und/oder Ergebnisdaten uebertragen werden. Sie haben innerhalb eines Prozessimages im Computer A eine rechnerspezifische Darstellung (z.B. Integer Big Endian oder IEEE-Float). Um solche Typen einem Prozess auf einem anderen Computer (B) zur Verfuegung zu stellen, muessen sie in das fuer B-Rechner spezifische Format (z.B. Little Endian) umgewandelt werden. Das geschieht durch eine Umwandlung in ein spezielles nichtrechnerspezifisches Format vor dem Verschicken und die Rueckumwandlung nach der Ankunft im Rechner B. Das nichtrechnerspezifische Format heisst eXternal Data Representation (XDR).
--------------- Format A ----> | XDR-Filter | --------------- | | Datenuebertragung | | V --------------- | XDR-Filter | ----> Format B --------------- Bild 2RPC bietet Dienste an, die die Eigenschaften der Netzwerkverbindung transparent (unsichtbar) machen. So koennen komfortabel Anwendungen konstruiert werden, die ihre Programme oder Daten in einem Netzwerk verteilen (verteilte Anwendungen). Ein Programm kann z.B. eine Prozedur rdate() aufrufen, die das Datum auf einem anderen Rechner liefert, ohne dass auf dieser Ebene die Probleme der Netzwerkkommunikation behandelt werden. Die RPC-Schnittstelle besteht aus Funktionsaufrufen, in denen alle Details der Netzwerkkommunikation und Formatumwandlung implementiert sind. Aus diesen Funktionsaufrufen koennte z.B. rdate() gebaut sein. Unter anderem sind Dienste wie NIS, REX, NFS, NLM mit Hilfe von RPC konstruiert. RPC selbst ruft low-level Netzwerkprotokolle wie TCP, UDP, SPX/IPX. Nach dem ISO-OSI Modell ist RPC/XDR zwischen Transportschicht und der Anwendungsschicht anzusiedeln.
-------------------------- ISO/OSI-Terminologie ------------------------ | Client- | | Server- | | Prozess ------------- | | ------------ Prozess | | | Client- | | | | Server- | | | | routinen | | Anendungsschicht | | routinen | | | ------------- | | ------------ | | lokaler | ^ | | | ^ | | Prozedur- V | | | V | | | aufruf --------------- | | --------------- | | | Client-Stub | | RPC | | Server-Stub | | | | XDR-Filter | | XDR: Presentation Layer | | XDR-Filter | | | --------------- | | --------------- | | | ^ | Sitzungsschicht | | ^ | ------------- | -- | ----- ------ | -- | ---------- | | | | System call | | Transportschicht z.B. TCP | | | | | | ------------- | -- | ----- ------ | -- | ---------- | lokaler V | | | V | | | Kernel --------------- | | | | | Netzwerk- | | Netwerkschicht z.B. IP | entfernter | | | routinen | | | Kernel | | --------------- | | | ------------- | -- ^ ----- ------ | -- ^ ---------- | | | | | | Interrupt | | V | V | ------------------------------ ------------------------ | Netzwerkkarte z.B.Ethernet | Datenverbindungsschicht| | ------------------------------ ------------------------ V | | | | -------<----------------------------------- | | physikalishe Schicht | ---------------------------------------->------------ Bild 3Da wir hier nur eine UNIX-Umgebung betrachten, kann RPC auch zur Kommunikation zwischen zwei Prozessen innerhalb eines Rechners verwendet werden. Der RPC-Mechanismus wurde jedoch in auf anderen Betriebssystemen, wie System/370 VM, MVS, VMS, MS-DOS implementiert.
RPC wurde in den 70-er Jahren entwickelt. Die ersten Versionen sind bei Xerox entstanden. Eine davon, "Courier", wurde als Produkt 1981 eingefuehrt. Die Version von Sun Microsystems, ONC (Open Network Computing) entstand 1985 und diente gleich demselben Entwickler zum Aufbau von NFS, das schnell zu einem Industriestandard wurde. Um moeglichst grosse Verbreitung der RPC/XDR-Schnittstelle zu erreichen, stellt Sun den Quellcode ihrer Implementierung als Public Domain allen zur Verfuegung und ermuntert so dazu sie einzusetzen. Als Library ist RPC/XDR von Sun auch in UNIX System V Release 4 (SVR4) von AT&T enthalten. Sie wurde ebenfalls dazu benutzt, Anwendungen auf Sun's SPARC-Stations und VAX, IBM-PC, oder Cray zu verteilen. Analoge Systeme sind:
Die RPC-Schnittstelle kann dazu verwendet werden, im Client ein Protokoll
auf der Applikationsebene zu unterhalten, das die interne Logik des
Programms wiedergibt. Z.B. kann dieses Protokoll besagen, dass der Client
mit dem ersten RPC-Aufruf die Daten im Server sucht und sie erst mit dem
zweiten RPC-Aufruf vom Server zum Client uebertraegt. Dann haengt die
Antwort des Servers auf den zweiten Aufruf davon ab, ob der erste Aufruf
vorher ueberhaupt stattfand. Der Zustand des Servers nach dem ersten Aufruf
ist in Bezug auf diese Antwort sicherlich nicht der gleiche, wie davor.
Sollte der Server z.B. zwischen den Aufrufen abstuerzen, so muss er nach dem
erneuten Start auch noch in den richtigen Zustand gebracht werden, damit der
zweite Aufruf richtig beantwortet werden kann. Der Client soll von Abstuerzen
des nicht betroffen sein. Solche Server nennt man Server mit Zustand. Im
Gegensatz dazu antworten zustandslose Server auf die gleiche Request immer
gleich, der Zustand der Servers ist in solchen Faellen deshalb immer der
gleiche.
Als Beispiel betrachten wir die NFS Implementierung von Sun. Sie unterstuetzt
zustandslose Server. Deshalb wird jedes open() von einem File auf dem
entfernten Filesystem zuerst im Client zurueckgehalten. Die Uebertragung
der fuer open() spezifischen Daten geschieht erst bei dem nachfolgenden
read() oder write() zusammen in einer Transaktion. Der Absturz des
entfernten Filesystems zwischen open() und read() fuehrt zu keinen Problemen.
Zustaende sind lokal. Problematische Zustaende propagieren nicht im
Netzwerk.
Diese Begriffsbildung ist nicht notwendigerweise auf RPC beschraenkt,
sondern ist allgemein auf das Client/Server Modell bezogen. Sie gestattet
eine adaequate Beschreibungen der Probleme, die in Client/Server
Konfigurationen auftreten.
Bei der Uebertragung der Prozedurparametern bzw. deren Ergebnissen mittels RPC muss der zahlenmaessige Informationsgehalt und die Struktur von, unter Umstaenden komplizierten Datengebilden bewahrt werden. Dabei muessen folgende Gesichtspunkte beachtet werden.
serialising deserialising | | V V ------------- ---------- ---------- ------------- | | ---> | XDR - | | XDR - | ---> | | | | | Filter1| ---------------> | Filter1| | | | | ---------- ---------- | | | App1 z.B. | | App2 z.B. | | | Datentransfer | | | Client | | Server | | | ---------- ---------- | | | | | XDR - | <--------------- | XDR - | | | | | <--- | Filter2| | Filter2| <--- | | ------------- ---------- ---------- ------------- ^ ^ | | deserialising serialising Bild 4Die XDR-Bibliothek liefert Funktionen, die diese Aufgaben groesstenteils automatisch erledigen koennen. Sie teilen sich in zwei Gruppen:
Die Operationen der Serialisierung/Deserialisierung von Daten beziehen sich auf Datenstroeme, die realisiert werden entweder
enum xdr_op {
XDR_ENCODE = 0,
XDR_DECODE = 1,
XDR_FREE = 2
};
typedef struct {
enum xdr_op x_op;
struct xdr_ops {
bool_t (*x_getlong)();
bool_t (*x_putlong)();
bool_t (*x_getbytes)();
bool_t (*x_putbytes)();
u_int (*x_getpostn)();
bool_t (*x_setpostn)();
caddr_t (*x_inline)();
void (*x_destrpy)();
} *x_ops;
caddr_t x_public;
caddr_t x_private;
caddr_t x_base;
int x_handy;
} XDR;
Diese Struktur bildet die einzige Schnittstelle zum Stream. Der Pointer
auf sie wird an die meisten Funktionen aus der XDR-Bibliothek als Parameter
weitergegeben. Alle Funktionen, deren Aufgaben unabhaengig vom eingesetzten
Strom formuliert werden koennen, brauchen sich dann um stromspezifische
Inhalte nicht zu kuemmern.
Die Komponenten der XDR-Struktur haben folgende Bedeutungen, die jedoch
fuer den Anwendungsprogrammierer meistens unerheblich sind und spielen nur
bei der Definition neuer XDR-Stroeme eine Rolle.
x_op | die aktuelle Operation, die an dem Strom durchgefuehrt wird. Sie wird getrennt spezifiziert, weil die spaeter beschriebenen XDR-Filter von diesem Parameter meistens unabhaengig sind. |
x_getlong | schreiben und lesen von long's von dem oder auf den Strom. |
x_putlong | Diese Funktionen nehmen die eigentliche Umwandlung vor (z.B. Big- to Little-Endian). |
x_getbytes | schreiben und lesen von Bytesequenzen von dem oder auf den |
x_putbytes | Strom. Sie geben je nach Ergebnis TRUE oder FALSE zurueck. |
x_getpostn | Macros fuer Zugangsoperationen |
x_setpostn | |
x_destroy | |
x_inline | nimmt als Parameter (XDR *) und die Anzahl der Bytes. Gibt die Adresse in dem strominternen Puffer zurueck. |
x_public | Daten des XDR-Benutzers. Sie sollen in der Stream-Implementation nicht benutzt werden. |
x_private | Daten privat bzgl. der jeweiligen Stream-Implementierung |
x_base | auch privat, wird fuer Informationen ueber die Position im Strom benutzt |
x_handy | auch privat, zusaetzlich |
---------------------------------- | | | --------- Umwandlung mittels | ---------- | | user | XDR-Filter | | output | | | data | -----------> | UDP-Pakete --------->| data | | | | | / | | | | | X X X X / ---------- | --------- ----------------------- | / | -----------> Rueckumwandlung | / | XDR-Strom mit dem gleichen | user prozess / ------------------------ XDR-Filter | space / / | / / | | / / | | / / | | XDR- / | | handle | | | | | ---------------------------------- Bild 5Die gueltigen Operationen, die XDR Struktur vermittelt, sind:
enum xdr_op {
XDR_ENCODE, /* Serialisierung */
XDR_DECODE, /* Deserialisierung */
XDR_FREE /* befreit den Speicherplatz */
};
Standard IO-Streams benutzen fuer die Datenuebertragung den IO-Mechanismus, wie er in <stdio.h> definiert ist. Daten, die aus ihrem lokalen Format ins XDR-Format umgewandelt, dh. serialisiert werden, werden auf ein FILE geschrieben. Umgekehrt beinhaltet die Ruecktransformation das Lesen aus einem FILE. Der Standard IO-Stream wird mit folgendem Aufruf erzeugt:
void xdrstdio_create (handle, file, op)
XDR *handle;
FILE *file;
enum xdr_op op;
Vor diesem Aufruf muss der Speicherbereich "handle" im user space, wie
im Bild 5 dargestellt, bereits existieren. Analog muss der Parameter "file"
auf ein bereits geoeffnetes File zeigen. Mit xdrstdio_create() werden diese
Komponenten so "zusammengebunden" und in der Struktur handle abgelegt, dass
dieses handle fuer weitere XDR Operationen bequem benutzt werden kann. Der
Parameter op kann entsprechend der Deklaration von enum xdr_op als
XDR_ENCODE, XDR_DECODE oder XDR_FREE gesetzt werden. Damit wird entschieden
fuer welche Operation der in handle beschriebene IO-Stream benutzt wird,
wobei dieser Parameter in der XDR *handle Struktur auch nachtraeglich "per
Hand" veraendert werden kann. Im Falle von XDR_FREE wird der IO-Stream-
Buffer mit fflush() "gefluscht", aber das File wird nicht geschlossen. Die
Funktion fclose() muss nachtraeglich explicit aufgerufen werden. Damit hat
man die Moeglichkeit ein File nacheinander fuer verschiedene IO-Streams zu
benutzen und erst am Ende zu schliessen.
Mit Standard IO-Streams kann man Daten in XDR-Format in einem File
zwischenspeichern und erst spaeter von einem anderen Programm fuer die
Weiterbehandlung einlesen.
Im Gegensatz zu Standard IO-Streams bleiben Daten, die mit Memory Streams zwischengespeichert werden, im Hauptspeicher im Speicherbereich des jeweiligen Prozesses. Sie koennen anschliessend vom gleichen Prozess zurueckgewandelt, oder mit "shared memory" von einem anderen Prozess abgeholt werden.
void xdrmem_create (handle, addr, len, op)
XDR *handle;
char *addr;
u_int len;
enum xdr_op op;
Die Aufruf-Parameter sind analog zu xdrstdio_create(). Parameter addr
ist der Pointer auf den betreffenden Speicherbereich, len seine Laenge in
Bytes.
Die dritte Art von XDR-Streams ist allgemeiner. Standard IO-Stream und Memory-Stream koennen als seine Spezialfaelle betrachtet werden.
void xdrrec_create (handle, sendsize, recvsize,
iohandle, readproc, writeproc)
XDR *handle;
u_int sendsize, recvsize;
char *iohandle;
int (*readproc)(), (*writeproc)();
Aehnlich wie Standard IO-Stream unterhaelt Record-Stream eigene Pufferzonen,
deren Groesse fuer den Ausgabepuffer mit "sendsize" und fuer den
Eingabepuffer mit "recvsize" angegeben werden. Bei Nullwerten werden vom
System Voreinstellungen geliefert. Die zwei Parameter readproc, bzw. wri-
teproc sind Zeiger auf Funktionen, die die eigentliche Ein/Ausgabe
durchfuehren. Sie muessen vom Anwender geliefert werden. Als Beispiel koennen
an dieser Stelle die Systemcalls read(2) und write(2) von clib.a stehen. Sie
koennen jedoch auch vom Anwender selbst geschrieben werden. Nach der
Erzeugung des Stromes werden sie von XDR-System fuer den eigentlichen
Datentransfer mit folgenden Parametern aufgerufen:
int readproc/writeproc (iohandle, buf, len)
char *iohandle,
*buf;
int len;
Der Parameter iohandle wird in xdrrec_create() nicht benutzt. Er wird
an readproc oder writeproc weitergeleitet und kann dort beliebig verwendet
werden. Als Beispiel muesste im Falle von Systemcalls read(2) und write(2)
an dieser Stelle der Filedeskriptor stehen. Aehnlich wie read(2) und
write(2) geben readproc und writeproc die Anzahl der transferierten Bytes
zurueck.
-------------------------------------- | | | ------------- Umwandlung mittels | | | user data | XDR-Filter | | | | -----------> | z.B. UDP-Pakete | | | | | | | X X X X | ------------- -------------------------------- | / | writeproc() --> | / | | user prozess / -------------------------------- | space / / <-- readproc() | / / | | / / | | / / | Transportmedium | XDR- / | mit den XDR-Stroemen | handle | | | | | -------------------------------------- Bild 6Der Transportmechanismus wird mit dem XDR-Filter jeweils fuer einen Datensatz in Gang gesetzt, aber seine Richtung und alle sonstigen fuer den Transport verantwortlichen Variablen werden vom XDR handle gesteuert. Im Gegensatz zu xdrstdio_create () und xdrmem_create () entfaellt bei xdrrec_create () der Parameter, der die Kodierungsrichtung angibt. Sie kann nachtraeglich mit
handle->x_op = XDR_ENCODE oder XDR_DECODE;
explicit im handle hin und her geschaltet werden. Die Records (Saetze)
werden zusammen mit der Information ueber die Laenge des jeweiligen
Fragmentes und der end_of_record Mark auf den Stream geschrieben:
------------------------- | 0 | length 1 | data 1 | fragment 1 | 0 | length 2 | data 2 | fragment 2 | 0 | . | . | . ein Record | 0 | . | . | . | 0 | . | . | . | 0 | . | . | . | 0 | . | . | . | 1 | . | . | . ------------------------- ^ ^ | | | 31 Bits fuer die Laenge des Datenfragmentes | 1 Bit: 1 fuer das letzte Fragment, 0 sonst Bild 7Drei Zusatzfunktionen ermoeglichen die explizite Aufteilung des Stromes in Records:
bool_t xdrrec_endofrecord (handle, flushnow)
XDR *handle;
bool_t flushnow;
bool_t xdrrec_skiprecord (handle)
XDR *handle;
bool_t xdrrec_eof (handle)
XDR *handle;
xdrrec_endofrecord beendet den Record (Satz), indem das laufende Fragment
als letztes markiert wird. Bei flushnow == TRUE wird sogleich writeproc
aufgerufen und bei flushnow == FALSE erst, wenn der Puffer gefuellt ist.
xdrrec_skiprecord wird beim Lesen aus dem Strom dazu benutzt, den
laufenden Record zu ueberspringen.
Die Funktion xdrrec_eof () gibt TRUE zurueck, falls keine Daten mehr im
Strom darauf warten, abgeholt zu werden.
Wie beim Record-Strom gibt es, in <xdr.h> definierte Macros, die sich auf alle drei Strom-Typen beziehen:
u_int xdr_getpos (handle)
XDR *handle;
bool_t xdr_setpos (handle, pos)
XDR *handle;
u_int pos;
void xdr_destroy (handle)
XDR *handle;
Die zwei ersten ermoeglichen es die Position im Strom zu erfragen, bzw.
zu setzen. Mit dem Returnwert von xdr_getpos() kann die tatsaechliche Anzahl
von Bytes abgefragt werden, die bis jetzt fuer die Kodierung gebraucht
wurden.
Die dritte Funktion, xdr_destroy() muss vom Anwender aufgerufen werden
um den mit xdr..._create() erzeugten Strom zu schliessen. Damit wird auch
der beim Erzeugen moeglicherweise allokierte Speicherplatz freigegeben.
Im vorigen Kapitel wurden Funktionen beschrieben, die den Mechanismus der XDR-Umwandlung initialisieren. Dieser, in der handle-Variable beschriebene, Mechanismus wird erst spaeter mit den Filter-Funktionen in Gang gesetzt. Alle diese Filter werden, je nach Besetzung der Variablen handle->x_op, zum Kodieren oder zum Dekodieren benutzt. Diese Eigenschaft stellt sicher, dass das je nach Objekttyp kodierte Objekt auch richtig (mit der gleichen Funktion) dekodiert wird.
Es gibt in XDR-Librabry folgende sog. primitive Filter-Funktionen:
bool_t xdr_char (handle, cp)
XDR *handle;
char *cp;
bool_t xdr_u_char (handle, ucp)
XDR *handle;
unsigned char *ucp;
bool_t xdr_int (handle, ip)
XDR *handle;
int *ip;
bool_t xdr_u_int (handle, uip)
XDR *handle;
unsigned *uip;
bool_t xdr_long (handle, lp)
XDR *handle;
long *lp;
bool_t xdr_u_long (handle, ulp)
XDR *handle;
u_long *ulp;
bool_t xdr_short (handle, sp)
XDR *handle;
short *sp;
bool_t xdr_u_short (handle, usp)
XDR *handle;
u_short *usp;
bool_t xdr_float (handle, fp)
XDR *handle;
float *fp;
bool_t xdr_double (handle, dp)
XDR *handle;
double *dp;
bool_t xdr_enum (handle, ep)
XDR *handle;
enum_t *ep;
bool_t xdr_bool (handle, bp)
XDR *handle;
bool_t *bp;
Der erste Parameter ist der Pointer auf den bereits existierenden
Strom. Der zweite gibt die Adresse an, von wo die Daten abgeholt bzw. wo sie
abgelegt werden. Das ist notwendig, damit der Filter in beide Richtungen
arbeiten kann: Beim Dekodieren muessen ja die Daten unter die angegebene
Adresse geschrieben werden. Der Returnwert gibt an, ob eine erfolgreiche
Umwandlung stattgefunden hat.
Der primitive Filter xdr_enum setzt voraus, dass enum sowohl beim
Absender als auch beim Adressat mit Integer realisiert ist. Das ist jedoch
nach Kernighan & Ritchi Standard C:
#define enum_t int;
xdr_bool ist der Spezialfall von xdr_enum fuer:
enum bool_t { TRUE = 1, FALSE = 0 };
Zusaetzlich gibt es in der XDR-Library die Funktion:
bool_t xdr_void ();
die dann verwendet wird, wenn keine Daten umgewandelt werden. Sie wird z.B
bei discriminated unions oder gebundenen Listen gebraucht.
Ausser den beschriebenen Funktionen liefert die XDR-Library in-line
Macros, die zwar den Funktionsaufruf sparen, jedoch keine Fehlerbehandlung
ermoeglichen:
long IXDR_GET_LONG (buf)
long *buf;
bool_t IXDR_GET_BOOL (buf)
long *buf;
type IXDR_GET_ENUM (buf, type)
long *buf;
type type;
u_long IXDR_GET_U_LONG (buf)
long *buf;
short IXDR_GET_SHORT (buf)
long *buf;
u_short IXDR_GET_U_SHORT (buf)
long *buf;
Entsprechend gibt es PUT-Macros. Um beide (GET oder PUT) anwenden zu
koennen, muss vorher die Pufferadresse mit der Funktion
long *xdr_inline (handle, len)
XDR *handle;
int len;
geholt werden. Es folgt ein Beispielprogramm mit Macros, die standard IO-
Streams verwenden.
#include
#define N 10
...
FILE *fp, *fopen();
XDR handle;
long *buf;
int i, count;
short array[N];
fp = fopen (filename, "w");
xdrstdio_create (&handle, fp, XDR_ENCODE);
count = BYTES_PER_XDR_UNIT * N;
buf = inline (&handle, count);
for (i = 0; i < N; i++)
array[i] = IXDR_GET_SHORT (buf);
xdr_destroy (&handle);
fclose (fp);
Composite Filter werden zusaetzlich zu den primitiven Filtern von der XDR-Library geliefert. Sie ermoeglichen die Umwandlung von zusammengesetzten Datenstrukturen. Es gibt composite Filter fuer:
-
strings,
-
byte-arrays,
-
opaque data,
-
arrays,
-
fixed size arrays
-
discriminated unions
-
pointer
bool_t xdr_string (handle, sp, maxlen)
XDR *handle;
char **sp;
u_int maxlen;
maxlen kann protokollspezifisch als eine feste Zahl, z.B. 256 gesetzt
werden. Der zweite Parameter ist ein doppelter Pointer. Das ist notwendig,
um den Fall der Dekodierung abzudecken. Weil dem Aufrufer die tatsaechliche
Laenge des Strings, den er bekommt, nicht bekannt ist, kann einen genuegend
grossen Puffer zur Verfuegung stellen, oder der Puffer wird vom Filter
selbst bereitgestellt. Der Aufrufer gibt nur die Adresse der Speicherzelle
unter der xdr_string() die Adresse seines Puffers hineinschreibt. In beiden
Faellen ist eine zusaetzliche Zwischenvariable, die die Adresse des
Stringpuffers enthaelt, notwendig.
char buf[MAXLEN], *p;
p = buf;
xdr_string (handle, &p, maxlen)
---------------- | | | V -----|------------------------------------------------- linearer Speicher | p | | der String buf, dh. Stringdaten | ----- ----------------------------------- ^ | Adresse von p Bild 8Der Stringpuffer wird vom Filter xdr_string() erst dann allokiert, wenn *sp == NULL ist, dh. wenn die Zwischenvariable (p), deren Adresse an xdr_string() geleitet wird (sp = &p), keine gueltige Pufferadresse enthaelt (p == NULL). Andernfalls ist der Anwender selbst dafuer verantwortlich, dass sein Puffer fuer den Filter nicht zu klein ist. Bei der XDR_FREE operation findet, falls p != NULL ist, der Aufruf free (p) statt. Bemerkung: Der Adressoperator & darf in C nur vor einem real im Speicher existierenden Objekt stehen. Insbesondere hat die Konstruktion &&x keinen Sinn. Die Methode mit der Uebergabe des doppelten Pointers ist zwar bei Angabe von maxlen theoretisch nicht notwendig, sie bildet aber den einzigen Austauschmechanismus, der auch fuer Strings, die wesentlich kuerzer als maxlen sind, effizient bleibt. Ein "Wrapper" ueber xdr_string() ist xdr_wrapstring(): Der Aufruf
xdr_wrapstring(hand, strp)
ist aequivalent zu
xdr_string(hand, strp, MAXUNSIGNED)
MAXUNSIGNED ist der maximale vorzeichenlose Integer. Es ist natuerlich
schlecht, wenn er im System, wo die Kodierung stattfindet groesser ist als
im Addressat-System beim Dekodieren, aber das kann nur bei der Uebertragung
von sehr langen Strings eine Rolle spielen. Eine praktische Grenze ist mit
der Paketgroesse des UDP-Protokolls erreicht, auf dem XDR aufbaut.
Bytearray unterscheidet sich von einem String dadurch, dass er nicht
mit 0 beendet wird. Entsprechend koennen Nullen im Bytearray selbst
vorkommen.
bool_t xdr_bytes (handle, sp, lp, maxlen)
XDR *handle;
char **sp;
u_int *lp;
u_int maxlen;
Der Filter xdr_bytes() bekommt zusaetzlich zu xdr_string() ein Argument
lp, das auf die Speicherstelle zeigt, wo die Laenge des Arrays abgelegt
wird. Ist bei der "Deserialisierung" *sp == NULL, so wird wie bei
xdr_string(), der Speicherplatz vom Filter selbst allokiert.
Falls die Anzahl der zu Uebertragenden Bytes bekannt ist, kann an der
Stelle von xdr_bytes() die schnellere Routine xdr_opaque() benutzt werden:
bool_t xdr_opaque (handle, sp, lp)
XDR *handle;
char *sp;
u_int lp;
xdr_bytes() kann als Spezialfall des Filters fuer Arrays betrachtet
werden:
bool_t xdr_array (handle, sp, lp, maxlen, esize, xdr_element)
XDR *handle;
char **sp;
u_int *lp;
u_int maxlen;
u_int esize;
bool_t (*xdr_element)();
esize ist die Groesse eines Elementes in Bytes (z.B. mit sizeof()
berechnet) und (*xdr_element)() ein Pointer auf den XDR-Filter fuer das
einzelne Element.
Ist die Anzahl der Bytes lp bekannt, so kann statt xdr_array() die
schnellere Routine xdr_vector() verwendet werden (aehnlich wie xdr_opaque()
statt xdr_bytes()).
bool_t xdr_vector (handle, sp, lp, esize, xdr_element)
XDR *handle;
char *sp;
u_int lp;
u_int esize;
bool_t (*xdr_element)();
An der Stelle von Varianten (unions) gibt es in XDR discriminated
unions. Der Diskriminator, ein enum_t discr, der die Information darueber
traegt, welche Komponente der Variante konkret zum Einsatz kommt, muss vor
der Uebertragung bekannt sein. Ebenfalls muss fuer jede Komponente eine
spezielle Prozedur angegeben werden, die diese Komponente transformiert. Das
kann auf folgende Weise geschehen: Jeder Komponente der Variante ist ein
Paar (value, proc) zugeordnet:
struct xdr_discrim {
enum value;
bool_t (*proc)();
};
Value ist der Wert, der sich bei einer Komponente mit discr decken
muss, und (*proc)() die Prozedur, die diese Komponente transformiert. Dann
hat xdr_union:
bool_t xdr_union (handle, discr, unp, arms, defaultarm)
XDR *handle;
enum_t *discr;
char *unp;
struct xdr_discrim *arms;
bool_t (*defaultarm)();
folgende Parameter:
enum utype { INTEGER = 1, STRING = 2, MYTYPE = 3 };
struct dunion {
enum utype discr; /* der Diskriminator */
union {
int ival;
char *pval;
struct mytype mval;
} uval;
};
struct xdr_discrim dunion_arms[4] = {
{ INTEGER, xdr_int },
{ STRING, xdr_wrapstring },
{ MYTYPE, xdr_myfilter },
{ _dontcare_, NULL }
}
bool_t xdr_dunion (handel, utp)
XDR *handle;
struct dunion *utp;
{
return (xdr_union (handle, &utp->utype, &utp->uval,
dunion_arms, NULL));
}
xdr_myfilter() fuer die Uebertragung von MYTYPEs muss natuerlich noch
geschrieben werden. Es gehoert zum guten Programmierstil, in der ersten
Zeile des Beispiels die Angaben: INTEGER = 1, etc. explizit zu machen, damit
der Compiler des Empfaengers hier keine andere Vorbesetzung trifft.
Oft tritt der Fall auf, dass eine Datenstruktur nur mit der Angabe des
Pointers transformiert werden soll. Das ist insbesondere dann der Fall, wenn
der Pointer selbst ein Element einer anderen Struktur ist. Diese Aufgabe
kann mit xdr_reference() erledigt werden.
bool_t xdr_reference (handle, pp, size, proc)
XDR *handle;
char **pp;
int *size;
bool_t (*proc)();
pp ist der Pointer auf die Struktur, size ihre Groesse (sizeof()
verwenden !), proc, der Filter fuer die Tranformation der Struktur. Wir
geben ein Beispiel:
struct pgn {
char *name;
struct mytype *mval;
};
#define MYSIZE sizeof(struct mytype)
bool_t xdr_pgn (handle, sp)
XDR *handle;
struct pgn *sp;
{
return (xdr_string (handle, &sp->name, NLEN) &&
xdr_reference (handle, &sp->mval, MYSIZE,
xdr_mytype));
}
xdr_reference () kann nur angewendet werden, wenn der betreffende
Pointer kein NULL-Pointer ist. Anderenfalls kommt xdr_pointer() zum Einsatz.
Die Synopsis ist die gleiche.
Die Tatsache, dass alle XDR-Filter als erstes Argument den Pointer auf den XDR-Handle haben und bool_t als Returnwert zurueckgeben, kann dazu verwendet werden, eigene XDR-Filter zu bauen. Dabei koennen die primitiven Filter als Bausteine dienen. So kann z.B. die Umwandlung der Struktur
struct my_struct {
int i;
char c;
short s;
};
mit dem folgenden sog. "Custom-Filter" erreicht werden.
bool_t my_filter (handle, data)
XDR *handle;
struct my_struct *data;
{
return (xdr_int (handle, &data->i) &&
xdr_char (handle, &data->c) &&
xdr_short (handle, &data->s));
}
Der Custom-Filter wandelt die Struktur my_struct komponentenweise um.
Gelingt eine Umwandlung nicht, so bricht er ab und gibt FALSE zurueck.
Anderenfalls wird TRUE zurueckgegeben. Gerade aus diesem Grunde gilt bei
XDR:
#define TRUE 1
#define FALSE 0
Die Eigenschaft der primitiven Filter, dass sie in beide Richtungen
(Kodieren, Dekodieren, aber auch XDR_FREE) arbeiten, uebertraegt sich
automatisch auf den Custom-Filter. Auch Alignment-Probleme bei Strukturen
fuehren zu keinem Fehler, falls die primitiven Filter richtig funktionieren.
Der zweite Parameter bei dem my_filter()-Aufruf kann verschiedene Bedeutung
haben, wie spaeter an einem Beispiel demonstriert wird.
Der Speicherplatz, der fuer die Zwischenpufferung der Daten in den Fil-
tern noetig ist, muss nicht immer vor der Uebertragung bekannt sein. In
diesem Fall muss der Filter ihn zur Laufzeit allokieren. Folgende Filter
koennen je nach Situation Platz mit Hilfe von malloc() allokieren:
xdr_array(),
xdr_bytes(),
xdr_pointer(),
xdr_reference(),
xdr_string(),
xdr_vector(),
xdr_wrapstring()
Um den Platz spaeter zu befreien kann xdr_free() benutzt werden:
void xdr_free (proc, obj)
xdr_proc_t proc;
char *obj;
Das erste Argument ist der Pointer auf den Filter, der den Platz allokierte,
z.B. xdr_string, das zweite die Adresse des Pointers auf das erzeugte
Objekt, z.B.:
char *ptr = NULL;
xdr_string (&handle, &ptr, MAXSIZE);
xdr_free (xdr_string, &ptr);
Naturgemaess uebertraegt sich die Eigenschaft Speicher zu allokieren
oder mit xdr_free() freizugeben auf die Custom-Filter. Wird malloc() im
Custom-Filter explizit verwendet, so sollte in der gleichen Routine fuer den
Fall handle->x_op == XDR_FREE auch free() aufgerufen werden.
Das folgende Beispiel zeigt die Anwendung eines XDR-Filters zum Speichern von numerischen Experimentdaten in einer rechnerunabhaengigen Weise und das Einlesen von diesen Daten, das auf einem anderen Rechner stattfinden kann. Die Daten werden beim Aufruf mit -i eingelesen, und mit -o auf das File geschrieben.
#include
#include
#define MAXNAME 128
#define MAXX 1024
#define DATLEN 27
struct edata {
char *autor;
char date[DATLEN];
int n;
float *x;
};
bool_t filter (handle, d)
XDR *handle;
struct e_data *d;
{
register int i;
char *da = d->date;
if (!xdr_string (handle, &d->autor, MAXNAME) ||
!xdr_string (handle, &da, DATLEN) ||
!xdr_int (handle, &d->n) ||
d->n > MAXX)
return (FALSE);
for (i = 0; i < d->n; i++)
if (!xdr_float (handle, &d->x[i])
return (FALSE);
return (TRUE);
}
main (argc, argv)
int argc;
char *argv[];
{
struct e_data d;
float *malloc ();
XDR hand;
FILE *fp, *fopen();
int i;
char *ctime();
if (argc < 3) {
printf ("usage: %s -o | -i file\n", argv[0]);
exit (1);
}
d.x = malloc (MAXX * sizeof (float));
if (argv[1][1] == 'i') { /* input data */
fp = fopen (argv[2], "r");
xdrstdio_create (&hand, fp, XDR_DECODE);
d.autor = NULL;
}
else {
fp = fopen (argv[2], "w");
xdrstdio_create (&hand, fp, XDR_ENCODE);
strcpy (d.date, ctime (time (0)));
d.autor = "zbyszek";
d.n = MAXX;
for (i = 0; i < MAXX; i++)
d.x[i] = i / 3;
}
printf ("filter = %d\n", filter (&hand, &d));
}
Im "-o" Fall werden die Daten zuerst erzeugt (else-Block im main). Die
eigentliche Lese- oder Schreiboperation geschieht im Filter-Aufruf ( letzte
Zeile). Beachten Sie bitte die unterschiedliche Handhabung von autor und
date Variablen im Filter. Die Hilfsvariable:
char *da = d->date;
ist notwendig. Der xdr_string Aufruf benoetigt die Adresse von der Adresse
vom String.
In dem zweiten Beispiel wird ein Record-Stream verwendet. Dazu muss der
create-Aufruf des ersten Bespiels ersetzt werden durch
xdrrec_create (&hand, 0, 0, fp, rdata, wdata);
Vorher muss die Operation im hand gesetzt werden:
hand.x_op = XDR_DECODE oder XDR_ENCODE;
Die zwei Funktionen rdata() und wdata() sind Wrapper fuer fread() und
fwrite():
rdata (fp, buf, n)
FILE *fp;
char *buf;
int n;
{
if (n = fread (buf, 1, n, fp))
return (n);
else
return (-1);
}
wdata (fp, buf, n)
FILE *fp;
char *buf;
int n;
{
if (n = fwrite (buf, 1, n, fp))
return (n);
else
return (-1);
}
An dieser Stelle koennten zwei beliebige Funktionen stehen, die den
Zugang zu einem beliebigen Medium darstellen, das vom vierten Parameter des
create-Aufrufs beschrieben, und an diese Funktionen weitergegeben wird.
Zusaetzlich sind am Ende des Filters die Zeilen:
if (hand->x_op == XDR_ENCODE) xdr_endofrecord (hand, TRUE);
if (hand->x_op == XDR_DECODE) xdr_skiprecord (hand);
sinnvoll.
Diese verkuerzte Beschreibung entspricht RFC1014 vom ARPA Network
Information Center. Wir geben hier die Byte-Reihenfolge der Uebertragung
einzelner Datentypen, sowie auch die Syntax der XDR-Sprache an. Die XDR-
Sprache aehnelt in der Daten-Beschreibung der C-Sprache, deshalb werden hier
hauptsaechlich die Unterschiede angegeben.
Alle Daten sind Vielfache von 4 Bytes (32 Bits). Das sichert die
korrekte Ausrichtung an der am meisten eingesetzten Hardware zu. (Ausnahme
CRAY: 8 Byte Ausrichtung). Es wird immer am zu 4-Byte Grenze Ende mit Nullen
aufgefuellt (insbesondere bei Opaque oder String). Reihenfolge der
Uebertragung ist Byte 0, Byte 1, ..., d.h. Big-Endian
Die Uebertragung von einem Byte auf eine fuer das Medium spezifische
Weise wird vorausgesetzt. XDR spezifiziert nicht wie ein Byte aus einzelnen
Bits zusammengesetzt wird. Ob das hoeherwertige Bit eines Bytes auch
tatsaechlich als erstes durch die Leitung fliesst, wird in den tiefer liegenden
Schichten, meistens in Data Link Layer, spezifiziert.
Integer sind 4 Byte, 2-Komplement
(MSB) (LSB)
+------+------+------+------+
|Byte 0|Byte 1|Byte 2|Byte 3|
+------+------+------+------+
Unsigned Integer ist ein Integer ohne Vorzeichen. Enum wird mit Integer
gebaut, wie in C. Boolean ist enum { FALSE = 0, TRUE = 1 }. Es wird als
bool identifier;
beschrieben. Hyper Integer und Unsigned Hyper Integer sind Erweiterungen von
Integer und Unsigned Integer auf 8 Byte:
(MSB) (LSB)
+------+------+------+------+------+------+------+------+
|Byte 0|Byte 1|Byte 2|Byte 3|Byte 4|Byte 5|Byte 6|Byte 7|
+------+------+------+------+------+------+------+------+
Float: ist ein 4-Byte lange IEEE single precision Float mit den Feldern:
S: Vorzeichenbit, 0 - positiv, 1 - negativ E: Exponent mit Basis 2, 8 Bit lang V: Verschiebung des Exponenten um 127 F: Mantisse mit Basis 2, 32 Bits (-1)**S * 2**(E-V) * 1.F ** ist der Potenzoperator
+------+------+------+------+
|Byte 0|Byte 1|Byte 2|Byte 3|
S| E | F |
+------+------+------+------+
1|<- 8->|<------23 Bit----->|
Da, wie oben ausgefuehrt, die Uebertragung eines Bytes von tieferen
Protokollschichten erledigt wird, bezeichnet S in der oberen Zeichnung nur
das hoeherwertige Bit des nullten Bytes und nicht das tatsaechlich als
erstes uebertragene Bit.
Double entspricht dem 8 Byte langen Float:
+------+------+------+------+------+------+------+------+
|Byte 0|Byte 1|Byte 2|Byte 3|Byte 4|Byte 5|Byte 6|Byte 7|
S| E | F |
+------+------+------+------+------+------+------+------+
1|<--11-->|<-----------------52 Bit-------------------->|
Signed Zero, Signed Infinity (Overflow), oder Denormalized (underflow)
entsprechen der IEEE-Konvention, NaN (not a number) wird nicht benutzt.
Fixed-length Opaque:
+------+------+ ... +------+ ... +------+ ... +------+ |Byte 0|Byte 1| ... |Byte n-1 0 | 0 | ... | 0 | +------+------+ ... +------+ ... +------+ ... +------+ |<---------n Byte---------->| aufgefuellt mit Nullen |Variable-length Opaque sind kodiert als unsigned Integer gefolgt vom fixed-length Opaque. Die Syntax ist:
opaque identifier;
oder
opaque identifier<>;
m bezeichnet die maximale Laenge, z.B, fuer UDP ist m=8192 wegen der
maximalen Paketgroesse. Falls nicht anders spezifiziert, ist m = 2**32 - 1.
Strings sind analog wie opaque: unsigned Integer gefolgt vom String-
Data in der aufsteigenden Byte-Reihenfolge: Byte0, Byte2, ... und auf-
gefuellt mit Nullen bis zur naechsten 4-Byte Grenze. Die Syntax ist:
string identifier;
oder
string identifier<>;
Fixed-length Array:
+-------------+-------------+ ... +-------------+
| Element 0 | Element 1 | ... | Element n-1 |
+-------------+-------------+ ... +-------------+
Die Elemente sind Vielfache von 4 Byte, koennen aber ansonsten verschieden
lang sein. Die letzte Eigenschaft ist notwendig fuer den Fall, dass
der Typ einzelner Elemente eine variable Laenge hat wie z.B. String.
Variable-length Array ist ein Integer gefolgt von Fixed-length Array.
Die Syntax ist:
type-name identifier;
oder
type-name identifier<>;
Structure ist analog zum Array eine Folge von Struct-Elementen in der
Reihenfolge, wie sie in der Definition vorkommen. Auch hier, wie ueberall in
RPC, muessen die Elemente Vielfache von 4 Bytes sein.
union switch (discriminant-declaration) {
case discriminant-value-A;
arm-declaration-A;
case discriminant-value-B;
arm-declaration-B;
.
.
.
default :
default-declaration;
} identifier;
Der Default-Ast ist optional.
Der Typ void beinhaltet keine Daten. Konstante ist ein Integer:
const identifier = n;
"typedef" deklariert keine Daten, sondern dient wie in der Sprache C
dazu, neue Bezeichnungen fuer die folgenden Datendeklarationen zu schaffen.
Es gibt eine zu typedef aequivalente, jedoch bei XDR bevorzugte
Schreibweise: Statt z.B.:
typedef enum {
FALSE = 0,
TRUE = 1
} bool;
wird folgende gleichwertige Form empfohlen:
enum bool {
FALSE = 0,
TRUE = 1
};
In der XDR-Sprache haben Pointer wie:
type-name *identifier;
eine etwas andere Bedeutung als in C. Man nennt sie Optional-Data. Die obige
Deklaration ist aequivalent mit der folgenden:
union switch (bool opt) {
case TRUE:
type-name element;
case FALSE:
void;
} identifier;
Dies ist auch aequivalent zu:
type-name identifier<1>;
Hier wird die 4 Byte lange bool opt von der union-Deklaration als die
Laenge des Arrays interpretiert (siehe Arrays). Bitfelder existieren im
jetzigen XDR nicht.
XDR-Datentyp
C-Datentyp
Decode/Encode-Filter
int
int
xdr_int
unsigned int
unsigned int, uint
xdr_uint
enum
enum
xdr_enum
bool
int, bool_t
xdr_bool
long
long
xdr_long
unsigned long
unsigned long, ulong
xdr_u_long
float
float
xdr_float
double
double
xdr_double
opaque identifier[n]
char identifier[n]
xdr_opaque
opaque identifier
opaque identifier<>
struct {
uint identifier_len;
char *identifier_val;
} identifier;
xdr_bytes
string identifier
char *identifier
xdr_string
string identifier<>
typename identifier[n]
typename identifier[n]
xdr_vector
typename identifier
typename identifier<>
struct {
uint identifier_len;
typename *identifier_val;
} identifier;
xdr_array
struct {
component-delar-A;
component-delar-B;
...
} identifier;
same as XDR
custom filter
union switch (discr) {
case discr-value-A:
arm-A;
case discr-value-B:
arm-B;
...
default:
default-decl;
} identifier;
struct identifier {
int discr;
union {
arm-A;
arm-B
}
};
custom filter
void
void
xdr_void
constant
int
xdr_int
typename *identifier;
struct identifier {
int discr;
union {
typename element;
void
}
}
custom filter
Bei der Syntaxbeschreibung wird die Backus-Naur Notation (siehe Anhang) verwendet.
declaration:
type-specifier identifier
| type-specifier identifier "[" value "]"
| type-specifier identifier "<" value ">"
| "opaque" identifier "<" value ">"
| "string" identifier "<" value ">"
| type-specifier "*" identifier
| "void"
value:
constant
| identifier
type-specifier:
[ "unsigned" ] "int"
| [ "unsigned" ] "hyper"
| "float"
| "double"
| "bool"
| enum-type-spec
| struct-type-spec
| union-type-spec
| identifier
enum-type-spec:
"enum" enum-body
enum-body:
"{"
( identifier "=" value )
( "," identifier "=" value )*
"}"
struct-type-spec:
"struct" struct-body
struct-body:
"{"
( declaration ";" )
( declaration ";" )*
"}"
union-type-spec:
"union" union-body
union-body:
"switch" "(" declaration ")" "{"
( "case" value ":" declaration ";" )
( "case" value ":" declaration ";" )*
[ "default" ":" declaration ";" ]
"}"
constant-def:
"const" identifier "=" constant ";"
type-def:
"typedef" declaration ";"
| "enum" identifier enum-body ";"
| "struct" identifier struct-body ";"
| "union" identifier union-body ";"
definition:
type-def
| constant-def
specification:
defintion *
keyword:
"bool" | "case" | "const" |
"default" | "double" | "enum" |
"float" | "hyper" | "opaque" |
"string" | "struct" | "switch" |
"typedef" | "union" | "unsigned" |
"void"
identifier:
letter
| identifier letter
| identifier digit
| identifier \"_\"
must not be a keyword
letter:
upper and lower case are same
comment:
"/*" comment-text "*/"
Im Gegensatz zur entfernten Ausfuehrung von Kommandos (Remote Command
Execution - RCX) wie z.B. rsh, rcp oder rlogin, muss bei einem RPC-Call im
Server-Rechner nicht notwendig ein neuer Prozess gestartet werden. Die
Verbindung wird zu einem bereits laufenden, auf die Anforderung wartenden
Prozess hergestellt, der als Server im engeren Sinne bezeichnet wird. So ein
Server-Prozess kann mehrere ausfuehrbare Prozeduren als Dienste anbieten. Es
ist aber ebenfalls moeglich, dass in einem Server-Rechner mehrere
Server-Prozesse laufen, die verschiedene Arten von Diensten anbieten.
Die entfernten Dienste werden durch Programm- und Prozedurnummer
adressiert. RPC ermoeglicht zusaetzlich eine Unterscheidung zwischen
verschiedenen Versionen: In einem Server-Prozess koennen gleichzeitig mehrere
Versionen der gleichen Prozedur angeboten werden. Damit kann man eine
Prozedur-Entwicklung im Netzwerk sinnvoll verwalten. Der Client muss also bei
der Adressierung der entfernten Prozedur den Server-Rechner, die
Programmnummer, die Versionsnummer und die Prozedurnummer kennen.
Netzwerk Transport-Protokolle, wie z.B (TCP) ermoeglichen zusaetzlich
zu Netzwerkadresse und Hostadresse (bereits in der Netzwerkschicht
vorhanden) Adressierungen, die aus sog. Portnummern bestehen.
Transportadresse = Netzwerkadresse + Hostadresse + Portnummer | | ----------------------------- | z.B. IP-Adresse Bild 9Unter einem Port versteht man eine Warteschlange mit zwei Semaphoren: fuer den Eingang und fuer den Ausgang. Der Transportweg ist durch das Paar (Port des Sender, Port des Empfaengers) und nicht nur durch einen Port allein gekennzeichnet. Mit der Portnummer kann ein Prozess angesprochen werden, der auf die Ankunft einer Message an diesem Port wartet. Diese Tatsache macht sich der RPC-Mechanismus zunutze. Eine direkte Adressierung des Server-Prozesses ueber die UNIX Prozess-Id waere nicht sinnvoll, da der Server-Prozess nicht immer die gleiche Prozess-Id haben kann. Er koennte ja z.B. abstuerzen und noch einmal gestartet werden.
(Programmnummer, Prozedurversion) <--> (Protokoll, Portnummer)Man nennt diese Zuordnung die Bindung (engl. Binding).
----------------------------------- | Programm PROGNR | | | | --------------------------- | | | Version VERSNR | | | | | <- | ------> Portnummer | | ------------------- | | | | | Prozedur PROCNR | | | | | ------------------- | | | | . | | | | . | | | --------------------------- | | . | | . | | . | | -------------------------- | | | Version VERSNR_2 | | | | . | | | | . | | | -------------------------- | ----------------------------------- Bild 10Der Portmapper ist ein Daemon-Prozess im Server-Rechner, der einmal (z.B. beim Booten des Systems) gestartet wird. Jeder neu gestartete Server-Prozess muss seine Dienste vom Portmapper registrieren lassen und bekommt dafuer eine Portnummer zugewiesen. Sie kann je nach Rechner oder Situation verschieden sein. Der Portmapper wartet selbst immer am gleichen Port (Nummer 111). Alle neuen Client-Requests greifen zuerst auf diesen Port zu, werden jedoch vom Portmapper an den richtigen Port verwiesen, den sie fuer weitere Kommunikation verwenden. Bei einem Request an einen anderen Server-Dienst wird der Client an einen anderen Port verwiesen. Die Kommunikations-Details dieses Mechanismus werden als Portmapper Protokoll bezeichnet.
------------------ ------------------------- | | | | | | ----------- | | Client | | Port | Server | | | | a | | | | ----------- | | | | | | ------------ | ----------- ---------- | | | | -------------------------> | Port | ---> | Port- | | | | Client- | | ------> | 111 | <--- | mapper | | | | | ----------- | ----------- ---------- | | | Programm | | | | | | | | | <------ | | ----------- | | ------------ | | | Registrierung | Port | | | | | | des Services | b | | | | | | | ----------- | | | | | | | | | | | | | ----------- ----------- | | | | | ------- | Port | ---> | Server- | | | | | ---------------> | c | | program | | | | -------------------- | | <--- | | | | | ----------- ----------- | | | | | ------------------ ------------------------- Bild 11
SUN erlaubt dem Anwender Programmnummern zwischen 0x 20 000 000 und 0x
3f fff fff, und reserviert andere fuer sich (siehe Anhang). Diese Nummer
ist es, unter der Server-Prozess seine Dienste beim Portmapper registriert,
und nach der der Client seinen Request anmeldet (well-known-number).
Diese Liste ist nicht vollstaendig (siehe auch /etc/rpc). Sie wird von
SUN immer wieder erweitert. Um eigene Dienste fest eintragen zu lassen,
wendet man sich an SUN.
0 - 1fffffff
Definiert von SUN
100000
PMAPPROG
Portmapper
100001
RSTATPROG
remote stats
100002
RUSERSPROG
remote users
100003
NFSPROG
NFS
100004
YPPROG
NIS
100005
MOUNTPROG
mount daemon
100006
DBXPROG
remote dbx
100007
YPBINDPROG
NIS Binder
100008
WALLPROG
shutdown msg
100009
YPPASSWDPROG
yppasswd server
100010
ETHERSTATPROG
ether stats
100011
RQUOTAPROG
disk quotas
100012
SPRAYPROG
spray packets
100013
IBM3270PROG
IBM 3270 mapper
100014
IBMRJEPROG
RJE mapper
100015
SELNSVCPROG
selection service
100016
RDATABASEPROG
remote database access
100017
REXECPROG
remote execution
100018
ALICEPROG
Alice Office Automation
100019
SCHEDPROG
scheduling service
100020
LOCKPROG
local lock manager
100021
NETLOCKPROG
network lock manager
100022
X25PROG
x.25 inr protocol
100023
STATMON1PROG
status monitor 1
100024
STATMON2PROG
status monitor 2
100025
SELNLIBPROG
selection library
100026
BOOTPARAMPROG
boot parameters service
100027
MAZEPROG
mazewars game
100028
YPUPDATEPROG
NIS update
100029
KEYSERVERPROG
key server
100030
SECURECMDPROG
secure login
100031
NETFWDIPROG
nfs net forwarder init
100032
NETFWDTPROG
nfs net forwarder trans
100033
SUNLINKMAP_PROG
sunlink MAP
100034
NETMONPROG
network monitor
100035
DBASEPROG
lightweight database
100036
PWDAUTHPROG
password authorisation
100037
TFSPROG
translucent file svc
100038
NSEPPROG
nse server
100039
NSE_ACTIVATE_PROG
nse activate daemon
100043
SHOWHFD
showfh
150001
PCNFSDPROG
pc password authorisation
200000
PYRAMIDLOCKINGPROG
Pyramid locking
200001
PYRAMIDSYS5
Pyramid Sys5
200002
CADDS_IMAGE
CV cadds_image
300001
ADT_RFLOCKPROG
ADT file locking
20000000 - 3fffffff
steht fuer freie Benutzung
40000000 - 5fffffff
voruebergehend besetzt
60000000 - 7fffffff
reserviert
80000000 - 9fffffff
reserviert
a0000000 - bfffffff
reserviert
c0000000 - dfffffff
reserviert
e0000000 - ffffffff
reserviert
Das Portmapper Protokoll ist in der RPC-Sprache angegeben.
const PMAP_PORT = 111 /* portmapper port number */
/*
** A mapping of (program, version, protocoll) to port number
*/
struct mapping {
unsigned int prog;
unsigned int vers;
unsigned int prot;
unsigned int port;
};
/*
** Supported values for port field
*/
const IPPROTO_TCP = 6;
const IPPROTO_UDP = 17;
/*
** A list of mappings
*/
struct *pmaplist {
mapping map;
pmaplist *next;
};
/*
** Arguments to callit()
*/
struct call_args {
unsigned int prog;
unsigned int vers;
unsigned int proc;
opaque args<>;
};
/*
** Results of callit()
*/
struct call_result {
unsigned int port;
opaque res<>;
};
/*
** Portmapper procedures
*/
program PMAP_PROG {
version PMAP_VERS {
void PMAPPROOC_NULL (void) = 0;
bool PMAPPROOC_SET (mapping) = 1;
bool PMAPPROOC_UNSET (mapping) = 2;
uint PMAPPROOC_GETPORT (mapping) = 3;
pmaplist PMAPPROOC_DUMP (void) = 4;
call_result PMAPPROOC_CALLIT (call_args) = 5;
} = 2;
} = 100000;
Beschreibung der Prozeduren:
PMAPPROOC_NULL: | keine Aktion, nur fuer Testzwecke |
PMAPPROOC_SET: | Registrierung |
PMAPPROOC_UNSET: | Macht die Registrierung rueckgaengig |
PMAPPROOC_GETPORT: | Gibt die Portnummer zurueck (program, version, protokol), oder 0, wenn nicht registriert. |
PMAPPROOC_DUMP: | Gibt die Liste aller registrierten Tripel (program, version, protokol). |
PMAPPROOC_CALLIT: | Ruft das angesprochene Programm und sendet die Ergebnisse mit UDP an den Client zurueck, wenn Programm fehlerfrei abgelaufen ist. Wird benutzt bei Broadcasting. |
Das Interface zur RPC - Programmierung ist in drei Schichten unterteilt: 1. High-, 2. Middle- und 3. Low-Level.
High-level RPC: Fuer diese Schicht ist das Betriebssystem, die
Rechnerhardware und das verwendete Netzwerk komplett transparent. Der
Programmierer ruft eine C-Routine auf, die das Netzwerkhandling vollstaendig
verdeckt: z.B. rnusers(), um die Anzahl der eingeloggten Benutzer auf einem
entfernten Rechner zu erfragen. Andere bekannte Routinen sind
gibt Informationen ueber User auf einem entfernten Rechner
| ueberprueft, ob der entfernte Rechner eine Harddisk hat
| erfragt Informationen ueber Rechnerauslastung
| schreibt eine Nachricht an entfernte Rechner
| startet den Password-Update im Network Information Service
| |
Die Programmierschnittstelle zur mittleren Schicht besteht aus drei Bibliotheksfunktionen: registerrpc(), svc_run() und callrpc(), deren Synopsis sich im Anhang befindet. registerrpc() dient dazu, die Existenz einer Dienstroutine auf der Serverseite anzumelden. Mit svc_run() wird die Hauptschleife des Serverprozesses gestartet, die Anfragen des Clients entgegennimmt und beantwortet. Mit callrpc() laesst sich dann der eigentliche Client-Aufruf realisieren. Ein dem rnuser()-Beispiel analoges aus High-level RPC sieht dann so aus:
#include
#include
#include
extern unsigned long *nuser();
main ()
{
registerrpc (RUSERSPROG, RUSERSVERS, RUSERSPROC_NUM,
nuser, xdr_void, xdr_long);
svc_run ();
fprintf (stderr,
"Server: main loop broke down, server died\n");
exit (1);
}
Dieses Programm muss auf dem Server-Rechner als Prozess gestartet werden.
Nach der Registrierung von nuser() mit den Parametern: Programmnummer,
Programmversion, Prozedurnummer und xdr_void/xdr_long als Ein/Ausgabe-Filter
folgt die Endlosschleife svc_run(), innerhalb der der Server auf Anfragen
wartet, sie empfaengt und beantwortet. Dieser Prozess laeuft im Server als
Daemon, der die Client-Anfragen nacheinander beantwortet. Die eigentliche
Server-Prozedur nuser() mit den dabei ueblicherweise verwendeten RUSER...
Parametern ist in unserem Fall in der Standard Library enthalten. Der
dazugehoerige Client-Teil sieht dann so aus:
#include
#include
#include
main (argc, argv)
int argc;
char *argv[];
{
unsigned long n;
int stat = -1;
if (argc > 1) {
if (stat = callrpc (argv[1],
RUSERSPROG, RUSERSVERS, RUSERSPROC_NUM,
xdr_void, 0, xdr_long, &n))
fprintf (stderr, "%s callrpc error %d\n",
argv[0],stat);
printf ("%d users on %s\n", n, argv[1]);
}
exit (0);
}
Die vier letzen Parameter von callrpc sind nacheinander die
Ein/Ausgabefilter und -Daten. Fuer die Datenfelder werden in der Synopsis
von callrpc() Character-Pointer benutzt, um die Uebergabe laengerer Felder
zu ermoeglichen. Die Ein/Ausgabefilter sind Pointer auf Funktionen, an die
spaeter, beim Aufruf zwei Parameter weitergeleitet werden. So ist an dieser
Stelle xdr_long mit:
xdr_long (handle, l)
XDR *handle;
long *l;
erlaubt, xdr_string, der drei Parameter benoetigt, dagegen nicht.
xdr_string() muss man mit einen Wrapper umwickeln, also mit einer Prozedur,
die nur die Argumentenzahl, bzw. -Reihenfolge aufbereitet und selbst
xdr_string() aufruft..
Middle-level RPC-Routinen verstecken Details der RPC-Kommunikation. Manchmal besteht jedoch die Notwendigkeit gerade diese Details zu beeinflussen. In der Praxis kann das drei Faelle betreffen:
--------------------------------- | Middle-level | --------------------------------- | Client-Seite | Server-Seite | ------------ --------------------------------- ------------ | | -------> | callrpc() | registerrpc() | <------- | | | | | | svc_run() | | | | Client | --------------------------------- | Server | | Programm | | | | Programm | | | | | | | | | V V | | | | ----------------------------------------- | | | | | Low-level | | | | | ----------------------------------------- | | | | ---> | Client-Seite | Server-Seite | <--- | | ------------ ----------------------------------------- ------------ | | clnt_create() | | | | | clntudp_create() | svcudp_create() | | | | clnttcp_create() | svctcp_create() | | | | clntraw_create() | svcraw_create() | | | | clnt_destroy() | svc_destroy() | | | | clnt_freeres() | svc_freeargs() | | | | clnt_call() | | | | | clnt_control() | | | | | | svc_register() | | V | | svc_unregister() | V ------------ | | svc_sendreply() | ------------ | | <--- | | svc_getreqset() | ---> | | | XDR | ----------------------------------------- | XDR | | Library | | | | Library | | | | | | | ------------ V V ------------ ------------------------------------- | Transport Library des Netzwerks | ------------------------------------- Bild 12Die Synopsis der Low-level RPC-Routinen befindet sich im Anhang. Wie das rnuser-Server-Programm aus dem vorherigen Beispiel mithilfe von Low-level Routinen implementiert werden kann zeigt das folgende Listing:
#include
#include
#include
#include
main ()
{
SVCXPRT *transp;
int nuser();
transp = svcudp_create (RPC_ANYSOCK);
pmap_unset (RUSERSPROG, RUSERSVERS);
svc_register (transp, RUSERSPROG, RUSERSVERS, RUSERSPROC_NUM,
nuser, IPPROTO_UDP);
svc_run ();
fprintf (stderr, "main loop with dispatcher broke down\n");
svc_unregister (RUSERSPROG, RUSERSVERS);
svc_destroy (transp);
}
nuser (req, transp)
struct svc_req *req;
SVCXPRT *transp;
{
unsigned long n, clnt_data;
switch (req->rq_proc)
case NULLPROC:
svc_sendreply (transp, xdr_void, 0);
return;
case RUSERPROC_NUM:
svc_getargs (transp, xdr_u_int, &clnt_data);
/*
** count user number in n-variable here
*/
svc_sendreply (transp, xdr_u_long, &n);
return ();
default :
svcerr_noproc (transp);
return ();
}
}
Das Transportprotokoll war bei SUN urspruenglich mit Berkeley Sockets
implementiert und wird vermutlich spaeter durch TLI ersetzt. Durch
Lokalisierung dieser Abhaengigkeit zunaechst nur an zwei Stellen im Code:
#include
#include
#include
#include
#include
main (argc, argv)
int argc;
char *argv[];
{
struct hostent *hp;
struct timeval t1, t2;
struct sockaddr_in s_addr;
int sock = RPC_ANYSOCK;
register CLIENT *cp;
unsigned long n;
if (argc != 2) {
fprintf (stderr, "usage: %s hostname\n", argv[0]);
exit (1);
}
t1.tv_sec = 3;
t1.tv_usec = 0;
hp = gethostbyname (argv[1]);
bcopy (hp->h_addr, (caddr_t)&s_addr.sin_addr, hp->h_length);
s_addr.sin_family = AF_INET;
s_addr.sin_port = 0;
cp = clntudp_create (&s_addr, RUSERPROG, RUSERVERS, t1, &sock);
t2.tv_sec = 20;
t2.tv_usec = 0;
clnt_call (cp, RUSERSPROG_NUM, xdr_void, 0, xdr_u_long, &n, t2);
printf ("%d users on %s\n", n, argv[1]);
clnt_destroy (cp);
}
Dieses Stueck Code zeigt in etwa den internen Aufbau der Middle-level
Routine callrpc(). So wird auch deutlich, an welcher Stelle UDP durch TCP
ersetzt werden koennte (Vorsicht: clnttcp_create() hat etwas andere
Argumente als clntudp_create()). Natuerlich muss der Transportmechanismus
fuer den Client und den Server der gleiche sein. Zu erwaehnen sei auch die
allgemeinere Routine clnt_create(), die als letztes Argument "udp" oder
"tcp" enthaelt.
Unter Raw RPC versteht man eine Testumgebung fuer Low-level RPC. Sie besteht aus einem Programm, das sowohl den Client- als auch der Server-Stub enthaelt. Ausgelassen wird die ganze Netzwerkkommunikation. Es folgt ein Beispiel:
#include
#include
#include
main ()
{
CLIENT *cp;
SVCXPRT *sp;
int ni, no;
struct timeval timeout = {0, 0};
sp = svcraw_create ();
svc_register (svc, 200000, 1, server, 0);
cp = clntraw_create (200000, 1);
clnt_call (cp, 1, xdr_int, &ni, xdr_int, &no, timeout);
printf ("input %d output %d\n", ni, no);
}
server (req, transp)
struct svc_req *req;
SVCXPRT *transp;
{
int n;
svc_getargs (transp, xdr_int, &n); /* get client data n */
n++;
svc_sendreply (transp, xdr_int, &n); /* pass n to the client */
}
Die Testumgebung erlaubt es den Client- und den Server-Stub, sowie die
Server-Prozedur beliebig auszubauen und ihre Funktionsweise ohne Netzwerk zu
pruefen. Nach abgeschlossenem Test kann ...raw_create() durch ..._create()
ersetzt werden und das Programm auf Client und Server aufgespaltet werden.
Alle anderen RPC-Routinen sind so gestaltet, dass sie in einem Prozess, der
auch mit Standardwerkzeugen (sdb) debuggt werden kann, zusammenspielen.
Von den drei Middle-level Routinen registerrpc(), svc_run() und callrpc() wurde svc_run noch nicht in einfachere Bestandteile zerlegt. Das kann jedoch notwendig sein, wenn der Server zusaetzlich zu der Endlosschleife mit dem Warten auf einen Request und seiner Bearbeitung noch weitere Aufgaben erledigen soll. Der vollstaendige Code fuer den standard Dispatcher sieht wie folgt aus:
void svc_run ()
{
fd_set readfds;
int dtbsz = getdtablesize ();
for (;;) {
readfds = svc_fdset;
switch (select (dtbsz, &readfds, NULL, NULL, NULL)) {
case -1:
if (errno != EBADF)
continue;
perror ("select");
return;
case 0
continue;
default:
svc_getreqset (&readfds);
}
}
}
Hier sehen wir eine Endlosschleife mit dem Warten auf einen Request mit
select() und der Verzweigung zu der Service-Routine mit svc_getreqset().
svc_getreqset() ruft den eigentlichen Service auf und schickt seine
Ergebnisse an der Client zurueck. In dieser Schleife kann der Benutzer
zusaetzlichen Code einbauen, der meistens mit dem Warten auf andere
Ereignisse und
der Reaktion auf sie verknuepft ist. Durch die richtige Erweiterung der
Maske svc_fdset kann mithilfe von einem select() auf RPC-Requests und
gleichzeitig auf andere Events gewartet werden.
svc_fdset ist eine globale read-only Variable aus der RPC-Bibliothek.
Wenn die entfernte Prozedur mehrere Versionen hat, was bei der Entwicklung oft der Fall ist, ist es sinnvoll, dass sie alle durch den gleichen Dispatcher bzw. mithilfe des Wrappers unterstuetzt werden, z.B.:
main () /* Server */
{
.
.
int proc();
.
.
svc_register (transp, PROGNR, VERS_SHORT, proc, IPPROTO_TCP);
svc_register (transp, PROGNR, VERS_LONG, proc, IPPROTO_TCP);
.
.
svc_run ();
}
proc (req, transp) /* wrapper */
struct svc_req *req;
SVCXPRT *transp;
{
extern short proc1(); /* procedures differ in ret value */
extern long proc2();
static short n1;
static long n2;
switch (req->rq_proc {
case NULLPROC:
.
case PROCNR:
switch (req->rq_vers {
case VERS_SHORT:
n1 = proc1 (req, transp);
svc_sendreply (transp, xdr_short, &n1);
break;
case VERS_LONG:
n2 = proc2 (req, transp);
svc_sendreply (transp, xdr_long, &n2);
break;
}
default:
svcerr_noproc (transp);
}
}
Ist das nicht der Fall, so kann der Client anhand der zurueckgegebenen
Fehlermeldung die unterstuetzte Version herausfinden:
main () /* Client */
{
struct rpc_err err;
short s;
long l;
.
.
cl = clnt_create (host, PROGNR, VERS2, "udp");
switch (clnt_call (cl, PROCNR, xdr_void, NULL, xdr_long,
&l, timeout)) {
case RPC_SUCCESS: /* version ok. */
printf ("%d\n", l);
break;
case RPC_PROGVERSMISMATCH: /* try former version */
clnt_geterr (cl, &err);
if (err.re_vers.high < VERS2) {
clnt_destroy (cl);
cl = clnt_create (host, PROGNR, VERS1, "udp");
clnt_call (cl, PROCNR, xdr_void, NULL, xdr_short,
&s, timeout);
printf ("%d\n", s);
}
break;
}
}
Das hier gezeigte Beispiel lehnt sich an die SUN rcp-Implementierung
an. Es zeigt eine typische TCP-Anwendung. Die stdin wird auf der Client
Seite gelesen, zum Server transportiert und dort von der entfernten Prozedur
rcp auf dem stdout geschrieben. Die Lese/Schreib-Operation uebernimmt dabei
ein untypischer XDR-Filter xdr_rcp(), der gegenueber ENCODE/DECODE
unsymmetrisch ist.
Der Client-Teil sieht so aus:
#include
#include
#include
#include
#include "rcp.h" /* with PROG-, VERS-, PROC */
main (argc, argv) /* client side */
int argc;
char *argv[];
{
int xdr_f();
int socket = RPC_ANYSOCK
struct sockaddr_in server;
struct hostent *hp;
struct timeval tio;
CLIENT *cl;
hp = gethostbyname (argv[1]);
bcopy (hp->h_addr, (caddr_t)&server.sin_addr), hp->h_length);
server.sin_family = AF_INET;
server.sin_port = 0;
cp = clnttcp_create (&server, PROG, VERS, &socket,
BUFSIZ, BUFSIZ);
tio.tv_sec = 20;
tio.tv_usec = 0;
clnt_call (cl, PROC, xdr_f, stdin, xdr_void, 0, tio);
clnt_destroy (cl);
}
Es folgt der Server-Teil:
#include
#include
#include "rcp.h"
main () /* server side */
{
SVCXPRT *transp;
int rpc();
transp = svctcp_create (RPC_ANYSOCK, BUFSIZ, BUFSIZ);
pmap_unset (PROG, VERS);
svc_register (transp, PROG, VERS, rpc, IPPROTO_TCP);
svc_run ();
}
rcp (req, transp) /* remote procedure */
struct svc_req *req;
SVCXPRT *transp;
{
extern int xdr_f();
switch (req->rq_proc) {
case NULLPROC :
svc_sendreply (transp, xdr_void, 0);
break;
case RPCPROC :
svc_getargs (transp, xdr_f, stdout);
svc_sendreply (transp, xdr_void, 0);
break;
default :
svcerr_noproc (transp);
break;
}
}
mit dem folgenden Filter:
#include
#include
xdr_rcp (xdr, fp) /* XDR-Filter */
XDR *xdr;
FILE *fp;
{
unsigned long size = 0;
char buf[BUFSIZ], *p = buf;
if (xdr->x_op == XDR_FREE)
return (1);
while (1) {
if (xdr->x_op == XDR_ENCODE) /* in client */
size = fread (p, sizeof (char), BUFSIZ, fp);
if (!xdr_bytes (xdr, &p, &size, BUFSIZ))
return (0);
if (size == 0)
return (1);
if (xdr->x_op == XDR_DECODE) /* in server */
fwrite (p, sizeof (char), size, fp);
}
}
Unter Broadcast-RPC versteht man die Situation, in der ein Client sich
gleichzeitig an mehrere Server mit einem Call-Request wendet. Natuerlich
muss er mehrere Antworten erwarten koennen. Ein Thread kann jedoch nur eine
Verbindung aufbauen. Verbindungsorientiere Transporte (TCP) kommen aus
diesem Grunde nicht in Frage. Da Broadcasting meistens dann eingesetzt wird,
wenn nicht genau bekannt ist, welcher Server welche Services anbietet,
werden Fehlerantworten auf der Client Seite automatisch ausgefiltert, und
das Client Programm bekommt darueber (z.B. falsche Versionsnummer) keine
Information.
Parameter von Broadcast Call-Requests sind auf eine maximum transfer
unit (MTU) Groesse beschraenkt. Sie betraegt fuer Ethernet 1,5 kByte,
abzueglich des RPC-Header sind das 1,4 k. Die Antworten sind wie gewoehnt
auf UDP-Packetgroesse (z.Zt. 8,8k) beschraenkt. Wie weit Broadcasting
ausgestrahlt wird, haengt von dem Netzwerk ab. Fuer Ethernet sind alle
Hosts im LAN die Empfaenger. Weitere Verbreitung haengt von den Routern ab.
Die Call-Request-Pakete gehen, wie bei jedem Broadcasting, nur einmal
an die Server. Der Weg von Bild 11, in dem sich der Client nach dem Erhalt
der Portmapperinformation zum zweiten Mal, jetzt an den richtigen Port
wendet, wird ausgespart. Der Portmapper muss mithilfe von callit() den
richtigen Serverprozess selbst anrufen.
------------------ ------------------------- | Client | | Server | | | | | | ------------ | ----------- ---------- | | | | -------------------------> | Port | ---> | Port- | | | | Client- | | ------- | 111 | <--- | mapper | | | | | | | ----------- ---------- | | | Programm | | callit() | | | | | | | | ----------- ----------- | | ------------ | ------> | Port | ---> | Server- | | | | | a | <--- | program | | | | ----------- ----------- | | | | | ------------------ ------------------------- Bild 13Broadcasting wird mit nur einem Aufruf clnt_broadcast() anstelle von clnt_call() eingeschaltet. Damit werden Client Broadcast Call-Requests abgeschickt und Antworten empfangen. Die Synopsis sieht wie folgt aus:
enum clnt_stat clnt_broadcast (prognum, versnum, procnum
outproc, outdata, inproc, indata,
eachresult)
u_long prognum,
versnum,
procnum;
xdrproc_t outproc;
caddr_t outdata;
xdrproc_t inproc;
caddr_t indata;
bool_t (*eachresult)();
Dabei sind prog-, vers-, procnum die uebliche Identifikation der
entfernten Prozedur, out/indata Aufruf- und Ergebnisdaten und out/inproc die
zugehoerigen XDR-Filter. Bei jedem Empfang einer Antwort ruft
clnt_broadcast() die Funktion eachresult() mit folgenden Parametern auf:
bool_t eachresult (resultp, raddr)
caddr_t resultp;
struct sockaddr_in raddr;
Diese Funktion muss der Benutzer selbst liefern. Sie muss so gebaut
sein, dass clnt_broadcast() anhand ihres Returnwertes (Typ bool_t) erkennen
kann, ob sie auf weitere Server-Antworten warten soll (FALSE) oder nicht
(TRUE). Bei TRUE kommt clnt_broadcast mit RPC_SUCCESS zurueck. Bei einer
genuegend grossen Zahl der FALSE Ergebnisse von eachresult() wiederholt die
Broadcast-Routine Call-Requests in den Zeitabstaenden von 4, 6, 8, 10, 12
und 14 Sek. Das ist bei nicht verlaesslichem (unreliable) Transport wie UDP
sinnvoll. Nach zusammen 54 Sek. kommt clnt_broadcast() mit RPC_TIMEOUT
zurueck. Ist eachresult() ein Nullpointer, so wird zwar Broadcast
ausgesendet, jedoch auf keine Antworten gewartet.
#include
#include
#include
static char host[HOSTNAME_SIZE+1];
char *findserver ()
{
int reply();
host[0] = 0; /* clean previous result */
if (clnt_broadcast (PROGNR, VERSNR, NULLPROC,
xdr_void, NULL, XDR_void, NULL, reply) == RPC_SUCCESS)
return (host);
return (NULL);
}
reply (data, server)
void *data;
struct sockaddr_in *server;
{
struct hostent *hostentp;
if (hostentp = gethostbyaddr (&server->sin_addr.s_addr,
sizeof (server->sin_addr.s_addr), AF_INET)) {
strncpy (host, hostentp->h_name, HOSTNAME_SIZE);
return (1);
}
return (0);
}
Unter Batching versteht man eine Situation, in der der Client eine
Reihe von Auftraegen ("in Batch") an den Server verschickt ohne auf die
Ergebnisse zu warten. Sinnvollerweise soll der Server in diesem Fall auch
keine Teilergebnisse liefern. Damit wird das Schema vom Bild 1 ausser Kraft
gesetzt: Der Client verliert keine Zeit und kann eventuelle Ergebnisse
spaeter abholen. Waehrend die Auftraege abgearbeitet werden, kann der Client
sogar andere RPC-Requests an den gleichen Server verschicken.
Die Auftraege werden in einem TCP-Puffer positioniert und mit einem
sicheren (reliable) Transport (TPC/IP) an den Server geleitet. Die letzte
Aktion geschieht oft mit einem write() auf einmal. Damit wird die
Netzbelastung vermindert. Das Leeren des TCP-Puffers kann der Client mit
einem Remote-Call ohne Batching erreichen.
Um Batching "einzuschalten" muss der Client folgende Bedingungen erfuellen:
#include
#define NULL ((char *)0)
#define BSIZE 256
main (argc, argv)
int argc;
char *argv[];
{
struct timeval timeout;
register CLIENT *cl;
char buf[BSIZE], *s = buf;
cl = clnt_create (argv[1], PROGNR, VERSNR, "tcp");
timeout.tv_sec = 0;
timeout.tv_usec = 0;
while (scanf ("%s", s) != EOF)
clnt_call (cl, PROCNR, xdr_wrapstring, &s, NULL, NULL,
timeout);
/* flush the TCP-pipeline now */
timeout.tv_sec = 20;
clnt_call (cl, NULLPROC, xdr_void, NULL, xdr_void, NULL,
timeout);
clnt_destroy (cl);
}
Anders als in unserem Client-Server Modell ist es manchmal sinnvoll,
dass der Server eine mehr aktive Rolle als Kommunikationspartner uebernimmt
und selbst Requests (Callbacks) an den bisherigen Client sendet. Dabei muss
der bisherige Client eine Prozedur registriert haben und auf ihren Aufruf
warten.
Ein Beispiel fuer eine solche Situation waere ein entfernt ablaufendes
Programm, das ein lokales Fenstersystem benutzt. Die Benutzereingabe findet
lokal durch die Fenster statt. Sie laesst eine Aktion im entfernten Server
aus, der nicht passiv antwortet, sondern selbst entscheidet welche
Bildumformung im lokalen Client stattfinden soll und die zugehoerige
Fensterfunktion durch einen Callback aufruft.
In dem folgenden Beispiel muss die Callbackroutine jeweils neu registriert
werden. gettransient() waehlt die richtige Programnummer aus dem
voruebergehend (transient) besetzten Bereich zwischen 0x400000000 bis
0x5fffffff.
gettransient (protocol, vers, port)
int protocol;
u_long vers;
u_short port;
{
static u_long prognum = 0x40000000;
while (!pmap_set (prognum++, vers, protocol, port))
continue;
return (prognum - 1);
}
prognum ist static, um die Suche nicht jedes Mal von Anfang an zu starten.
pmap_set() scheitert, solange eine Registrierung ungueltig war.
#include
#include
#include "example.h"
callback (req, transp)
struct svc_req *req;
SVCXPRT *transp;
{
if (req->rq_proc == 1)
fprintf (stderr, "got callback\n");
svc_sendreply (transp, xdr_void, 0);
return (0);
}
main () /* client */
{
int prognum;
char hostname[256];
SVCXPRT *xprt;
gethostbytname (host, sizeof (hostname));
svcudp_create (RPC_ANYSOCK));
prognum = gettransient (IPPROTO_UDP, 1, xprt->xp_port);
svc_register (xprt, prognum, 1, callback, 0);
callrpc (hostname, EXAMPLEPROG, EXAMPLEVERS, CALLBACK,
xdr_int, &prognum, xdr_void, 0);
svc_run ();
}
Der dazugehoerige Server sieht wie folgt aus:
#include
#include
#include
#include "example.h"
int pnum = -1;
char hostname[256];
char *getnewprog (pnump)
int *pnump;
{
pnum = *(int *)pnump;
return (NULL);
}
docallback ()
{
if (pnum == -1) {
signal (SIGALRM, docallback);
return (0);
}
if (callrpc (hostname, pnum, 1, 1,
xdr_void, 0, xdr_void, 0) != RPC_SUCCESS)
fprintf (stderr, "server error\n");
}
main () /* server */
{
gethostbytname (host, sizeof (hostname));
registerrpc (EXAMPLEPROG, EXAMPLEVERS, CALLBACK,
getnewprog, xdr_int, xdr_void);
signal (SIGALRM, docallback);
alarm (10);
svc_run ();
}
Das Beispiel ist nur in einem Rechner lauffaehig, aber die Verteilung
des angegebenen Codes auf zwei Computer bereitet keine Probleme.
RPC bietet drei Stufen der sicheren Identifikation (Identification) und der Pruefung, ob diese Identifikation authentisch ist (Authentication):
/* RPC service request */
struct svc_req {
u_long rq_prog;
u_long rq_vers;
u_long rq_proc;
struct opaque_auth rq_cred;
caddt_t rq_clntcred; /* read only */
};
mit
struct opaque_auth {
enum_t oa_flavor; /* style of credentials */
caddr_t oa_base; /* address of more auth stuff */
u_int oa_length; /* length of oa_base field */
Der Typ des zweiten Feldes (caddt_t) ist absichtlich ein allgemeiner
Zeiger, weil hier je nach Algorithmus Adressen von verschiedenen Strukturen
uebertragen werden: Bei Stufe 2 - Identifizierungen (credential), bei Stufe
3 - Verifikationsfelder (verifier).
struct svc_req req;
CLIENT cl;
req.eq_cred->oa_flavor = AUTH_NONE;
cl.cl_auth = authnone_create();
Auch in diesem Fall besteht jedoch eine Identifizierung des Call-Requests
und des Reply-Messages nach Programm-, Version-, Prozedurnummer
etc., wie das aus der svc_req Struktur ersichtlich ist. Genauere Angaben
darueber stehen im Kapitel ueber das RPC Message Protokoll.
Die zweite Sicherheitsstufe (UNIX-spezifische Identifiakation) wird im Client durch die Besetzung der Variable cl_auth eingeschaltet, die nach der Erstellung des Client-Handles erfolgt:
CLIENT *clnt;
.
clnt = clntudp_create (address, prognum, versnum, waittime, sockptr);
clnt->cl_auth = authunix_create_default ();
Die Funktion authunix_create_default() besetzt automatisch
struct authunix_perms {
u_long aup_time; /* credential creation time */
char *aup_machname; /* client's host name */
int aup_uid; /* client's UNIX effective uid */
int aup_gid; /* client's current group id */
u_int aup_len; /* element length of aup_gids */
int *aup_gids; /* array of groups user is in */
Die Verwendung dieser Felder, die an den Dispatcher des entfernten
Servers transferiert werden, soll folgendes Beispiel verdeutlichen, in dem
die Serverdienste nur auf zwei User uid=104 und uid=105 beschraenkt sind.
nuser (req, transp)
struct svc_req *req;
SVCXPRT *transp;
{
struct authunix_parms *up;
static int n;
if (req->rq_proc == NULLPROC) {
/*
** customary no auth-check for NULLPROC
*/
svc_send_reply (transp, xdr_void, 0);
return;
}
switch (req->rq_cred.oa_flavor) {
case AUTH_UNIX:
up = (struct authunix_parms *) req->rq_clnt_cred;
break;
case AUTH_NULL:
default:
/*
** only weak error if flavor type not appropriate
*/
svcerr_weakauth (transp);
return;
}
switch (req->rq_proc) {
case USERSPROG_NUM:
if (up->aup_uid != 104 ||
up->aup_uid != 105) {
svcerr_systemerr (transp); /* access denied - hard error */
return;
}
/* calculate n */
.
.
svc_send_reply (transp, xdr_u_long, &n);
return;
default:
svcerr_noproc (transp);
return;
}
}
Nach dem Einsatz der Felder dieser Sicherheitsstufe sollte der Client
sie wieder mit:
auth_destroy (clnt->cl_auth);
zerstoeren und damit den belegten Speicherplatz freigeben.
In einem LAN bietet die UNIX-spezifische Identifikation meistens
ausreichende Sicherheit, da die Vortaeuschung eines anderen Partners den
urspruenglichen Partner aus der Kommunikation nicht eliminiert. Das ist
anders bei Gateways: Die richtigen Pakete koennen abgefangen werden, und
dann an ihrer Stelle die falschen geschickt werden. Ausser diesem Problem
der "Schreibberechtigung" kann die "Leseberechtigung" durch
Verschluesselungen auf hoeheren Protokollebenen gut geregelt werden.
Die Diffie-Hellman Methode bietet die Sicherheit, dass die Pakete von
dem angegeben Absender (Netname) kommen. Die Voraussetzung dafuer ist bei
RPC NIS mit publickey.byname und netid.byname Datenbasis. Dabei muss der
oeffentliche Schluessel (publickey) mit DES-Encryption-Kit verschluesselt
werden. DES-Encryption-Kit ist durch Comecon-Beschraenkungen nur in den
Vereinigten Staaten zugelassen und in Europa vertriebenen UNIX-Systemen
zur Zeit nicht vorhanden.
Die dritte Sicherheitsstufe wird durch:
CLIENT *clnt;
.
clnt = clntudp_create (address, prognum, versnum, waittime, sockptr);
clnt->cl_auth = authdes_create (netname, timecred, syncaddr, deskeyp);
eingeschaltet. Dabei ist netname der Netzname des Serverprozesses, der mit:
char *netname[MAXNETNAMELEN+1];
.
user2netname (netname, getuid (), domain);
or
host2netname (netname, rhostname, domain);
berechnet werden kann. Timecred ist die Zeit in Sekunden, wie lange die
abgeschickte Message gueltig ist. Syncaddr ist die Socketadresse, durch die
die Zeitsynchronisation erfolgen kann. Syncaddr kann auf NULL gesetzt
werden. Auf die Serveradresse gesetzt bewirkt sie, dass ohne Synchronisation
nur die Zeit des Servers verwendet wird. deskeyp ist eine ebenfalls
optionale Adresse des verwendeten DES Schluessels.
#include
#include
nuser (req, transp)
struct svc_req *req;
SVCXPRT *transp;
{
struct authdes_cred *dc;
int uid, gid, gidlen, gidlist[10];
.
.
switch (req->rq_cred.oa_flavor) {
case AUTH_DES:
dc = (struct authdes_cred *) req->rq_clnt_cred;
/*
** get user and group for this netname
*/
if (! netname2user (dc->adc_fullname.name,
&uid, &gid, &gidlen, gidlist)) {
fprintf (stderr, "unknown user: %s\n",
dc->adc_fullname.name);
svcerr_systemerr (transp);
return;
}
break;
case AUTH_NULL:
default:
svcerr_weakauth (transp);
return;
}
.
.
}
Wie die meisten Daemon-Prozesse lassen sich RPC-Server vom Internet Services Daemon inetd(8) starten, der anhand des Konfigurationsfiles /etc/inetd.conf die dort spezifizierten Prozesse nur dann anstoesst, wenn deren Services gebraucht werden. Die Eintraege im config-File inetd.conf sehen wie folgt aus:
p_name/version dgram rpc/udp wait/nowait user server args
p_name/version stream rpc/tcp wait/nowait user server args
wobei version auch ein Versionsbereich, z.B. 1-3 sein kann. Bei der inetd-
Benutzung muessen zwei Punkte beachtet werden:
transp = svcudp_create (0);
transp = svctcp_create (0, 0, 0);
transp = svcfd_create (0, 0, 0);
svc_register (transp, PROGNUM, VERSION, service, 0);
RPC-Library stellt eine Reihe Error-Routinen zur Verfuegung. Sie werden
teilweise von anderen RPC-Funktionen aufgerufen oder koennen vom Benutzer
selbst verwendet werden. Im ersten Fall ermoeglicht die Kenntnis ihrer Namen
und Parameter, dass sie vom Benutzer durch eigenen Code ersetzt werden. Das
wird erreicht, indem man dem Linker eigene Routinen vor der RPC-Bibliothek
voranstellt.
Die folgenden Routinen werden im Serverstub aufgerufen. Sie nehmen als
Parameter nur einen Zeiger auf eine SVCXPRT-Struktur und geben void zurueck.
Als Ausnahme braucht svcerr_auth() zwei Parameter. Die meisten
benachrichtigen auch den Client:
svcerr_auth ()
Verletzung eines Sicherheitsmechanismus, (zweiter
Parameter vom Typ enum auth_stat)
svcerr_decode ()
kann die uebertragenen Parametern nicht dekodieren
svcerr_noproc ()
Prozedur mit dieser Nummer wird nicht angeboten
svcerr_noprog ()
Programm mit dieser Nummer wurde nicht registriert
svcerr_progvers ()
Programm mit dieser Version wurde nicht registriert
svcerr_systemerr ()
Systemfehler, jedoch von keinem Protokoll
svcerr_weakaout ()
Sicherheitsmechanismus zwar nicht verletzt, aber
die Sicherheitsstufe zu niedrig
Folgende Routinen werden im Clientstub aufgerufen:
Die Parameter sind von Typ:
void clnt_pcreateerror (str)
Error bei Client-Erstellung (auf stderr)
char *clnt_spcreateerror (str)
" , Meldung nur als Rueckgabewert
void clnt_perrno (stat)
stderr-Ausgabe ueber die Variable stat
char *clnt_sperrno (stat)
", Meldung nur als Rueckgabewert
void clnt_perror (handle, str)
Message ueber fehlerhaften clnt_call
char *clnt_sperror (handle, str)
", Meldung nur als Rueckgabewert
char *str;
enum clnt_stat stat;
CLIENT *handle;
Die Variable clnt_stat (Ergebnis eines RPC-Calls) kann folgende Werte
annehmen, deren Bedeutung am Namen gut zu erkennen ist. Die genaue
Beschreibung befindet sich in den Manual Pages:
RPC_SUCCESS
RPC_CANTENCODEARGS
RPC_CANTDECODERES
RPC_CANTSEND
RPC_CANTRECV
RPC_TIMEDOUT
RPC_VERSMISMATCH Version der ganzen RPC-Bibliothek
RPC_AUTHERROR
RPC_PROGUNAVAIL
RPC_PROGVERSMISMATCH
RPC_PROCUNAVAIL
RPC_CANTDECODEARGS
RPC_SYSTEMERROR
RPC_UNKNOWNHOST
RPC_UNKNOWNPROTO
RPC_PMAPFAILURE
RPC_PROGNOTREGISTERED
RPC_FAILED
Die Kommunikation zwischen Client und Server geschieht bei RPC durch
message-, bzw. PDU- (Protokoll Data Unit) -Uebergabe. Der Client verschickt
die call-message und der Server die reply-message. Diese Messages enthalten
im XDR-Format kodierte Daten. Der Austausch von PDUs zwischen den Instanzen
(Client, Server) erfolgt ueber fest definierte Regeln. Die Summe dieser
Regel wird als Protokoll bezeichnet.
Bei der Implementierung des RPC/XDR Protokolls werden Dienste niedriger
Kommunikationsschichten verwendet. Im RPC-Standard sind keine Angaben zu dem
Transport-Protokoll enthalten. Sie sind der Wahl derer ueberlassen, die RPC
auf einer konkreten Rechnerarchitektur implementieren. In UNIX-Systemen sind
das meistens TCP,UDP/IP mit ihren Zugriffsschnittstellen TLI oder Sockets.
So ist XDR nach dem ISO/OSI Modell in der sechsten, Darstellungsschicht und
RPC in der siebten, der Applikationsschicht, anzusiedeln.
Der folgender Teil dieses Kapitels ist ein kommentierter Ausschnitt aus
RFC1050.
enum msg_type {
CALL = 0,
REPLY = 1
};
enum reply_stat {
MSG_ACCEPTED = 0,
MSG_DENIED = 1
};
/*
* Erfolg und Misserfolg beziehen sich hier nicht
* auf den Ausgang der aufgerufenen Prozedur, sondern
* ausschliesslich auf die RPC-Kommunikation.
* Entsprechend bedeutet der Erfolg nicht, dass die
* entfernte Prozedur ueberhaupt richtig aufgerufen wurde.
*/
enum accept_stat {
SUCCESS = 0,
PROG_UANVAIL = 1, /* remote hasn't exported program */
PROG_MISMATCH = 2, /* remote can't support version # */
PROC_UANVAIL = 3, /* program can't support procedure */
GARBAGE_ARGS = 4 /* procedure can't decode params */
};
enum reject_stat {
RPC_MISMATCH = 0, /* wrong RPC version # */
AUTH_ERROR = 1
};
enum auth_stat {
AUTH_BADCRED = 1, /* bad credential */
AUTH_REJECTEDCRED = 2, /* client must begin new session */
AUTH_BADVERF = 3, /* bad verifer */
AUTH_REJECTEDVERF = 4, /* verifier expired or replayed */
AUTH_TOOWEAK = 5, /* rejected for security reasons */
};
struct rpc_msg {
/*
** xid der Reply-Message gleicht xid von Call-Message.
** xid dient der Identifiakation der richtigen Antwort
** und der Call-Wiederholung, keine laufende Sequenznummer
** wird gesichert.
*/
unsigned int xid;
union switch ( msg_type) {
case CALL:
call_body cbody;
case REPLY:
reply_body rbody;
} body;
};
struct call_body {
unsigned int rpcvers; /* erlaubt verschiedene RPC-Versionen */
unsigned int prog; /* prog, vers, proc identifizieren */
unsigned int vers; /* eindeutig die im Server aufzurufende */
unsigned int proc; /* Prozedur */
opaque_auth cred;
opaque_auth verf;
/* procedure specific parameters start here */
};
union reply_body switch (reply_stat stat) {
case MSG_ACCEPTED:
accepted_reply areply;
case MSG_DENIED:
rejected_reply areply;
} reply;
struct accepted_reply {
opaque_auth verf;
union switch (accept_stat stat) {
case SUCCESS:
opaque results[0]; /* procedure result data */
case PROG_MISMATCH:
struct {
unsigned int low; /* lowest prog_version supported */
unsigned int high; /* highest prog_version supported */
} mismatch_info;
default:
void; /* include PROG_UNAVAIL, PROC_UNAVAIL, GARBAGE_ARGS */
} reply_data;
};
union rejected_reply switch (reject_stat stat) {
case RPC_MISMATCH:
struct {
unsigned int low; /* lowest RPC-version supported */
unsigned int high; /* highest RPC-version supported */
} mismatch_info;
accepted_reply areply;
case AUTH_ERROR:
auth_stat stat;
};
Auf der Protokoll-Ebene besteht die Schnittsstelle zu den Sicherheitsmechanismen aus zwei Feldern in der Service-Request Message: einen fuer die Identifikation (client credential) und einen fuer die Authentizitaetspruefung (Authentication) (client verifier). Reply-Message enthaelt nur ein Feld (server verifier), eine Identifikation ist hier nicht mehr noetig.
struct call_body {
.
.
opaque_auth cred; /* Identification */
opaque_auth verf; /* Verification */
};
mit:
enum auth_flavor {
AUTH_NULL = 0;
AUTH_UNIX = 1;
AUTH_SHORT = 2;
AUTH_DES = 3;
};
struct opaque_auth {
auth_flavor flavor;
opaque body<400>;
};
In der ersten Sicherheitsstufe werden Client-credential (cred) und
Client- und Server-verifier (verf) zu flavor = AUTH_NULL gesetzt.
In der zweiten Sicherheitsstufe (UNIX-Identifikation) wird flavor =
AUTH_UNIX gesetzt. opaque body weist folgende Struktur auf:
struct auth_unix {
unsigned int stamp;
string machinename<255>;
unsigned int uid,
gid,
gids<10>;
};
Der Server-verifier (verf) wird auf AUTH_NULL gesetzt (keine Verifizierung
der Authentizitaet).
/*
* Der Client kann aus Effizienzgruenden ausser in der ersten
* Message einen Spitznamen (Nickname) des Servers benutzen.
* Er wird vom Server vorgeschlagen.
*/
enum authdes_namekind {
ADN_FULLNAME = 0;
ADN_NICKNAME = 0;
};
typedef opaque des_block[8];
const MAXNETNAMELEN = 255;
/*
* Konversationsschluessel ist verschluesselt mit DES
* Zeitfenster ist die Zeitspanne fuer die Gueltigkeit
* der folgenden Messages
*/
struct authdes_fullname {
string name; /* Name des Clients */
des_block key; /* Konversationsschluessel */
unsigned int window; /* Zeitfenster */
};
/*
* Nickname ist meistens der Index in der Tabelle
* des Clients, die der Server fuehrt.
*/
union authdes_cred switch (authdes_namekind adc_namekind) {
case ADN_FULLNAME:
authdes_fullname adc_fullname;
case ADN_NICKNAME:
unsigned int adc_nickname;
};
struct timestamp {
unsigned int seconds; /* seit 1.1.1970 */
unsigned int useconds; /* Mikrosekunden */
};
/*
* Die Komponenten sind verschluesselt mit DES, mode ECB
*/
struct authdes_verf_clnt {
timestamp adv_timestamp;
unsigned int adv_winverf; /* window verifier */
};
/*
* Bei der ersten Transaktion wird stattdessen
* folgende Struktur gebaut und als eine Einheit
* mit DES, mode CBC verschluesselt:
*/
struct authdes_verf_clnt_first {
timestamp adv_timestamp; /* 1 DES Block */
unsigned int w = adc_fullname.window; /* 1/2 DES Block */
unsigned int adv_winverf; /* 1/2 DES Block */
};
/*
* Die Komponenten sind verschluesselt mit DES, mode ECB
*/
struct authdes_verf_svr {
timestamp adv_timeverf;
unsigned int adv_nickname;
};
Client Server
Credential: ckey(key), key(win) ---->
Verifier: key(t1), key(win+1) ---->
<---- Verifier: key(t1-1), nickname
Credential: nickname ---->
Verifier: key(t2) ---->
<---- Verifier: key(t2-1), nickname
.
.
.
.
Bild 14
Die Methode, nach der man als einziger Netzteilnehmer aus dem
gewaehlten Konversationsschluessel key und dem oeffentlichen Schluessel des
Servers den gemeinsamen Schluessel ckey berechnen kann, wurde von Diffie &
Hellman angegeben. Sie beruht auf folgender ueberlegung:
Die Kommunikationspartner waehlen ihre privaten Schluessel A und B.
Fuer die Konstanten BASE und MODULUS berechnen sie die oeffentlichen
Schluessel PA und PB nach der folgenden Regeln:
PA = (BASE ** A) mod MODULUS
PB = (BASE ** B) mod MODULUS
Doppelter Stern bedeutet, wie ueblich die Potenzierung. BASE und MUDULUS
sind:
const BASE = 3;
const MODULUS = "d4a0ba0250b6fd2ec626e7efd637df76c716e22d094"
gesetzt. Fuer spezielle Einstellung in Ihrem system siehe
<rpc/key_prot.h>.
Nur die beiden genannten Partner im ganzen Netzwerk sind in der Lage,
anschliessend den gemeinsamen Schluessel C zu finden, der nach:
C := (PB ** A) mod MODULUS
or
C := (PA ** B) mod MODULUS
berechnet wird. Diese Tatsache beruht auf der Gleichung:
(PB ** A) mod MODULUS = (PA ** B) mod MODULUS
die bei Vernachlaessigung der Modulo-Operation sofort einsichtig wird:
PB ** A = PA ** B
(BASE ** B) ** A = (BASE ** A) ** B
BASE ** (B * A) = BASE ** (A * B) .
Die XDR/RPC-Programmierung beinhaltet oft Editiertaetigkeit, die teilweise automatisiert werden kann. Die Erzeugung von C-Code aufgrund einer etwas allgemeinerer Beschreibung in einer speziell dazu geschaffenen C- aehnlichen Sprache RPC-Sprache (RPCL) kann rpcgen uebernehmen. Es folgt eine Diagrammdarstellung, welche Files aus welchen generiert werden koennen. Die RPCL-Files haben die Extension ".x" .
--------------- --------------- | transf.data | | XDR-filter | -- | structure | --------> | | | | definitions | rpcgen | data_xdr.c | ------ | data.x | --------------- | | --------------- ------------- | | . | remote | | | . | procedure | -------| . | proc.c | | | -------------- . ------------- | | | Server | . --------------- | |------> | executable | . ------> | server stub | | | cc | | . | | file_svc.c | -----| -------------- --------------- | --------------- | | | remote proc.| --- ---------- | | | interface | --------> | header | ----------- | definition | --- | file.h | ------| | file.x | | ---------- | --------------- | --------------- | ------> | client stub | -| -------------- rpcgen | file_clnt.c | | | Client | --------------- |----------> | executable | ------------ | cc | | | client | | -------------- | program | ----- | client.c | ------------ Bild 15Fuer rpcgen gibt es zwei Quellen in RPCL: data.x und file.x, die auch in einer Datei zusammengefasst werden koennen, wie es durch die Punkte angedeutet ist. Data.x enthaelt struct-Definitionen fuer die Parameteruebergabe beim Aufruf der entfernten Prozedur. Aus diesen Definitionen werden XDR-Filter erzeugt. Falls die primitive Filter ausreichen, kann dieser Teil ganz entfallen. File.x enthaelt Definition der Schnittstelle zu der entfernten Prozedur. Aus ihr werden Server- und Client-Stubs erzeugt. Zusaetzlich wird ein Headerfile mit gemeinsamen #define-Konstanten generiert, das in den Stubs included ist. Die entfernte Prozedur und das Client-Programm, das sie aufruft, muss der Benutzer natuerlich selbst schreiben. Die so gelieferten C-Files werden zu dem Client und dem Server-Programm uebersetzt und zusammengebunden. Oft ist es sinnvoll den rpcgen-Aufruf im Makefile zu halten, nur die rpcgen-Quellen zu editieren und die generierten C-Files nicht mehr zu aendern. Einige zusaetzlich zu beachtende Details werden an den Beispielen erlaeutert.
/* msg.x: remote procedure interface definition */
program MESSAGEPROG {
version MESSAGEVERS {
int PRINTMESSAGE (string) = 1;
} = 1;
} = 0x20000099;
Der obige Text im File msg.x kann als Schnittstellenbeschreibung fuer
den Input von rpcgen dienen. Damit werden die ueblichen Angaben ueber die
Programmnummer, Prozedurnummer und Prozedurversion gemacht: in unserem Fall
entsprechend 0x20000099, 1 und 1, sowie Angaben ueber Inputparameter
(String) und Returnwert der Prozedur (int). Der Name der Prozedur, hier
PRINTMESSAGE, wird in dem C-File zu printmessage_1() geaendert. Die
Versionsnummer (hier "_1") wird immer angehaengt. Die Konvention der
Schreibweise mit Grossbuchstaben erleichtert diese Unterscheidung.
Entsprechend muss auch die Prozedur im Client aufgerufen werden. Das
Client-Programm muss vom Benutzer geliefert werden:
#include "msg.h"
main ()
{
CLIENT *cl;
int *result;
char *text = "hello";
.
.
cl = clnt_create (servername, MESSAGEPROG, MESSAGEVERS, "tcp");
.
.
result = printmessage_1 (&text, cl);
.
.
}
#include "msg.h"
int *printmessage_1 (text)
char **text
{
static int result;
.
.
.
return (&result);
}
Die Variable result ist static, weil sie auch nach dem Beenden der
Prozedur printmessage_1() gueltig sein muss. Nicht der Client, der sich in
einem anderen Rechner befindet, sondern der Server-Stub braucht eine
gueltige Adresse, von wo er die Daten (hier ein int) abholt. Falls
printmesage_1() keinen Wert liefert (return (NULL)), wird an den Client auch
keine Antwort geschickt. Void-Prozeduren, die keine Ergebnisse liefern, sind
vereinbarungsgemaess als void-Pointer deklariert ((void *)&variable muss
nicht NULL sein). Natuerlich laesst der Server-Stub in diesem Fall trotzdem
eine Antwort als Bestaetigung der abgeschlossenen Aufgabe aus, jedoch ohne
Ergebnisdaten.
In einem komplexeren Beispiel haben die Parameter- und Ergebnisstypen keine primitiven XDR-Filter, so dass zu den geaenderten rpcgen-Quellen noch die Definitionen dieser Typen hinzukommen:
const MAXLEN = 256;
typedef string msgtext;
struct msglist {
msgtext text;
msglist *next;
};
/* result-typ */
union result switch (int errno) {
case 0:
msglist *list;
default:
void;
}
/* remote procedure interface defintion */
program MESSAGEPROG {
version MESSAGEVERS {
result GETMESSAGE (void) = 1;
} = 1;
} = 0x20000098;
Beachten Sie, dass die Schluesselworte "struct" und "union" vor den
Typnamen in der PRINTMESSAGE-Prototyp-Zeile nicht mehr vorkommen. In Wirk-
lichkeit wandelt rpcgen unions in structs um und fuegt alle noetigen
typedef-Statements hinzu.
Als Ergebnis liefert jetzt rpcgen auch das File msg_xdr.c mit den XDR-
Filtern. Die entfernte Prozedur muss jetzt wie folgt geaendert werden:
#include "msg.h"
#define MSGSZ sizeof(struct msglist)
result *getmessage_1 (void)
{
static result r = { 0 };
register struct msglist *l = r->list, **lp = &l;
register msgtext t;
if (!r.errno) /* free space from previous call */
xdr_free (xdr_msglist, l);
while (t = readtext ()) {
l = *lp = (struct msglist *)malloc (MSGSZ);
l->text = t;
lp = &l->next;
}
lp = NULL;
if (!t) r.errno = errno;
return (&r);
}
Die nichttriviale Konstruktion der Message-Liste koennte auch ohne die
Variable lp erfolgen, die lediglich dazu dient Adressberechnungen zu sparen.
Ausser dem generierten C-Text werden von rpcgen die fuenf folgenden #define-Konstanten angeboten:
RPC_HDR
RPC_XDR
RPC_SVC
RPC_CLNT
RPC_TBL
Durch die Zeile: #ifdef RPC_... im x-Source koennen sie dazu dienen C-Text
einer .x-Quelle nur an den Server- oder nur an den Clientstub zu
transferieren. Das %-Zeichen am Anfang der Zeile dient im x-Source als
Kommentarzeichen. Der nachfolgende Text wird ohne %-Zeichen an die
Ergebnisdateien
weitergereicht. Dies kann sinnvoll dazu eingesetzt werden die generierten C-
Files nach Moeglichkeit nicht mehr zu editieren.
rpcgen -DDEBUG proto.x
Mit der "-s" Option kann nur die Erzeugung der Server-Seite erreicht
werden. Mit "-I" wird ein Code generiert, der inetd unterstuetzt (siehe
hierzu $5.7). Mit "-K Zahl" kann die Laenge des defaultmaessig zweiminuetigen
Wartens vor dem Ausstieg zum inetd veraendert werden.
Die "-T" Option erzeugt Dispatchtabellen im .i-File. Der Tabelleneintrag hat folgende Struktur:
struct rpcgen_table {
char *(*proc)(); /* Service Routine */
xdrproc_t xdr_arg; /* Zeiger auf den Input des XDR-Filters */
unsigned len_arg; /* Laenge in Bytes des Input Argumentes */
xdrproc_t xdr_res; /* Zeiger auf den Output des XDR-Filters */
unsigned len_res; /* Laenge in Bytes des Output Argumentes */
};
Mit solchen Tabellen kann man den Serverstub fuer viele Serviceroutinen
gleichzeitig erzeugen. Um nicht aufloessbare Linker-Referenzen zu vermeiden,
werden die Eintraege mit RPCGEN_ACTION initialisiert. Mit
#define RPCGEN_ACTION(routine) 0
oder
#define RPCGEN_ACTION(routine) routine
kann der gleiche Sourcecode sowohl im Client als auch im Server verwendet
werden.
Die RPC-Sprache (RPCL) ist eine Erweiterung der XDR-Sprache (siehe XDR Protokoll - Spezifikation, Syntax) um folgende drei Elemente:
program-def:
"program" identifier "{"
version-def
version-def *
"}" "=" constant ";"
version-def:
"version" identifier "{"
procedure-def
procedure-def *
"}" "=" constant ";"
procedure-def:
type-specifier identifier "(" type-specifier ")"
"=" constant ";"
sowie zusaetzliche keywords: "program", "version".
XDR/RPC gehoert heute zum festen Bestandteil von fast jedem UNIX-System.
Wohl das wichtigste Projekt, das auf RPC aufsetzt, ist CORBA (Common Object Request Broker Architektur), das von den groessten Softwarehersteller gemeinsam (OMG - Objekt Managament Group) verfolgt wird. Mithilfe von CORBA ist es moeglich in einem Netzwerk netzwerktransparent Objekte zu Verwalten und ihre Dienste in Anspruch zu nehmen. Solche Objekte sind Zusammenschluesse von einem Daten- und Programmteil, der die Zugriffsmethoden (methods) auf diese Daten enthaelt. Damit ist es moeglich im Netzwerk Programme ueber Datenstrukturen auszufuehren ohne Kenntnis, wo die Objekte liegen und auf welchen Rechner die Programme effektiv ablaufen (distributed data / distributed processing). Fuer das Verstaendnis von CORBA ist die Kenntnis von RCP eine Voraussetzung.
Ebenfalls auf XDR/RPC baut das SUN-Produkt ToolTalk. Es definiert ein Pro- tokoll auf der Applikationsebene des ISO-OSI Modells. Mit ToolTalk gebaute Applikationen sind in der Lage ihre Daten auf einfache Weise auszutauschen.
Abstract Syntax Notation One (ASN.1) ist eine XDR-aehnliche Sprache von ISO/OSI. Sie enthaelt als Elemente z.B. folgende Datentypen:
boolean |
integer |
bit string |
octet string |-- einfache Datentypen
null |
object identifier |
object descriptor |
external |
sequence of |-- zusammengesetzte Datentypen
set of |
numeric string |
printable string |
teletex string |-- Strings
videotex string |
IA5string |
generalized time |
UTCTime |-- andere Datentypen
graphic string |
general string |
Die Schnittstellenspezifikationen in ASN.1 sind abstrakter, weisen aber
auch groessere Flexibilitaet auf.
Das vorliegende Skript ist als Arbeitsgrundlage fuer ein Praktikum konzipert
und ist fuer ein Studium ohne Zugang zu einem lauffaehigen
RPC-System wenig geeignet. Auf viele selbstverstaendliche Details, die bei
den Uebungen automatisch erlernt werden, wurde verzichtet.
Kursdauer:
In ein bis zwei Wochen kann bei Kursteilnehmern mit sehr guter Vorbil-
dung alles Notwendige gesagt werden, so dass sie in der Lage sind mithilfe
des Skripts und des Referenz Manuals selbstaendig Applikationen zu entwickeln.
Die normale Kursdauer, ohne Protokoll-Spezifikationen, betraegt
drei bis vier Wochen. Fuer Personen, die RPC auf einer Rechnerarchitektur
neu portieren moechten, ist eine mindestens einwoechige detaillierte
Auseinandersetzung mit Protokoll-Spezifikationen unerlaesslich.
Voraussetzungen der Kursteilnehmer:
http://www12.w3.org/History/1992/nfs_dxcern_mirror/rpc/doc/Introduction/Abstract.html http://docs.sun.com/
Begriff | Beschreibung |
API | Application Programmers Interface |
ASN.1 | Abstract Syntax Notation One |
asynchron | wahlfrei, ohne festgelegte Zeitgrenzen, wie ein Takt |
synchron | nach einem vorgegebenen Zeitmuster, z.B. nach einem Takt |
Big Endian | die Reihenfolge der Bytes in hoeheren Objekten wie Word, DoubleWord, Integer: das hoeherwertige Byte zuerst |
Little Endian | das niederwertige Byte zuerst |
Client/Server | Partner im Netzwerk: Der Server bietet seine Dienste dem Client an und fuehrt sie auf ein Request aus |
CORBA | Common Object Request Broker Architektur |
Courier | erste RPC Version (bei Xerox) |
DCE | Distributed Computing Environment: eine Spezifikation von OSF, enthaelt u.a. RPC, Nameserver und Autorisierungsdienste |
DES | Data Encryption Standard (Patent IBM) |
dispatcher | Verteiler |
entfernt | verbunden durch ein Netzwerk, nicht lokal |
lokal | ohne Netzwerk erreichbar |
gutbekannte Information | Information, die bereits vor der eigentlichen Kommunikation den Partnern im Netzwerk bekannt ist, englisch: well known number (z.B. die Telefonnummer) |
Hostadresse | Adresse des Rechners im Netzwerk |
Netzwerkadresse | Adresse des ganzen Netzwerks |
IEEE | Institute for Electrical and Electronic Engineers, Aussprache: Ai-triple-i, |
IEEE-Float | ein Format fuer die Kodierung der Fliesskommazahlen |
IP | Internet Protokoll |
ISO | International Standard Organisation |
MTU | maximum transfer unit (Datalink Layer) |
NCS | Network Computing System, entwickelt bei Apollo/Hewlett Packard |
NDR | Datenformat in NCS |
NIS | Network Information System |
NFS | Network File System |
ONC | Open Network Computing: RPC von Sun |
OSF | Open Software Foundation |
OSF/1 | ein UNIX-basiertes Betriebssystem von OSF |
OSI | Open Systems Interconnection |
PDU | Protocoll Data Unit |
Portmapper | Daemon Prozess im Server, der die Zuordnung der Portnummer zu den einzelnen Diensten verwaltet |
Portnummer | Adresse einer Resource innerhalb eines Rechners |
POSIX | Portable Operating Systems Environment Committee |
Programmnummer | die Nummer, nach der der Portmapper das richtige Programm identifiziert |
Propagation | Ausbreitung |
RCX | Remote Command Execution |
RFC | Request for Comment: Veroeffentlichungsart |
ROSE | Remote Operation Service Element: RPC von OSI |
RPC | Remote Procedure Call |
RPCL | RPC Language: eine Erweiterung der XDR-Sprache |
rpcinfo(8c) | zeigt die beim Portmapper aktuell registrierten Programme |
REX | Remote execution |
SVR4 | UNIX System V Release 4 von AT&T |
Sockets | Berkeleys Interface zur Transportschicht |
SPARC-Station | ein Mini von Sun gebaut mit dem RISC Processor SPARC |
SPARC | scalable processor architecture (SUN) |
RPCTOOL | ein Produkt der Firma Netwise |
Transaktion | eine Zusammenfassung von mehreren Anweisungen, die als eine Einheit, nicht unterbrochen durch andere Aktivitaeten, ausgefuehrt werden muss |
TCP | Transport Control Protocol |
TLI | Transport Layer Interface, Konkurrenz zu Berkeley Sockets |
UDP | User Datagram Protocol |
XDR | eXternal Data Representation |
zustandsloser Server | Server, der seinen Zustand in Bezug auf die Requests des Clients nicht aendert |