R P C - Remote Procedure Calls
(Aufruf entfernter Prozeduren)

Autor: Zbigniew Lisiecki
Version 1.4, 21 Jan 2004
po polskupo polsku english english deutsch deutsch pa russkij по руccкий translate by google translate


Der Text beschreibt die Netzwerkprogrammiertechnik RPC. Sie wird verwendet um einen Prozeduraufruf ueber die Grenzen eines Rechners hinaus zu erreichen. Ein Datenformat XDR fuer die Spezifikation der Aufrufparameter und Rueckgabewerte wird angegeben.


Vorwort


Die folgende Abhandlung entstand nach einem zweiwoechigen Kurs im Winter 1993 bei der Firma ISP-DATA in Muenchen. Der praktische Teil wurde mit SUN's ONC auf SPARC-Station2 unter dem Betriebssystem SunOs 4.1 / Solaris durchgefuehrt. Programmbeispiele wurden in Anlehnung an den Network Programming Guide von SUN Rev. A 27.3.1990 entwickelt. Um die Programm-Listings uebersichtlich zu halten, wurde die sonst notwendige Fehlerbehandlung nicht angegeben.

Ich bitte den Leser, den Autor wegen den sprachlichen Unzulaenglichkeiten zu entschuldigen. Um den Zugang zur Sekundaerliteratur zu erleichtern, wurden viele zum Computerslang gehoerende englische Termini verwendet. Der Autor ist fuer alle Hinweise auf Fehler und Vorschlaege ueber bessere Darstellung sehr dankbar.

