Netzwerk-Know-how (tecCHANNEL COMPACT) Kapitel 9: Remote Procedure Call – RPC

Veröffentlicht: 13. Jul 2005

Von PROF. DR. STEPHAN EULER

Häufig basieren Netzwerkanwendungen auf einem Frage-und-Antwort-Schema. Ein Prozess (der Client) auf einem Rechner beauftragt einen zweiten Prozess (den Server) auf einem anderen Rechner, eine bestimmte Aktion auszuführen und ein dazu gehörendes Ergebnis zu melden. In diesem Szenario steht weniger die Datenübertragung als die Aufteilung der Arbeit im Mittelpunkt.

Auf dieser Seite

In diesem Beitrag

Dn151205.ACDCF196BC98A92A7E35715F19C8C405(de-de,TechNet.10).png Grundlagen

Dn151205.ACDCF196BC98A92A7E35715F19C8C405(de-de,TechNet.10).png RPC-Beispiel

Dn151205.ACDCF196BC98A92A7E35715F19C8C405(de-de,TechNet.10).png RPC-Server

Dn151205.ACDCF196BC98A92A7E35715F19C8C405(de-de,TechNet.10).png RPC-Client

Dn151205.ACDCF196BC98A92A7E35715F19C8C405(de-de,TechNet.10).png Compilieren und Linken

Dn151205.ACDCF196BC98A92A7E35715F19C8C405(de-de,TechNet.10).png Abfrage-Prozedur

Dn151205.ACDCF196BC98A92A7E35715F19C8C405(de-de,TechNet.10).png Literatur

Betrachten wir als Beispiel einen Datenbank-Server. Ein Client fragt nach allen Einträgen zu einem bestimmten Nachnamen. Der Ablauf ist:

  1. Der Client schickt eine Abfrage mit dem Nachnamen und wartet danach.
  2. Der Server sucht nach entsprechenden Einträgen.
  3. Der Server sendet die Liste mit gefundenen Einträgen zurück.
  4. Der Client empfängt die Antwort und arbeitet damit weiter.

Folgendes Bild zeigt den Ablauf in der Darstellung als Zeitstrahl.

Dn151205.AA9323528D5F1D879FB9A3955BEAEEEB(de-de,TechNet.10).png

Bild 1: Ablauf in einem Frage-und-Antwort-Schema

Bei dem Ansatz mit Remote Procedure Call RPC betrachtet man diesen Ablauf wie den Aufruf eines Unterprogramms. Der Client entspricht dem übergeordneten Modul, das eine Funktion mit einer Argumentliste und Rückgabewerten aufruft. Der RPC-Formalismus unterstützt diese Sichtweise durch eine Reihe von Werkzeugen. Mit deren Hilfe erfolgt die Umsetzung in den auf unterer Schicht ablaufenden Austausch von Datenpaketen weit gehend automatisch. Im Allgemeinen können die beiden Prozesse in verschiedenen Programmiersprachen geschrieben worden sein und auf verschiedenen Betriebssystemen laufen. Daher ist es notwendig, eine plattformunabhängige Repräsentation der Daten einzusetzen.

Im Folgenden wird zunächst eine kurze Beschreibung der grundlegenden Mechanismen von RPC gegeben. RPC ist allerdings eher ein allgemeines Konzept als ein gemeinsames Protokoll. Es gibt eine ganze Reihe von Implementierungen (Sun RPC, Microsoft RPC et cetera), die sich in den Details unterscheiden. Daneben stehen in anderen Programmiersprachen ähnliche Möglichkeiten zur Verfügung. Java bezeichnet dieses Konzept als Remote Methode Invocation (RMI). Im Folgenden werden Details der Realisierung an Hand eines einfachen Beispiels für ein Telefonbuch besprochen. Die Entwicklung erfolgt mit Visual C unter Windows 2000. Die Darstellung versteht sich als Einführung in die Programmierung mit RPC anhand der Basisfunktionalitäten. Für weitere Möglichkeiten sei auf die umfangreiche Dokumentation verwiesen [1].

Dn151205.590B5404BFEA7F06684DB47B00539355(de-de,TechNet.10).png Zum Seitenanfang

Grundlagen

