Programmieren unter MS-Windows

Kapitel 22: Remote Procedure Calls

Letzte Änderung: 2.11.97 von B. Tritsch

Zurück zum Index

Zurück zum Inhalt


Die grundlegende Idee hinter Remote Procedure Calls (RPCs) ist der Aufruf einer Prozedur, die jedoch physikalisch auf einem anderen Rechner läuft. Der Funktionsaufruf wird einschließlich der zugehörigen Parameter als Datenstrom vom Client zum Server geschickt. Der Server kann daraufhin mit einem Datenstrom in die Gegenrichtung antworten. Hierbei kann für die Transfermethode zwischen mehreren Mechanismen ausgewählt werden, z.B. zwischen Named Pipes, NetBIOS, TCP/IP und IPX/SPX. Diese Mechanismen werden dann jedoch von den RPCs völlig verborgen und treten als solche nicht mehr in Erscheinung.

Die grundlegende Spezifikation der RPCs stammt von der Open Software Foundation (OSF), die damit die Kommunikation innerhalb ihres Standardisierungssvorschlags Distributed Computing Environment (DCE) festlegen wollte. Die Microsoft RPCs halten sich weitgehend an den OSF-Vorschlag, sind jedoch nicht vollstädig kompatibel.

Microsoft-RPC ist ein Satz von API-Funktionen und Bibliotheken, die die Entwicklung von Client/Server-Anwendungen erlauben ohne daß sich der Programmierer um Netzwerkspezifika kümmern muß. Nach der Auswahl eines oder auch mehrerer zu unterstützender Netzwerkprotokolle stellen die RPC-Bibliotheken den Code für Kommunikationszwecke zur Verfügung. Die Stärke der RPC-Implementierung liegt daran, daß sie konzeptionell unabhängig vom zugrundeliegenden Betriebssystem arbeitet. Standardmäßig werden Windows NT, Windows 95, Windows für Workgroups und sogar MS-DOS unterstützt. Portierungen von Microsoft-Partnern erschließen inzwischen auch andere Plattformen wie Macintosh, verschiedene UNIX-Derivate (z.B. Sun Solaris) und sogar Mainframes.

Der Programmaufruf einer Prozedur über RPCs basiert dabei auf sogenannten Stubs, die den betreffenden Aufruf zunächst einmal lokal entgegennehmen. Aus Programmierersicht ist das so, als würde man eine Funktion aus einer lokalen Bibliothek aufrufen. Der Stub reicht die Funktion einschließlich ihrer Parameter über das Netz an eine entfernte Plattform mit einem spezifischen Zielprozeß weiter. Dieser besitzt wiederum einen Stub, der die Funktion entgegennimmt und dem Zielprozeß in einer Weise weiterreicht, als wäre sie lokal generiert worden. Die Programmiertechnik für die Client- und Server-Komponenten ist daher nicht wesentlich unterschiedlich zur konventionellen Programmentwicklung. Die RPC-Komponenten versuchen sämtliche benötigten Netzwerkmechanismen vor dem Entwickler zu verbergen, sie ihm aber dennoch vollständig zur Verfügung zu stellen.

Um nun ein einfaches Client/Server-System mit Hilfe von RPCs zu erzeugen, sind vier Quelldateien nötig:

Mit den Dateien RPCNET.IDL und RPCNET.ACL wird der Microsoft IDL Compiler (MIDL) veranlaßt, Client- und Server-Stubs zu erzeugen. Das Ergebnis sind zwei C-Dateien (RPCNET_C.C für den Client sowie RPCNET_S.C für den Server) und eine gemeinsame Header-Datei RPCNET.H, die die C-Prototypen für gemeinsame Datenstrukturen und Remote-Aufrufe enthält. Die Funktionalität dieses Beispiels soll die Ausgabe einer Zeichenkette auf der Serverseite durch einen entsprechenden Aufruf des Clients sein.

// RPCNET.IDL
[uuid(a7f70df0-836a-11d0-9281-0000c072920b)]
interface Rpcnet
{
  void TypeOnServer([in, string] char* pszStringToServer);
}
// RPCNET.ACF
[auto_handle]
interface Rpcnet
{
}
// RPCCLT.C
#include <stdio.h>
#include <rpc.h>
#include "rpcnet.h"
void main(void)
{
  // Aufruf einer lokalen Prozedur
  TypeOnClient("Ausgabe durch den Client\n");
  // Aufruf einer Remote-Prozedur
  TypeOnServer("Ausgabe durch den Server\n");
}
void TypeOnClient(char* pszClientString)
{
  printf("Client: %s\n", pszClientString);
}
// RPCSRV.C
#include <stdio.h>
#include <rpc.h>
#include "rpcnet.h"
void main(void)
{
  RPC_STATUS status;
  // Hier fehlen noch Funktionen, um den Server zu
  // initialisieren.
  Status = RpcServerListen(...);
  return;
}
// Sobald ein Client-Request ankommt, wird ein Thread
// gestartet, der die folgende Funktion ausführt.
void TypeOnServer(char* pszStringFromClient)
{
  printf("From Client: %s\n", pszStringFromClient);
}

