Das Interface zur RPC - Programmierung ist in drei Schichten unterteilt:
1. High-, 2. Middle- und 3. Low-Level.
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.
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>.
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:
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.