RPC soll möglichst weit gehend alle netzwerkrelevanten Details übernehmen. Im Idealfall ist für den Anwender nicht sichtbar, dass die Anwendung über ein Netzwerk läuft. Die Kapselung erfolgt durch so genannte Stubs (Englisch: (Baum)Stumpf, Stummel). Die Stubs wirken als Mittler zwischen der Anwendung und dem RPC-Protokoll. Ein entsprechender Generator oder Compiler erzeugt die Stubs für die jeweilige Programmiersprache automatisch aus einer abstrakten Definition des Interface. Der Client ruft den Client-Stub als normale Prozedur auf und übergibt die notwendigen Argumente. Im Stub erfolgen die Umsetzung auf eine netzwerktaugliche Darstellung und der Aufruf des RPC-Protokolls.

Dn151205.1E4F2A6763DB18EC684BCEE7C1B8442E(de-de,TechNet.10).png

Bild 2: Stubs setzen die Programmzugriffe auf das RPC-Protokoll um

Dn151205.590B5404BFEA7F06684DB47B00539355(de-de,TechNet.10).png Zum Seitenanfang

RPC-Beispiel

Als Beispiel soll ein Server für Anfragen nach Telefonnummern realisiert werden. Im ersten Schritt der RPC-Entwicklung muss der Programmierer einen eindeutigen Bezeichner für das so genannte Interface erzeugen. Dazu steht das Programm uuidgen zur Verfügung. Eventuell ist zunächst ein Aufruf von vcvars32.bat nötig, damit die Suchpfade entsprechend gesetzt sind.

Folgender Aufruf erzeugt dann eine Datei mit einem eindeutigen Identifikator (Universally Unique IDentifier, UUID):

> uuidgen /i /otelbu.idl

Im Beispiel resultierte daraus die Datei telbu.idl mit diesem Inhalt:

> //file telbu.idl
> [
> uuid(49db6843-b369-4551-86f6-b3254a9b8a1a),
> version(1.0)
> ]
> interface INTERFACENAME
> {
> 
> }

In diese IDL-Datei (IDL: Interface Definition Language) trägt man als Nächstes einen Namen für das Interface sowie die vorgesehenen Prozeduren ein. Das Interface bekommt den Namen telbu als Abkürzung von Telefonbuch. Zunächst ist nur eine einzige Prozedur testKomm als Test für die Kommunikation vorgesehen. Diese Prozedur erhält als Argument eine Zeichenkette, die dann als Bestätigung für eine geglückte Übertragung auf der Seite des Servers ausgegeben wird. Damit erhält man:

> //file telbu.idl
> [
> uuid(49db6843-b369-4551-86f6-b3254a9b8a1a),
> version(1.0)
> ]
> interface telbu
> {
> void testKomm([in, string] unsigned char * testString);
> }

In der Deklaration sind zusätzliche Attribute für den Parameter eingetragen. Die Angabe „in" legt fest, dass es sich um einen Eingabewert handelt, den die Prozedur nicht überschreiben darf. Das zweite Attribut string deklariert den Parameter als eine gemäß der C-Konvention mit '\0' abgeschlossene Zeichenkette. Damit kann der Compiler die Funktion strlen einsetzen, um die Länge des zu übertragenden Datenbereichs zu bestimmen. Weiterhin benötigt man ein so genanntes Appli-cation Configuration File (ACF).

> //file: telbu.acf
> [implicit_handle (handle_t telbu_IfHandle)
> ] interface telbu
> {
> }

Aus diesen beiden Dateien erzeugt der MIDL-Compiler (MIDL steht für Microsoft IDL) mit dem Aufruf midi telbu.idl den C-Code für den Server- und Client-Stub. Die beiden Dateien erhalten die Namentelbu_s.c und telbu_c.c. Weiterhin legt er eine Header-Datei telbu.h an.

Dn151205.590B5404BFEA7F06684DB47B00539355(de-de,TechNet.10).pngZum Seitenanfang

RPC-Server

Nach diesen Vorbereitungen lässt sich der Server implementieren. Den RPC-Me-chanismus kann der Programmierer auf verschiedene Protokolle aufsetzen. Das gewünschte Protokoll wird in Form einer Zeichenkette spezifiziert. Die Zeichenkette besteht aus einer Kennung für das RPC-Protokoll: ncacn oder ncadg für ström- oder paketorientierte Kommunikation. Anschließend folgen die Kennungen für ein Vermittlungsprotokoll, wie etwa IP, und ein Transportprotokoll, zum Beispiel TCP. Die einzelnen Bestandteile sind durch _-Zeichen getrennt.