Dieses Dokument unterliegt der Copyright nach GNU Public Licence.

  1. Die Verteilung einer Anwendung in einem Netzwerk
    1. Der RPC-Mechanismus
    2. 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 1
      
      Die 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 2
      
      RPC 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 3
      
      Da 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.

    3. Geschichte der RPC-Versionen
    4. 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:

      1. Network Computing System (NCS), entwickelt bei Apollo/Hewlett Packard mit dem entsprechenden Datenformat, Network Data Representation (NDR),

      2. RPCTOOL von der Firma Netwise

      3. Distributet Computing Environment (DCE), implementiert in dem Betriebssystem OSF/1.


      Dieser Abhandlung liegt die SUN Version zugrunde.

    5. Server mit und ohne Zustand
    6. 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.

  2. XDR - eXternal Data Representation
    1. Das Prinzip von XDR
    2. 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.

      1. Verschiedene Prozessoren verwenden verschiedene Zahlendarstellungen wie Big-, Little-Endian, 1- oder 2-Komplement, IEEE-Floats, oder BCD (Binary Coded Decimal).

      2. Der gleiche Datentyp kann je nach Rechner verschiedene Anzahl von Bytes besetzen: z.B. 16- oder 32-Bit Integer.

      3. Verschiedene Programmiersprachen koennen abweichende Darstellungen der Datentypen haben, wie z.B. Strings mit 0 am Ende oder mit der Stringlaenge im ersten Byte.

      4. Es gibt prozessor- oder compilercharakteristische Daten- Positionierung: (Alignment). Sie fuehrt zu Verschiebungen der Adressen oder zu den "Loechern" in struct-Gebilden.

      5. Programmdaten, die zwischen den Rechnern ausgetauscht werden sollen, koennen im Speicher komplizierte Gebilde mit Pointer usw. bilden. Bei der Uebertragung geht der topologische Bezug auf die Speicher- adressen verloren. Die Daten muessen in einen Byte-Strom umgewandelt ("serialising") und dann wieder zurueckgewandelt ("deserialising") werden.


      Das von SUN vorgeschlagene portable Datenformat eXternal Data Represen- tation (XDR) kommt diesen Erfordernissen entgegen.
                         serialising                deserialising
                             |                           |
                             V                           V
      
      -------------      ----------                  ----------      -------------
      |           | ---> |  XDR - |                  |  XDR - | ---> |           |
      |           |      | Filter1| ---------------> | Filter1|      |           |
      |           |      ----------                  ----------      |           |
      | App1 z.B. |                                                  | App2 z.B. |
      |           |                  Datentransfer                   |           |
      |  Client   |                                                  |  Server   |
      |           |      ----------                  ----------      |           |
      |           |      |  XDR - | <--------------- |  XDR - |      |           |
      |           | <--- | Filter2|                  | Filter2| <--- |           |
      -------------      ----------                  ----------      -------------
      
                             ^                           ^
                             |                           |
                       deserialising                serialising
      
      Bild 4
      
      Die XDR-Bibliothek liefert Funktionen, die diese Aufgaben groesstenteils automatisch erledigen koennen. Sie teilen sich in zwei Gruppen:

      1. Funktionen, die Datenstroeme erzeugen und manipulieren (siehe 2.1)

      2. Konvertierungsfilter, die Eingangsdaten in einen Strom und zurueck umwandeln. (siehe 2.2)


      XDR Funktionen sind orientiert auf den Einsatz in C-Programmen, aber nicht unbedingt unter UNIX. Insbesondere wird das UNIX I/O System nicht vorausgesetzt. Als Beispiel ermoeglichen MS-DOS Portierungen den Datenaustausch zwischen UNIX und DOS, bei dem der Zahleninhalt erhalten bleibt.

      XDR Anwendung ist auch nicht auf RPC beschraenkt. Sie bietet z.B. eine Moeglichkeit Teilergebnisse einer Datenverarbeitung zu speichern, die spaeter anderen Programmen (moeglicherweise in anderen Rechnern) zur Verfuegung gestellt werden sollen.

      Die lokale syntaktische Beschreibung der Daten wird bei uns die Sprache C sein. Mithilfe von XDR erfolgt die Umwandlung in eine XDR-Form. Die zugehoerige Sprache ist die XDR-Sprache mit ihrer sog. Transfersyntax, bzw. deren abstrakten Beschreibung sog. "abstrakten Syntax". Als Beispiel kann der Typ der Abstrakten Syntax "boolean" dienen. Seine Darstellung in der Transfersyntax koennte aus den Werten 0 oder 1 bestehen und die Darstellung in der lokalen Syntax z.B. aus den Werten 'Y' oder 'N' in einem Rechner und 'J', oder 'N' in einem zweiten. Ein Tupel (Abstrakte Syntax, Transfersyntax) wird bei ISO/OSI auch als "Presentation Context" bezeichnet. In unserem Beispiel: (bool, {0,1}).

    3. XDR-Streams
    4. Die Operationen der Serialisierung/Deserialisierung von Daten beziehen sich auf Datenstroeme, die realisiert werden entweder

      1. durch Standard-IO, dh. mithilfe von FILEs aus <stdio.h>,

      2. direkt durch einen Puffer im Arbeitsspeicher (Memory Streams),

      3. oder allgemein durch einen Mechanismus, der noch genauer spezifiziert werden muss (Record Streams)

      Ausser diesen bereits vorhandenen Streams ist es auch moeglich, eigene Custom-Streams zu definieren, indem man zu den entsprechenden Funktionen aus der XDR-Library selbst eigene dazuschreibt. Alle Angaben ueber den jeweils eingesetzten Strom werden in einer Struktur XDR gehalten. Sie ist in <rpc/xdr.h> deklariert:
      
      
              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 5
      
      Die gueltigen Operationen, die XDR Struktur vermittelt, sind:
      
      
      	enum xdr_op {
      		XDR_ENCODE,    /* Serialisierung             */
      		XDR_DECODE,    /* Deserialisierung           */
      		XDR_FREE       /* befreit den Speicherplatz  */
      	};
      
      

      1. Standard IO-Streams
      2. 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.

      3. Memory Streams
      4. 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.

      5. Record Streams
      6. 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 6
        
        Der 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 7
        
        Drei 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.

      7. XDR-Macros zur Strom-Manipulation.
      8. 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.

    5. XDR - Filter
    6. 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.

      1. Primitive Filter
      2. 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);
        

      3. Composite Filter
      4. 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:

        1. strings,
        2. byte-arrays,
        3. opaque data,
        4. arrays,
        5. fixed size arrays
        6. discriminated unions
        7. pointer

        Sie koennen wie primitive Filter ebenfalls in custom-Filter benutzt werden.
        
        
        	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 8
        
        Der 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:
        1. discr, mit der Angabe der zur Uebertragungszeit gueltigen Komponente, die zuerst transformiert wird,

        2. unp, ein Pointer auf die eigentlichen Daten der Variante,

        3. arms, der Vektor von struct xdr_discrim, mit den Angaben fuer jede Komponente. Dieser Vektor muss mit NULL beendet sein.

        4. defaultarm kann ein NULL-Pointer sein. Sonst steht an dieser Stelle der Filter, der aufgerufen wird, wenn discr mit keinem value in dem arms-Vektor uebereinstimmt.

        Wir geben einen Beispiel der Anwendung von xdr_union():
        
        
        	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.

      5. Custom Filter
      6. 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.

      7. Filter-Beispiele
      8. 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.

    7. Die XDR-Protokoll-Spezifikation
      1. XDR-Datentypen
      2. 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.

        Unions (Varianten) verzweigen auf mehrere Aeste, die verschiedene fuer die Union zulaessige Typen beschreiben, von denen nur einer zur bestimmten Zeit gueltig ist. Bei der Umwandlung in einen XDR-Stream muss der zur uebertragungszeit gueltige Typ durch eine Diskriminante (Kennung) angegeben werden. (Discriminated Union = Variante mit Kennung). XDR kennt keine Unions ohne Kennung. Der Diskriminator (Kennung) wird als unsigned Integer zuerst uebertragen. Danach folgen alle fuer die Union zulaessigen Aeste nacheinander mit jeweils einer Typangabe (unsigned Integer) davor. Die Kennung der ganzen Union muss mit einem der angegebenen Typen uebereinstimmen. Die Syntax ist:
        
        
                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.

        Die XDR-Datentypen zusammengefasst:

        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


      3. Die XDR-Syntax
      4. 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 "*/"
        
        

  3. Der Portmapper Network Service
    1. Das Funktionsprinzip
    2. 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 9
      
      Unter 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.

      Eine feste Abbildung der Server-Dienste in die Portnummer hat sich ebenfalls nicht als sinnvoll herausgestellt. Man muss ja auch an andere als lediglich RPC Benutzer der Porte denken. Die flexible Abbildung der Server-Dienste in die Portnummer wird von dem sog. Portmapper realisiert:
      	(Programmnummer, Prozedurversion) <--> (Protokoll, Portnummer)
      
      Man nennt diese Zuordnung die Bindung (engl. Binding).

      Bei RPC werden die Server-Dienste (Prozeduren) nicht durch den Namen sondern durch (Programmnummer, Version, Prozedurnummer) angesprochen. Dem Linker bekannte Prozedurnamen koennen ja aus dem Kompilat entfernt werden.

                  -----------------------------------
                  |      Programm PROGNR            |
                  |                                 |
                  |  ---------------------------    |
                  |  |   Version  VERSNR       |    |
                  |  |                         | <- | ------> Portnummer
                  |  |  -------------------    |    |
                  |  |  | Prozedur PROCNR |    |    |
                  |  |  -------------------    |    |
                  |  |    .                    |    |
                  |  |    .                    |    |
                  |  ---------------------------    |
                  |    .                            |
                  |    .                            |
                  |    .                            |
                  |  --------------------------     |
                  |  |   Version  VERSNR_2    |     |
                  |  |     .                  |     |
                  |  |     .                  |     |
                  |  --------------------------     |
                  -----------------------------------
      
                  Bild 10
      
      Der 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.

      Die Prozedurnummer wird nicht registriert, sondern an die registrierte Serviceroutine zur abschliessenden Verzweigung weitergereicht.

           ------------------                             -------------------------
           |                |                             |                       |
           |                |                        -----------                  |
           |   Client       |                        | Port    |     Server       |
           |                |                        |  a      |                  |
           |                |                        -----------                  |
           |                |                             |                       |
           | ------------   |                        -----------      ----------  |
           | |          | -------------------------> | Port    | ---> | Port-  |  |
           | | Client-  |   |                ------> | 111     | <--- | mapper |  |
           | |          | -----------        |       -----------      ----------  |
           | | Programm |   |       |        |            |                       |
           | |          | <------   |        |       -----------                  |
           | ------------   |   |   |  Registrierung | Port    |                  |
           |                |   |   |  des Services  |  b      |                  |
           |                |   |   |        |       -----------                  |
           |                |   |   |        |            |                       |
           |                |   |   |        |       -----------      ----------- |
           |                |   |   |        ------- | Port    | ---> | Server- | |
           |                |   |   ---------------> |  c      |      | program | |
           |                |   -------------------- |         | <--- |         | |
           |                |                        -----------      ----------- |
           |                |                             |                       |
           ------------------                             -------------------------
      
           Bild 11
      


      Die Portmapper-Dienste sind mit Zustand (statefull). Nach dem Neustart des Portmappers muessen alle Server-Prozesse noch einmal registriert werden. Der aktuelle Zustand des Portmappers kann mit rpcinfo(8c) abgefragt werden.

    3. Die Programmnummer
    4. 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


    5. Das Portmapper Protokoll
    6. 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.

  4. RPC-Programmierung I
  5. Das Interface zur RPC - Programmierung ist in drei Schichten unterteilt: 1. High-, 2. Middle- und 3. Low-Level.

    1. High-level RPC
    2. 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

    3. rusers
    4. gibt Informationen ueber User auf einem entfernten Rechner
    5. havedisk
    6. ueberprueft, ob der entfernte Rechner eine Harddisk hat
    7. rstat
    8. erfragt Informationen ueber Rechnerauslastung
    9. rwall
    10. schreibt eine Nachricht an entfernte Rechner
    11. yppasswd
    12. startet den Password-Update im Network Information Service
      Die Routinen des High-levels von RPC sind in der Bibliothek librpcsvc.a als fertige Objekt-Module enthalten. Ihre vollstaendige Auflistung findet man im R3-Teil der Standard UNIX Manual Pages.

      Wegen der Einfachheit der Programmierarbeit wird diese Schicht bei vielen Autoren (z.B. Corbin) gar nicht erwaehnt, das Programm-Interface wird nur in zwei Schichten geteilt und Middle-level von RPC als High-level bezeichnet. Die vorliegende Beschreibung haelt sich an den Sprachgebrauch von SUN.

    13. Middle-level RPC
    14. 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..

      Alle Details des Netzwerktransportes sind in den drei Prozeduren registerrpc(), svc_run() und callrpc() versteckt. Bei Middle-level RPC wird UDP/IP verwendet, so dass die Ein/Ausgabedaten die Packetgroesse von derzeit 8K nicht uebersteigen sollen. Siehe dazu auch UDPMSGSIZE in <rpc/clnt.h>.

    15. Low-level RPC
    16. 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:

      1. Man moechte mit TCP statt UDP die Grenze der Packetgroesse umgehen, oder andere Charakteristika der Kommunikation anpassen.

      2. Man moechte dass die RPC-Routinen den Speicherplatz fuer die Uebertragenen Daten selbst allokieren. Die Vorgehensweise wurde im Kapitel ueber XDR erklaert.

      3. Man moechte sicherstellen, dass nur ausgewaehlte Benutzer die Serverdienste in Anspruch nehmen koennen, und verlangt dafuer Berechtigungsnachweise.
      In diesen Faellen muessen die Middle-level Routinen in ihre Low-level Bestandteile zerlegt werden, mit denen man anschliessend den eigenen Middle-level RPC realisieren kann. Diese Zerlegung wird in der folgenden Skizze sichtbar:

                            ---------------------------------
                            |          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 12
      
      Die 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:
      1. auf der Server Seite - die Struktur SVCXPRT in

      2. auf der Client Seite - die Struktur CLIENT in
      ist die Portierung auf andere Protokolle einfach. Die dazu notwendigen Sourcen von RPC-Routinen sind bei SUN kostenlos erhaeltlich. Es ist aus dem Grunde empfehlenswert moeglichst wenig von Sockets API zu benutzen.

      Das Argument in svcudp_create(), RPC_ANYSOCK, bewirkt, dass ein neuer Socket generiert wird. Stattdessen kann hier auch die Nummer von einem bereits existierenden Socket angegeben werden. Bei einem gebundenen Socket muessen dann die Portnummer von svcudp_create() und clntudp_create() uebereinstimmen.

      pmap_unset() bewirkt, dass die vorherige Portmapper-Registrierung mit den gleichen Nummern geloescht wird. In diesem Fall sind die zwei letzten Zeilen von main() eigentlich ueberfluessig.

      Beachten Sie, dass weder die Angabe der Prozedurnummer noch die des xdr-Filters bei svc_register() noetig ist, wie das bei registerrpc() der Fall war. Die Prozedurnummer wird an den Dispatcher nuser() weitergereicht, der in registerrpc() noch vorhanden war und die letzte Verzweigung nach dieser Nummer vornahm.

      Die beiden switch-Faelle in nuser() case NULLPROC und default werden bei Middle-level RPC von registerrpc() abgedeckt. Es empfiehlt sich NULLPROC nicht auszulassen um z.B. dem Client die Moeglichkeit zu geben mit dem NULLPROC-Aufruf zu pruefen, ob der Server-Prozess ueberhaupt laeuft.

      Mit der Zeile mit svc_getargs(), die in dem Beispiel nicht notwendig ist, koennen in clnt_data Aufrufargumente des Clients geholt werden. Der entsprechende Client-Stub sieht wie folgt aus:
      
      
              #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.

      t1 und t2 sind timeout-Zeiten in Sekunden jeweils zwischen den einzelnen Kommunikationsversuchen und zusammen. Die getroffenen Einstellungen lassen sich auch im nachhinein mit clnt_control() veraendern.

      Nur bei der Voreinstellung: sin_port = 0 ist der entfernte Portmapper aufgefordert den eigentlichen Port anhand von Programmnummer und Version zu finden.


  6. RPC-Programmierung II, / vertiefte Methoden
    1. Debugging mit Raw RPC
    2. 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.

    3. Aufbau eines Server-Dispatchers
    4. 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.

      Damit der Server nach dem Auftragsempfang gleich wieder einen neuen Auftrag empfangen kann, muss er mit der Auftragsbearbeitung einen Kindprozess betrauen. Damit der Dispatcher keine leeren Ergebnisse an den Client zurueckschickt bevor die entfernte Prozedur zu Ende abgelaufen ist, muss fork() im Dispatcher aufgerufen werden.

    5. Server mit verschiedenen Versionen
    6. 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;
                  }
              }
      
      

    7. Beispiel einer TCP-Anwendung
    8. 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);
                  }
              }
      
      

    9. Techniken der asynchronen Kommunikation
      1. Broadcasting
      2. 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 13
        
        Broadcasting 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.

        resultp ist der Zeiger auf Ergebnisdaten nach ihrer XDR-Umwandlung mit inproc(), raddr die Adresse des jeweiligen Servers.

        Broadcast-RPC wendet standardmaessig den AUTH_UNIX Sicherheitsmechanismus an, den man auch nicht auf andere umschalten kann.

        Es folgt ein Beispielprogramm, das es moeglich macht, den richtigen Host im LAN festzustellen, der die gesuchte Prozedur als Server anbietet.
        
        
                #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);
                }
        
        

      3. Batching
      4. 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:

        1. Als Transport muss TCP gewaehlt werden.

        2. XDR-Filter fuer Ergebnisse muss NULL sein.

        3. timeout muss Null sein.

        Die Bedingungen fuer kein Warten auf Antworten (2. & 3.) tretten auch oft ohne Batching (ohne TCP) auf, wenn z.B. der Timer-Client in Zeitintervallen unzuverlaessige (unreliable) Messages mit dem Time-Update an den/die Server schickt und an den Antworten nicht interessiert ist. Solche Situationen (keine Antworten) bezeichnet man auch als nonblocking RPC.

        Hier ist ein Beispiel fuer Batching:
        
        
                #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);
                }
        
        

      5. Callback-Prozeduren
      6. 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.

        Der Callback-Mechanismus birgt die Gefahr von Deadlocks in sich.

    10. Sicherheitsmechanismen
    11. RPC bietet drei Stufen der sicheren Identifikation (Identification) und der Pruefung, ob diese Identifikation authentisch ist (Authentication):

      1. keine spezielle Sicherheitsmechanismen

      2. UNIX-spezifische Identifikation

      3. Diffie-Hellman/DES Authentizitaet


      1. keine spezielle Sicherheitsmechanismen
      2. Die Programmierschnittstelle zu den Sicherheitsmechanismen besteht aus zwei Feldern in der Service-Request Struktur: einem fuer die grobe Information ueber die eingesetzten Algorithmen (rq_cred) und einem fuer die algorithmusspezifischen Daten (rq_clntcred).
        
        
                /* 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).

        Darueberhinaus gibt es ein Feld in der CLIENT-Struktur: cl_auth. Selbstverstaendlich gehoeren zur vollstaendigen Interface-Definition noch die Angaben darueber, wie diese Felder von RPC gehandhabt werden.

        In der ersten Sicherheitsstufe (defaultmaessig) wird oa_flavor in rq_cred mit AUTH_NULL und cl_auth mit authnone_create() automatisch vor- besetzt.
        
        
                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.

        In der zweiten Stufe (UNIX-spezifische Identifikation) werden in dem rq_clntcred-Feld UNIX-spezifische Informationen wie der Rechnername, User-Id, Group-Id, etc. uebertragen. Das dient nur einer zusaetzlichen Identifikation. Die Sicherheit, dass sie nicht vorgetaeuscht ist, gibt es nicht. Trotz der Verwendung einiger Namen mit "auth" auf dieser Stufe gibt es hier keine Authentizitaetspruefung.

        Erst in der dritten Sicherheitsstufe unterliegt jede Message einer Pruefung, ob die angegebenen Identifikationen authentisch sind. Dazu wird die Methode von Diffie-Hellman verwendet. Der dabei benutzte Verschluesselungsalgorithmus ist DES (Data Encryption Standard), der durch die Password-Verschluesselung in UNIX grosse Verbreitung fand.

        Der zweiter Vorteil dieser Stufe liegt in der betriebssystemunabhaengigen User-Identifikation. Sie funktioniert auch auf Computer ohne UNIX, so z.B. bei MS-DOS, wo allein der Begriff eines Users zunaechst nicht vorhan- den ist. Hier werden zusaetzliche Namen eingefuehrt, sog. Netnames, die jeden Kommunikationspartner auf jedem Computer eindeutig fuer alle Netzwerke identifizieren.

        Die Einfachheit des Interfaces erlaubt fast beliebige Erweiterungen dieses Schemas durch hoehere Sicherheitsstufen, mit anderen benutzerimplementierten Pruefungen und Verschluesselungen, deren Informationen durch die dehnbaren rq_cred- und rq_clntcred-Felder uebertragen werden. Zusaetzlich besteht immer die Moeglichkeit, die Sicherheitsmechanismen in hoeheren auf RPC aufsetzenden Protokollen zu implementieren. Das erweist sich oft als sinnvoll.

      3. UNIX-spezifische Identifikation
      4. 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
        1. die oa_flavor mit AUTH_UNIX und
        2. rq_clntcred mit dem Zeiger auf die aufgefuellte Struktur authunix_perms:
        
        
        	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.

      5. Diffie-Hellman/DES Authentizitaet
      6. 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.

        Der so initialisierte Client verschickt im rq_clntcred-Feld die fuer DES charakteristischen Daten vom Typ struct authdes_cred. Sie koennen im Server wie folgt benutzt werden:
        
        
                #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;
                    }
                    .
                    .
                }
        
        

    12. Benutzung des inetd Daemons
    13. 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:

      1. Der Server muss die Kontrolle an inetd explicit durch exit() abgeben und dadurch die unendliche Schleife in svc_run() beenden.
      2. Die Aufrufparameter der Routinen fuer die Transporterzeugung muessen entsprechend geaendert werden:
      
      
              transp = svcudp_create (0);
              transp = svctcp_create (0, 0, 0);
              transp = svcfd_create  (0, 0, 0);
              svc_register (transp, PROGNUM, VERSION, service, 0);
      
      


    14. RPC Error Bibliothek
    15. 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:

      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
      Die Parameter sind von Typ:

      
      
                  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
      
      

  7. Das RPC Protokoll
    1. Die RPC-Messages
    2. 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;
              };
      


    3. Das Protokoll der Sicherheitsmechanismen
    4. 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>;
              };
      
      

      1. Sicherheitsstufe eins und zwei
      2. 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).

      3. Das Protokoll der dritten Sicherheitsstufe (Diffie-Hellman/DES)
      4. 
        
                /*
                * 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;
                };
        


        Vor der ersten Transaktion waehlt der Client den Konversationsschluessel key (mit Zufallszahlgenerator), berechnet den gemeinsamen Schluessel ckey und verschluesselt mit ihm den key, dh. berechnet ckey(key). Die Verschluesselung von x mit dem Schluessel k wird hier mit k(x) bezeichnet, win ist das Zeitfenster, t die Zeiten. Den Ablauf der Kommunikation zeigt das folgende Diagramm:
        
        
                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)    .
        
        


  8. Der RPC-Generator rpcgen
    1. Programmerzeugung
    2. 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 15
      
      Fuer 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);
                  .
                  .
              }
      
      


      Es wird auf einige Merkmale hingewiesen:
      1. Der Client-Handle cl muss explicit erzeugt werden (clnt_create-Aufruf).

      2. printmessage_1 braucht noch cl als zweiten Parameter.

      3. printmessage_1 nimmt als Parameter nur einen Zeiger auf die Daten und gibt ebenfalls einen Zeiger zurueck.

      Der letzte Punkt ist die Konsequenz einer von XDR bekannten Regel: Die Filter nehmen als Parameter nicht die Daten selbst, sondern Adressen, von wo sie sie abholen, bzw. wo sie sie hinschreiben. Die tatsaechliche Groesse der transferierten Daten kann der Empfaenger von vornherein ja noch nicht wissen.

      Zusammen mit dem von rpcgen in clnt_msg.c gelieferten Client-Stub, sowie msg.h, kann der Client jetzt kompiliert und gebunden werden. Fuer den Server braucht man noch die entfernte Prozedur selbst, die so aussehen koennte:
      
      
              #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.

      Mit dem C-Code fuer printmessage_1() kann der Server-Stub hergestellt werden.

    3. Ein Erweitertes Beispiel
    4. 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.

    5. Die rpcgen-Optionen
    6. 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.

      #define-Variablen koennen nach dem Muster von cc in der rpcgen-Aufrufzeile eingeschaltet werden, wie z.B.:
      
      
              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.

    7. Dispatcher Tabellen
    8. 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.

    9. Die Sprachbeschreibung
    10. 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".

  9. Sonstiges
    1. Ausblick
    2. XDR/RPC gehoert heute zum festen Bestandteil von fast jedem UNIX-System.

      CORBA

      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.

      ToolTalk-Service

      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.

      ASN.1

      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.

    3. Didaktische Hinweise
    4. 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:

      1. C gut beherrschen

      2. ISO/OSI-Modell, Grundlagen Netzwerke

      3. Ethernet oder Token Ring Grundlagen

      4. TCP/IP Grundlagen: Addressieren in einem Netzwerk

      5. API: Berkeley Sockets

      6. UNIX-Grundlagen: Prozess-Management: fork, exec


      Die mehr logische Reihenfolge der Darstellung in diesem Skript wird moeglicherweise den didaktischen Erfordernissen nicht genuegen. Aus diesem Grunde bilden einzelne Kapitel abgeschlossene Einheiten, so dass die endgueltige Reihenfolge und Auswahl in der Verantwortung des Dozenten liegt.

      Es wird empfohlen den Unterricht nach einer Einfuehrung und High-level RPC gleich mit Middle-level RPC anzufangen. Um die Argumente von registerrpc() und callrpc() zu erklaeren folgt die Erklaerung des Port Mappers und anschliessend ein Kapitel ueber XDR. Danach soll RPCGEN und Low-level RPC erklaert werden.

      Beim Studium des Protokolls empfiehlt sich den RFC1050 mit der offiziellen Protokoll-Spezifikation zur Hilfe zu ziehen. Man findet ihn auch im Network Programming Guide von SUN.

      Die Aufgaben dienen auch als Anleitung zum Praktikum. Es wurde weniger Wert auf technische Details und mehr auf allgemeines Verstaendnis gelegt.

    5. Fragen, Aufgaben
      1. Nennen Sie in Stichpunkten, welche Informationen fuer einen sinnvollen RPC-Call noetig sind. Kann man theoretisch gesehen auf einige dieser Punkte verzichten ?

      2. Warum bietet NFS nur einen zustandslosen Dienst ? Was bedeutet das fuer den NFS-Anwender ?

      3. Warum muss die XDR-Konvertierung hin- und wieder zurueck erfolgen, auch wenn unkonvertierte Daten technisch problemlos zu uebertragen sind ? Kann man nicht mit einer Umwandlungen auskommen ?

      4. Warum braucht der Server eine Endlosschleife ?

      5. Warum ist der Portmapper notwendig ?

      6. Warum muss ab Middle-level RPC die Registrierung einer Serverprozedur beim Neustart des Serverprozesses jedes Mal neu erfolgen ?

      7. Warum muessen mehrere Versionen einer entfernten Prozedur im Server koexistieren koennen ?

      8. Entwerfen Sie Programme, die zwei Versionen einer entfernten Prozedur verwenden.

      9. Erklaeren Sie den Sinn des XDR-Handles.

      10. Lassen sich Memory-Steams mit Record-Streams konstruieren, oder umgekehrt ?

      11. Ist der Aufruf xdr_string (&handle, &&stringdaten, ...) sinnvoll ?

      12. Schreiben Sie eine Prozedur, die zwei Strings von einer dem Aufrufer unbekannten Laenge liefert und keinen zusaetzlichen Platz allokiert oder als static belegt. Rufen Sie sie auf.

      13. Welche Eigenschaft der XDR-Filter bewirkt, dass alle Custom-Filter unabhaengig von ihrem Aufbau genauso in beide Richtungen konvertieren, wie das auch primitive Filter tun ?

      14. Was ist ein "Wrapper" ?

      15. Das Netzwerk ist fuer RPC nicht vollkommen transparent. Zu welchen Unterschieden fuehrt der Ersatz von UDP durch TCP im Low-level RPC Code ? Nennen Sie in Stichpunkten so viele Unterschiede, wie Sie kennen.

      16. Fangen Sie alle moegliche Fehler in den Beispiel-Programmen ab und behandlen Sie sie jeweils an den richtigen Stellen. Welche(r) Kursteilnehmer/Arbeitsgruppe findet die meisten Fehlermoeglichkeiten ?

      17. - gestrichen

      18. Programmieren Sie einen Server, der fuer jeden Auftrag einen Kind-Prozess erzeugt, und ihn mit der Weiterbearbeitung betraut.

      19. Was passiert bei der vorherigen Aufgabe wenn der Port nicht rechtzeitig frei wird, und beim naechsten Request benutzt wird.

      20. Warum ist es sinnvoll, dass Batching einen sicheren (reliable) Transport wie TCP benutzt ?

      21. Ist es notwendig, dass der Client keine anderen Aufgaben erledigt, waehrend er auf das Ergebnis eines RPC-Calls wartet ?

      22. Schreiben Sie mit rpcgen Client und Server, die auf der Konsole des anderen Rechners lokal eingetippte Texte ausgeben.

      23. Erstellen Sie aus den Beispielen des Kapitels ueber rpcgen einen Client und einen Server fuer die entfernte Directoryabfrage rls (remote ls).

      24. Vergleichen Sie die Protokollschnittstelle der Sicherheitsmechanismen struct call_body mit der Programmierschnittstelle struct svc_req. Werden die Felder cred und verf durch die Felder rq_cred und rq_clntcred abgedeckt ?

      25. Ist es mithilfe der Diffie-Hellman-Methode moeglich, durch ein oeffentliches Netz mit einem Partner, mit dem man vorher keinen Kontakt hatte um Passworte auszutauschen, so zu kommunizieren, dass keiner mithoeren kann ? Begruenden Sie die Antwort.

    6. Antworten
      1. 1. der Name (Adresse) des Servers,
        2. die Programm- Version- und Prozedurnummer,
        3. Semantik des Dienstes, Ein- und Ausgabedatenformate, die die Dienst- routine anbietet.
        Wuerde man die Datenformate gesondert kodiert mituebertragen, so waere bei komplexeren Strukturen die Information darueber, was der Dienst eigentlich anbietet (Semantik), schwer zu formulieren.

      2. Der Client wird durch Absturz/Neubooten des Servers in keine unbestimmte Lage geraten. Server-Dienste sind entweder ganz da, oder ueberhaupt nicht.

      3. Die Datenformate koennen auf verschiedenen Computern verschieden sein. Es ist leichter das lokale Format einmal fuer jeden Rechnertyp bei XDR- Portierung zu beruecksichtigen, statt bei jeder Kommunikation die Quelle- und den Zieltyp zu verwalten.

      4. Damit er mehrere nacheinander folgende Call-Requests bedienen kann.

      5. Der Aufrufer kann nicht wissen, an welchem Port der Serverprozess wartet, da sich die Portnummer beim Neustart des Serverprozesses aendern kann.

      6. Nach dem Neustart warten die Serverprozesse moeglicherweise an anderen Ports, die man vorher nur schlecht voraussagen kann.

      7. Das erleichtert z.B. die SW-Entwicklung, die gleichzeitig von mehreren Personen auf mehreren Rechner durchgefuehrt wird.

      8. XDR-Handle koppelt die transportbedingten Daten von den Konvertierungs- filtern ab. Die zwei Mechanismen sind weitgehend unabhaengig: weniger Moeglichkeiten fuer nichtlokale Fehler, einfachere Entwicklung, bessere Wartung, etc.

      9. Record-Streams bieten einen allgemeineren Mechanismus an. Mit ihrer Hilfe lassen sich Memory-Streams und Standard-IO-Streams als Spezial- faelle konstruieren.

      10. Man kann die Adresse nur von einem real im Speicher existierenden Objekt bilden, &stringdaten ist nicht ein Objekt, sondern eine Zahl. Man findet oft in der Synopsis mancher Funktionen die Beschreibungen von Parameter als: typ **variable; Es muessen im Programm zwei reale Objekte vor- kommen, um den Aufruf zu konstruieren.

      11. Die doppelten char-Pointer sind notwendig. Der Aufrufer muss der Prozedur mitteilen, wo sie die Adressen der Strings ablegen soll. Im Falle von nur einem string koennte man auch durch den Rueckgabewert ohne doppelte Pointer auskommen.

      12. Die Konvertierungsrichtung wird in den Filtern selbst nicht behandelt.

      13. Ein "Wrapper" ("Umwickler") ist eine Routine, die nur einfache Aufbereitung der Parameter durchfuehrt und dann die eigentliche Behandlungsroutine aufruft.

        1. Bedingt durch die Begrenzung der UDP-Paketgroesse koennen bei UDP die auf einmal transferierten Daten bis 8k gross sein.

        2. TCP ist fuer Broadcasting und UDP fuer Batching nicht geeignet.

        3. Wegen der Pufferung von TCP gibt es zwischen TCP und UDP Unterschiede im Zeitverhalten.

        4. UDP ist nicht sicher (unreliable). Fuer noetige Retransmissionen traegt RPC keine Sorge.
      14. Rufen Sie fork() im Dispatcher und nicht in der entfernten Prozedur auf, sonst wuerde der Dispatcher leere Ergebnisse an den Client schicken, noch bevor die eigentliche Aufgabe erledigt wurde. Es gibt keine Ueberschneidungen von den Ports, da fuer den Transportweg das Paar (Clientport, Serverport) entscheidend ist.

      15. Der Mechanismus der Ports wird von der Transportschicht (TCP, UDP) ange- boten und fuer Multiplexing benutzt. Der Transportweg wird auf beiden Transportendpunkten durch das Paar (Port des Senders, Port des Empfaengers) gekennzeichnet.

      16. Die Auftraege werden zusammen geschickt. Bei einem Fehler koennte die ganze Schlange zusammenbrechen.

      17. Nein. Es gibt dafuer mehrere Moeglichkeiten: 1. geeigneter eigener Dispatcher, 2. Callbacks

      18. Benutzen Sie das erste Beispiel des Paragraphen ueber rpcgen.

      19. Nein. Bei UNIX-Identifikation wird z.B. rq_cred und rq_clntcred in zwei nacheinander folgenden Messages in dem Feld cred uebertragen und verf wird gar nicht verwendet.

      20. Ja. Mit dem oeffentlichen Schluessel (public key) teilt man dem Partner mit, wie seine Nachrichten zu verschluesseln sind. Die anderen Teilnehmer sind jedoch nur aufgrund des oeffentlichen Schluessels nicht in der Lage, den dazu gehoerenden Entschluesselungs-Schluessel zu erstellen, der lokal angewendet geheim bleibt.

  10. Anhang
    1. Backus-Naur Notation
      1. Zeichen |, (, ), [, ], ,, * haben spezielle Bedeutung.

      2. Abschliessende Symbole sind Strings in Hochkommatas.

      3. Nichtabschliessende Symbole sind Strings aus Zeichen ohne spezielle Bedeutung.

      4. Alternative Eintraege sind mit | getrennt.

      5. Optionale Eintraege sind eingeschlossen in eckige Klammern [].

      6. Eintraege werden mit runden Klammern () gruppiert.

      7. Ein Stern nach einem Eintrag bedeutet 0 oder mehr Wiederholungen.

    2. Literaturangaben
      1. RFC1014 von ARPA Network Information Center enthaelt XDR Protokoll Beschreibung
      2. RFC1050, RFC1057 RPC Protocoll Specification
      3. New Directions in Cryptography, Diffie & Hellman, IEEE Transactions on Information Theory IT-22, November 1976
      4. Data Encryption Standard, National Bureau of Standards, Federal Informa- tion Processing Standards Publication 46, January 15, 1977
      5. Time Server, K. Herrenstein, RFC 738, October 1977
      6. Die Mathematik der Verschluesselungssysteme, Martin E. Hellman, (Hellmann fehlerhaft !) Spektrum der Wissenschaft, Oktober 1979, oder Scientific American, August 1979
      7. User Datagram Protocol, J.Postel, RFC 768, Information Science Institute, August 1980
      8. Transmission Control Protocol - DARPA Internet Program Protocol Specifi- cation, J.Postel, RFC 793, Information Science Institute, September 1981
      9. Courier: The Remote Procedure Call Protocol, XEROX Corporation, XSIS 038112, December 1981
      10. Distributed Deadlock Detection Algorithm, R.Obermarck, ACM Transactions on Database Systems 7:2, 187-208, 1982
      11. Implementing Remote Procedure Calls, Birrell, Andrew D & Nelson, Bruce Jay, XEROX CSL-83-7, October 1983
      12. Implementing Remote Procedure Calls, A.d.Birrell & B.J.Nelson, ACM Transactions on Computer Systems 2:1, 39-59, 1984
      13. Assigned Numbers, J.Reynolds & J.Postel, RFC 923 October 1984
      14. Sun External Data Representation Specification, B.Lyon, Sun Microsystems, Inc., Mountain View (Calif.), 1984
      15. Sun Remote Proceduire Call Specification, B.Lyon, Sun Microsystems, Inc., Mountain View (Calif.), 1984
      16. ANSI/IEEE Standard 754-1985, IEEE Standard for Binary Floating-Point Arithmetic, Institute for Electrical and Electronisc Engineers, August 1985
      17. VMTP: Versatile Message Transaction Protocol, D.Cheriton, Stanford University, Januar 1987
      18. Sun Microsystems: Network Programming, Mountain View (Clif.) Sun Microsystems, 1988
      19. Sun Microsystems: SunOs Reference Manual, Mountain View (Clif.) Sun Microsystems, 1988
      20. Sun Microsystems: Security Features Guide, Mountain View (Clif.) Sun Microsystems, 1988
      21. Internetworking with TCP/IP - Priciples, Protocols and Architectures, Douglas E. Comer, David L. Stevens, Band I und III, Eaglewood Cliffs New Jersey, Prentice Hall 1988
      22. Open Systems Standards - OSI Remote and Reliable Operations, H.Folts, IEEE Network 3:3 1989
      23. Sun Remote Procedure Call Implementation Made Transport Independant, V.Samar & C.McManis, Sun Microsystems, Mountain View (Calif.), December 1989
      24. The Design and Implementation of the 4.3BSD UNIX Operating System, S.J.Leffler et.al., Reading (Mass.), Addison-Wesley, 1990
      25. Network Programming Guide von Sun Microsystems, Revision A, March 1990
      26. UNIX Network Programming, W.R.Stevens, Prentice Hall, 1990
      27. ASN.1 Abstract Syntax Notation One, Walter Gora, Reinhard Speyerer, 1990, DATACOM Verlag
      28. Power Programming with RPC, John Bloomer, O'Reilly & Associates, Inc, 1991
      29. The Art of Distributet Applications, J.R. Corbin, Springer-Verlag, 1991
      30. Kryptologie, Albrecht Beutelspacher, Vieweg-Verlag, 1991
      31. Objektboerse - CORBA, Torsten Beyer, iX Heise Verlag 2.1993
      32. Bedeutungsschwanger: Datenbeschreibung mit Abstract Syntax Notation One (ASN 1), Hans Georg Baumguertel, iX Heise Verlag 3.1993, S.118 ff
      33. Entwicklung von Kommunkationsanwendungen mit ONC XDR Teil I und II, Peter Mandl, iX Heise Verlag 4,5.1993
      34. Kommunikation ueber alles - Tooltalk Einfuehrung, Michael Busch, iX 5.1993

    3. Online documentation:
    4. http://www12.w3.org/History/1992/nfs_dxcern_mirror/rpc/doc/Introduction/Abstract.html http://docs.sun.com/

  11. Glossarium / Wortschatz
  12. 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



Copyright © 2000, Z.Lisiecki