Das Listing der Datei RPCSRV.C ist noch unvollständig, die fehlenden Funktionen (z.B. RpcServerListen()) dienen der Initialisierung des Main-Threads, dem Starten von Child-Threads und dem Empfangen von Anforderungen der Clients. Sobald die RPC-Laufzeitumgebung eine solche Nachricht empfängt, übergibt sie diese an einen der Child-Threads, der die Remote-Funktion TypeOnServer() ausführt. Zuletzt werden mögliche Ergebnisse an den Client zurückübermittelt, was im obenstehenden Beispiel aber nicht der Fall ist.

Die geheimnisvolle hexadezimale Zahl am Anfang der Datei RPCNET.IDL nennt sich entweder Universal Unique Identifier (UUID) oder Global Unique Identifier (GUID). Der zweite Namen ergab sich aus der Erkenntnis, daß es wohl doch nicht möglich sei, eine im gesamten Universum einmalige Zahl mit einem irdischen Programm zu erzeugen. Die Reduktion auf eine „nur" global eindeutige Zahl sollte jedoch die Laufeigenschaften der wenigsten uns bekannten Applikationen beeinflussen können. Im folgenden werden wir jedoch weiterhin den Konventionen folgen und den Namen UUID verwenden.

Eine UUID ist so etwas wie ein Fingerabdruck, der eine RPC-Kommunikationsschnittstelle unabhängig von jeglicher Netzwerkkonfiguration eindeutig identifiziert. Die Schnittstelle beinhaltet neben der UUID die optionale Definition von verwendeten Datentypen (z.B. Arrays) sowie die Deklaration der Remote-Funktionen. Ein RPC-Server kann hierbei mehrere Schnittstellen besitzen, die jeweils aus einer Vielzahl von Funktionen bestehen können.

Zur Erzeugung einer UUID bzw. einer neuen RPC-Schnittstelle verwendet man das Programm UUIDGEN.EXE, das z.B. in der Entwicklungsumgebung des Microsoft Visual C++-Compilers enthalten ist. UUIDGEN erzeugt die UUID durch die Verwendung der aktuellen Uhrzeit und von bestimmten Netzwerkadreßinformationen.

Der Aufruf der Zeile

uuidgen -i -orpcnet.idl