Damit ist die Spezifikation für TCP/IP ncacn_ip_tcp. Ein anderes Beispiel ist ncacn_np für die Kommunikation über named pipes. Das Protokoll zusammen mit dem Namen des Endpunkts wird mit der Funktion RpcServerUseProtseqEp angegeben. Anschließend wird mit RpcServerRegisterlfdas Interface registriert. Schließlich wartet mit RpcServerListen der Server auf Anfragen. Insgesamt erhält man folgenden Code für den Server.

> /* file: telbus.c */
> #include <stdlib.h>
> #include <stdio.h>
> #include <ctype.h>
> #include "telbu.h"
> 
> void main()
> {
> RPC_STATUS status;
> unsigned char *pszProtocolSequence = "ncacn_ip_tcp";
> unsigned char *pszSecurity = NULL; /*no Security*/
> unsigned char *pszEndpoint = "5432";
> unsigned int cMinCalls = 1;
> unsigned int cMaxCalls = 20;
> unsigned int fDontWait = FALSE;
> 
> printf( "RpcServerUseProtseqEp\n");
> status = RpcServerUseProtseqEp(pszProtocolSequence,
> cMaxCalls,
> pszEndpoint,
> pszSecurity);
> 
> if (status) exit(status);
> printf( "RpcServerRegisterIf\n");
> status = RpcServerRegisterIf(telbu_v1_0_s_ifspec,
> NULL,
> NULL);
> if (status) exit(status);
> 
> printf( "RpcServerListen\n");
> status = RpcServerListen(cMinCalls,
> cMaxCalls,
> fDontWait);
> 
> if (status) exit(status);
> 
> } // end main()
> 
> /******************************************************/
> /* MIDL allocate and free */
> /******************************************************/
> 
> void __RPC_FAR * __RPC_USER midl_user_allocate(size_t len)
> {
> return(malloc(len));
> }
> 
> void __RPC_USER midl_user_free(void __RPC_FAR * ptr)
> {
> free(ptr);
> }

Zusätzlich werden zwei Routinen zum Reservieren und Freigeben von Speicher benötigt. Die beiden Routinen sind hier lediglich Hüllen (wrapper) um die entsprechenden Funktionen malloc und free aus der Standardbibliothek von C.

Dn151205.590B5404BFEA7F06684DB47B00539355(de-de,TechNet.10).pngZum Seitenanfang

RPC-Client

Der Client muss zunächst die RPC-Verbindung spezifizieren. Er benutzt dazu die Funktion RpcStringBindingCompose, um aus einer Anzahl von Parametern eine Zeichenkette zu konstruieren. Abhängig vom Protokoll müssen nicht unbedingt alle Parameter gesetzt werden. So benötigt das Protokoll über named pipes keine Angabe der Netzwerkadresse, da es nur zwischen Prozessen auf einem Rechner verwendet werden kann.

Diese Zeichenkette wird dann an RpcBindingFromStringBinding übergeben. Bei Erfolg ist das Interface aktiviert, und das zweite Argument enthält einen gültigen so genannten Handle auf das Interface. Danach kann der Client die Prozeduren aufrufen. Die Aufrufe sind im Beispiel in einen speziellen RpcTryExcept Block eingebettet, der eventuelle Laufzeitfehler auffängt. Der Client ruft in diesem Beispiel zehn Mal die Prozedur testKomm auf. Am Ende werden noch die belegten Ressourcen wieder freigegeben. Das Programm hat folgende Form:

> /* file: telbuc.c */
> #include <stdlib.h>
> #include <stdio.h>
> #include <ctype.h>
> #include "telbu.h"
> 
> void main()
> {
> RPC_STATUS status;
> unsigned char *pszUuid = NULL;
> unsigned char *pszProtocolSequence = "ncacn_ip_tcp";
> unsigned char *pszNetworkAddress = "127.0.0.1";
> unsigned char *pszEndpoint = "5432";
> unsigned char *pszOptions = NULL;
> unsigned char *pszStringBinding = NULL;
> unsigned char *testString = "Hallo Server";
> unsigned long ulCode;
> 
> status = RpcStringBindingCompose(pszUuid,
> pszProtocolSequence,
> pszNetworkAddress,
> pszEndpoint,
> pszOptions,
> &LpszStringBinding);
> if (status) exit(status);
> printf("pszStringBinding: %s\n", pszStringBinding );
> 
> status = RpcBindingFromStringBinding(pszStringBinding,
> &telbu_IfHandle);
> if (status) exit(status);
> 
> RpcTryExcept {
> int i;
> // mehrfach die Test-Prozedur aufrufen
> for( i=0; i<10; i++ ) {
> printf("%d ", i);
> testKomm(testString);
> Sleep( 1000 );
> }
> }
> RpcExcept(1) {
> ulCode = RpcExceptionCode();
> printf("Runtime exception 0x%lx = %ld\n", ulCode, ulCode);
> }
> RpcEndExcept
> 
> status = RpcStringFree(&pszStringBinding);
> if (status) exit(status);
> 
> status = RpcBindingFree(&telbu_IfHandle);
> if (status) exit(status);
> 
> exit(0);
> } // end main()
> 
> /******************************************************/
> /* MIDL allocate and free */
> /******************************************************/
> 
> void __RPC_FAR * __RPC_USER midl_user_allocate(size_t len)
> {
> return(malloc(len));
> }
> 
> void __RPC_USER midl_user_free(void __RPC_FAR * ptr)
> {
> free(ptr);
> }

