Inhalt

5. RPC-Programmierung I

Das Interface zur RPC - Programmierung ist in drei Schichten unterteilt:

1. High-, 2. Middle- und 3. Low-Level.

5.1 High-level RPC

High-level RPC: Für diese Schicht ist das Betriebssystem, die Rechnerhardware und das verwendete Netzwerk komplett transparent. Der Programmierer ruft eine C-Routine auf, die das Netzwerkhandling vollständig verdeckt: z.B. rnusers(), um die Anzahl der eingeloggten Benutzer auf einem entfernten Rechner zu erfragen. Andere bekannte Routinen sind

rusers    gibt Informationen über User auf einem entfernten Rechner
havedisk  überprüft, ob der entfernte Rechner eine Harddisk hat
rstat     erfragt Informationen über Rechnerauslastung
rwall     schreibt eine Nachricht an entfernte Rechner
yppasswd  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 vollständige 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 erwähnt, das Programm-Interface wird nur in zwei Schichten geteilt und Middle-level von RPC als High-level bezeichnet. Die vorliegende Beschreibung hält sich an den Sprachgebrauch von SUN.

5.2 Middle-level RPC

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() läßt sich dann der eigentliche Client-Aufruf realisieren. Ein dem rnuser()-Beispiel analoges aus High-level RPC sieht dann so aus:

#include <stdio.h>
#include <rpc/rpc.h>
#include <rpc/rusers.h>

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 muß auf dem Server-Rechner als Prozeß 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 empfängt und beantwortet. Dieser Prozeß läuft im Server als Dämon, der die Client-Anfragen nacheinander beantwortet. Die eigentliche Server-Prozedur nuser() mit den dabei üblicherweise verwendeten RUSER... Parametern ist in unserem Fall in der Standard Library enthalten. Der dazugehörige Client-Teil sieht dann so aus:


        #include <stdio.h>
        #include <rpc/rpc.h>
        #include <rpc/rusers.h>

        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. Für die Datenfelder werden in der Synopsis von callrpc() Character-Pointer benutzt, um die Übergabe längerer Felder zu ermöglichen. Die Ein/Ausgabefilter sind Pointer auf Funktionen, an die später, 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 benötigt, dagegen nicht. xdr_string() muß 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 daß die Ein/Ausgabedaten die Packetgröße von derzeit 8K nicht übersteigen sollen. Siehe dazu auch UDPMSGSIZE in <rpc/clnt.h>.

5.3 Low-level RPC

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 Fälle betreffen:

  1. Man möchte mit TCP statt UDP die Grenze der Packetgröße umgehen, oder andere Charakteristika der Kommunikation anpassen.
  2. Man möchte daß die RPC-Routinen den Speicherplatz für die Übertragenen Daten selbst allokieren. Die Vorgehensweise wurde im Kapitel über XDR erklärt.
  3. Man möchte sicherstellen, daß nur ausgewählte Benutzer die Serverdienste in Anspruch nehmen können, und verlangt dafür Berechtigungsnachweise.

In diesen Fällen müssen die Middle-level Routinen in ihre Low-level Bestandteile zerlegt werden, mit denen man anschließend 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 <stdio.h>
        #include <rpc/rpc.h>
        #include <utmp.h>
        #include <rpc/rusers.h>

        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 ursprünglich mit Berkeley Sockets implementiert und wird vermutlich später durch TLI ersetzt. Durch Lokalisierung dieser Abhängigkeit zunächst nur an zwei Stellen im Code:

auf der Server Seite        die Struktur SVCXPRT in <rpc/svc.h>

auf der Client Seite        die Struktur CLIENT  in <rpc/clnt.h>
ist die Portierung auf andere Protokolle einfach. Die dazu notwendigen Sourcen von RPC-Routinen sind bei SUN kostenlos erhältlich. Es ist aus dem Grunde empfehlenswert möglichst wenig von Sockets API zu benutzen.

Das Argument in svcudp_create(), RPC_ANYSOCK, bewirkt, daß ein neuer Socket generiert wird. Stattdessen kann hier auch die Nummer von einem bereits existierenden Socket angegeben werden. Bei einem gebundenen Socket müssen dann die Portnummer von svcudp_create() und clntudp_create() übereinstimmen.

pmap_unset() bewirkt, daß die vorherige Portmapper-Registrierung mit den gleichen Nummern gelöscht wird. In diesem Fall sind die zwei letzten Zeilen von main() eigentlich überflüssig.

Beachten Sie, daß weder die Angabe der Prozedurnummer noch die des xdr-Filters bei svc_register() nötig 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-Fälle 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 Möglichkeit zu geben mit dem NULLPROC-Aufruf zu prüfen, ob der Server-Prozeß überhaupt läuft.

Mit der Zeile mit svc_getargs(), die in dem Beispiel nicht notwendig ist, können in clnt_data Aufrufargumente des Clients geholt werden.

Der entsprechende Client-Stub sieht wie folgt aus:


        #include <stdio.h>
        #include <rpc/rpc.h>
        #include <rpcsvc/rusers.h>
        #include <sys/time.h>
        #include <netdb.h>

        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 Stück Code zeigt in etwa den internen Aufbau der Middle-level Routine callrpc(). So wird auch deutlich, an welcher Stelle UDP durch TCP ersetzt werden könnte (Vorsicht: clnttcp_create() hat etwas andere Argumente als clntudp_create()). Natürlich muß der Transportmechanismus für den Client und den Server der gleiche sein. Zu erwähnen sei auch die allgemeinere Routine clnt_create(), die als letztes Argument »udp« oder »tcp« enthält.

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() verändern.

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


Inhalt