aus der Kommandozeile („DOS-Box") führt zur Erzeugung der Datei RPCNET.IDL mit folgendem Inhalt:

[
uuid(a7f70df0-836a-11d0-9281-0000c072920b),
version(1.0)
]
interface INTERFACENAME
{
}

Abbildung 22.1: Bildschirmschnappschuß bei verschiedenen Aufrufen von UUIDGEN.EXE aus der NT-Shell.

Dies ist auch schon die Ausgangsbasis für die weiter oben aufgeführte Datei RPCNET.IDL. Mit einem ASCII-Editor oder mit dem Developer Studio kann nun RPCNET.IDL erweitert werden. Die Erweiterungen umfassen den gewählten Namen der Schnittstelle (Rpcnet) und die verwendete Remote-Funktion (TypeOnServer()). Die [in]- bzw. [out]-Parameterattribute sind bei der Funktionsdeklaration nötig, um dem MIDL-Compiler mitzuteilen, in welche Richtung die Daten über das Netz geschickt werden sollen:

[in]: Ein Wert wird zur Remote-Prozedur übergeben, wenn sie vom Client aufgerufen wird.

[out]: Ein Wert wird vom Server zur aufrufenden Prozedur auf dem Client zurückgeschickt, wenn die Remote-Prozedur abgearbeitet wurde. Ein out-Parameter muß ein Pointer oder ein Array sein, damit er über eine Referenz zum Client-Stub gelangen kann.

Das obenstehende Beispiel hat nur einen [in]-Parameter, der einen Pointer auf eine Zeichenkette referenziert. Daran erkennt man schon, daß nicht nur die Verwendung von einfachen Datentypen wie short oder int, sondern auch von Zeichenketten (Pointer auf Char) über RPCs möglich sind. Weiterhin gehören mehrdimensionale Arrays, Strukturen, Zeiger und Unions zu den unterstützten Datentypen. Hierbei läßt sich auch eine wesentliche Funktionalität des sendenden Stubs erkennen: Er sendet nicht nur die Adresse des Datenobjekts zum Empfänger, sondern auch das adressierte Datenobjekt selbst. Dazu werden alle Daten, die über das Netz gehen, in das NDR-Format (Network Data Representation) konvertiert. Der Empfänger-Stub wandelt diese Daten zurück in das Format des Ausgangsobjekt und generiert daraus auch wieder die zugehörigen Pointer.

Der Client-Prozeß kann nun Funktionen mit den unterschiedlichsten Parametern aufrufen, wenn sie in der .IDL-Datei entsprechend definiert wurden. Sie werden dann innerhalb des Server-Prozesse ausgeführt. Wie findet aber der Client einen zugehörigen Server? Hierzu gibt es eine Reihe von RPC-Mechanismen wie die Registrierung von Schnittstellen, die Verwendung von Name Services, die Etablierung von Endpunktion und den Aufbau einer Verbindung zu einem Objekt. Schlußendlich ist die RPC-Verbindung zwischen zwei entfernten Prozessen das Resultat einer erfolgreichen Suche des Clients nach einem Server.

Ein RPC-Server wird dabei durch folgende Attribute charakterisiert: Die individuelle Netzwerkadresse, die unterstützte Protokolle (Named Pipes, TCP/IP oder IPX/SPX), die vom Transportprotokoll abhängigen Endpunkt des Server-Dienstes und die Schnittstellen als Sammlung von RPC-Funktionen. Die ersten drei dienen der Auffindung durch den Client, die letzte spezifiziert den Server-Dienst.

Das Auffinden des Servers und die anschließende Anbindung an den Client (Binding) kann grundsätzlich über verschiedene Methoden erfolgen:

Der Client gibt der RPC-Laufzeitumgebung genügend Detailinformationen (Binding-Handle) über den Server wie z.B. die Server-Adresse. Diese Informationen sind entweder vom Benutzer zu erfragen oder sie sind fest im Programm-Code enthalten. Die zugehörigen API-Funktionen auf der Client-Seite sind RpcStringBindingCompose(), RpcBindingFromStringBinding(), RpcBindingFree(), RpcStringFree(), usw.

Der Server exportiert die von ihm unterstützten Schnittstellen über einen Name Service. Dieser bietet eine Datenbankfunktionalität, mit deren Hilfe die Server-Charakteristika im Netzwerk bekannt gemacht werden. Über die Verknüpfung eines logischen Namens mit den Server-Schnittstellen ist die Lokalisierung der zugehörigen Dienste für Clients in einer generalisierten Weise möglich. Die zugehörigen API-Funktionen auf der Server-Seite sind: RpcServerUseProtseqEp(), RpcServerRegisterIf(), RpcServerListen(), RpcServerStopListening(), usw.

Die Anbindung zwischen Client und Server (Binding) kann hierbei auf drei verschiedene Arten erfolgen:

Trotz der verschiedenen Methoden zur Anbindung zwischen RPC-Client und RPC-Server ist das Konzept der Kommunikation immer das gleiche: Der Client ruft den Server und wartet auf dessen Antwort. Der Server erhält die Anfrage, verarbeitet sie und sendet eine Antwort zurück. Der Client erhält die Antwort und arbeitet weiter. Die Verbindung zwischen Client und Server wird hierbei zwischen zwei RPC-Aufrufen bleibt typischerweise nicht bestehen. Dennoch kann auf der Server-Seite die Aktivität individueller Clients verfolgt werden, da der Server Stub Client-Informationen (z.B. Netzwerkadresse) bei jeder Verbindungsaufnahme erhält und über API-Funktionen zugänglich macht. Spezielle Server-Threads können sich daher potentiell um die Archivierung von individuellen Client-Informationen kümmern und sie geeignet aufbereiten.

Das Allokieren und Freigeben von Speicher über Rechnergrenzen hinaus wird ebenfalls durch entsprechende RPC-Mechanismen erlaubt. Die hierzu verwendeten API-Funktionen sind midl_user_allocate() und midl_user_free.

Als besondere Erweiterung zu DCE bieten die Microsoft-RPCs die Unterstützung von Callbacks. Damit kann der Client eine Callback-Routine für den Server verfügbar machen. Die Implementierung wird vom Client durchgeführt. Der Server kann die Callback-Funktion aufrufen, wobei er die selbe Verbindung benutzt, die der Client zuerst aufgebaut hat, um den Server anzusprechen.

Zum nächsten Kapitel