Dn151205.590B5404BFEA7F06684DB47B00539355(de-de,TechNet.10).pngZum Seitenanfang

Compilieren und Linken

Die Aufrufe zum Übersetzen und Linken der Programme sind vorteilhaft in einer Make-Datei zusammengefasst. Der folgende Text in der Datei makefile wird dann durch nmake ausgewertet, und die notwendigen Aufrufe von Compiler und Linker werden gestartet. Das Makefile enthält auch die Abhängigkeiten von der IDL-Da-tei. Nach einer Änderung an dieser Datei erzeugt ein Aufruf von nmake automatisch die gesamte Verarbeitungskette. Am Ende erhält man die beiden Anwendungen telbus und telbuc für Server und Client. Die beiden Anwendungen kann man in zwei Eingabefenstern ausführen.

Noch ein wichtiger Hinweis: nmake erwartet bei der Angabe der Abhängigkeiten in makefile jeweils ein Tabulatorzeichen nach dem Doppelpunkt. Dieses Zeichen darf nicht durch Leerzeichen ersetzt werden.

> #makefile for telbuc.exe and telbus.exe
> #link refers to the linker
> #conflags refers to flags for console applications
> #conlibs refers to libraries for console applications
> 
> !include <ntwin32.mak>
> 
> all : telbuc.exe telbus.exe
> Make the client side application telbuc
> telbuc.exe : telbuc.obj telbu_c.obj
> $(link) $(linkdebug) $(conflags) -out:telbuc.exe \
> telbuc.obj telbu_c.obj \
> rpcrt4.lib $(conlibs)
> 
> # telbuc main program
> telbuc.obj : telbuc.c telbu.h
> $(cc) $(cdebug) $(cflags) $(cvars) $*.c
> 
> # telbuc stub
> telbu_c.obj : telbu_c.c telbu.h
> $(cc) $(cdebug) $(cflags) $(cvars) $*.c
> 
> # Make the server side application telbus
> telbus.exe : telbus.obj proc.obj telbu_s.obj
> $(link) $(linkdebug) $(conflags) -out:telbus.exe \
> telbus.obj telbu_s.obj proc.obj \
> rpcrt4.lib $(conlibsmt)
> 
> # telbu server main program
> telbus.obj : telbus.c telbu.h
> $(cc) $(cdebug) $(cflags) $(cvarsmt) $*.c
> 
> # remote procedures
> proc.obj : proc.c telbu.h
> $(cc) $(cdebug) $(cflags) $(cvarsmt) $*.c
> 
> # telbus stub file
> telbu_s.obj : telbu_s.c telbu.h
> $(cc) $(cdebug) $(cflags) $(cvarsmt) $*.c
> 
> # Stubs and header file from the IDL file
> telbu.h telbu_c.c telbu_s.c : telbu.idl telbu.acf
> midl telbu.idl

Mit der erfolgreichen Ausführung von nmake werden die beiden Dateien telbus. exe und telbuc.exe erzeugt. Nach dem Start wartet der Server auf Anfragen von Client-Prozessen. Der Client ruft in einer Schleife mehrfach die Prozedur test-Komm auf. Bei jedem Aufruf gibt der Server die erhaltene Zeichenkette aus.

Dn151205.590B5404BFEA7F06684DB47B00539355(de-de,TechNet.10).pngZum Seitenanfang

Abfrage-Prozedur

Nach diesen Vorbereitungen kann man die eigentliche Funktionalität des Telefonbuchbeispiels angehen. Zunächst wird eine Struktur mit zwei Zeichenketten zur Aufnahme der Daten Name und Nummer eingeführt. Der folgende Codeabschnitt legt ein Feld der Größe 100 an. Er füllt die beiden ersten Einträge und setzt einen Zähler count auf die Anzahl zwei:

> \\ in Datei proc.c
> struct {
> char *name;
> char *number;
> } tb[100] = {"Maier", "123 567",
> "Schmidt", "33 33 22" };
> int count = 2;

Bei der Übergabe von Argumenten muss der Software-Entwickler die Besonderheiten der Netzwerkkommunikation beachten. So ist es wenig sinnvoll, einen Zeiger zu übergeben. Die Information, dass die Daten an der angegebenen Speicheradresse stehen, hat für einen anderen Prozess auf einem anderen Rechner keine Bedeutung. Vielmehr müssen die Daten in geeigneter Form übertragen werden. Der RPC-Compiler übernimmt die Aufgabe, aus den Definitionen in der IDL-Da-tei den entsprechenden Code zu generieren.

Allerdings sind dabei die beschriebenen Beschränkungen zu beachten. Es ist konkret nicht möglich, wie sonst in C üblich, das Ergebnis einer Anfrage nach einer Telefonnummer als Zeiger auf das Resultat zurückzugeben. Eine Möglichkeit zur Übergabe des Resultats ist jedoch, das Ergebnis in ein Feld vorgegebener Größe zu schreiben. Die IDL-Datei hat dann folgende Form:

> //file telbu.idl
> [
> uuid(49db6843-b369-4551-86f6-b3254a9b8a1a),
> version(1.0)
> ]
> interface telbu
> {
> #define STRSIZE 100
> cpp_quote("#define STRSIZE 100")
> void testKomm([in, string] unsigned char * testString);
> int getNumber( [in, string] unsigned char * name,
> [out ] unsigned char number[STRSIZE]);
> }

Die Prozedur getNumber enthält zwei Parameter:

  1. name mit dem Namen, für den der Client eine Telefonnummer sucht.
  2. number als Feld, in das der Server das Ergebnis der Suche einträgt.

Der Integer-Rückgabewert ist als Ergebnismeldung vorgesehen. Das Feld number ist mit einer festen Größe definiert. Zur besseren Übersicht ist die Größe über eine symbolische Konstante STRSIZE festgelegt. Die Anweisung define ist nur für die Auswertung durch den Generator midi gültig. Die Anweisung cpp_quote schreibt eine Kopie der Definition der Konstanten in die Datei telbu.h. Dadurch ist diese Definition auch in den C-Programmen zugänglich. Die Prozedur wird durch folgende C-Funktion realisiert:

> \\ in Datei proc.c
> int getNumber( unsigned char *name, unsigned char *number ){
> int i;
> strcpy( number, "");
> for( i=0; i<count; i++ ) {
> if( strstr( tb[i].name, name ) ) {
> strcpy( number, tb[i].number);
> return i;
> }
> }
> return -1;
> }

Mit der Bibliotheksfunktion strstr wird geprüft, ob die gesuchte Zeichenkette in einem der Namensfelder enthalten ist. Findet sich ein entsprechender Eintrag, kopiert der Server die zugehörige Telefonnummer in das Ergebnisfeld und beendet die Funktion. Als Rückgabewert dient der Index des Eintrags. Findet er keinen passenden Eintrag, signalisiert er dies durch den Rückgabewert -1.

In den Client wird eine Schleife mit Abfragen wie folgt eingefügt. Der Client liest dabei jeweils ein Wort von der Kommandozeile ein und ruft damit die Prozedur zur Suche auf.

> /* file: telbuc.c */
> ...
> unsigned char number[STRSIZE], name[STRSIZE];
> ...
> while( scanf("%s", name) ) {
> if( getNumber( name, number ) < 0 ) {
> printf("Kein Eintrag\n");
> } else {
> printf( "%s\n", number );
> }
> }...

Dn151205.590B5404BFEA7F06684DB47B00539355(de-de,TechNet.10).pngZum Seitenanfang

Literatur

© www.tecCHANNEL.de

[1] Microsoft. RPC Programmer’s Guide and Reference

Dn151205.590B5404BFEA7F06684DB47B00539355(de-de,TechNet.10).pngZum Seitenanfang

| Home | Technische Artikel | Community