Remote Procedure Calls (RPC)
Remote Procedure Calls (RPC)
Remote Procedure Calls (RPC) sind ein Kommunikationsprinzip, bei dem ein Programm eine Prozedur (Funktion) auf einem entfernten Rechner so aufruft, als wäre sie lokal. RPC abstrahiert Transportdetails (z. B. TCP/UDP), Datenkodierung und Fehlerbehandlung, sodass sich Entwickler auf Schnittstellen und Semantik konzentrieren können.
Motivation
Ohne RPC müssten Entwickler für jeden Aufruf selbst:
- Verbindungen aufbauen (Sockets),
- Daten serialisieren,
- Nachrichten schicken/empfangen,
- Fehlerbehandlung & Timeouts implementieren.
RPC kapselt diese wiederkehrenden Aufgaben in Stubs, Laufzeitbibliotheken und standardisierte Datenformate.
Grundkonzept
Client (Stub) --(encode)--> Transport (TCP/UDP) --(decode)--> Server (Stub + Implementierung)
- Client-Stub: erzeugt aus einem Funktionsaufruf eine Netzwerk-Nachricht.
- Server-Stub: dekodiert die Nachricht, ruft die Server-Implementierung auf und sendet das Ergebnis zurück.
- Marshaller/XDR: überführt Daten in ein plattformneutrales Format (z. B. XDR (External Data Representation)).
Typische Schritte
- Schnittstelle definieren (Signaturen, Datentypen)
- (Optional) Codegenerator (z. B. rpcgen) ausführen → Stubs & XDR-Code
- Server-Implementierung schreiben
- Client-Programm schreiben
- Start: Server registriert sich (Portmapper/Dispatcher), Client ruft Funktionen auf
ONC RPC (SunRPC) mit rpcgen in C
Das klassische ONC RPC (ehem. SunRPC) verwendet rpcgen, um aus einer .x-Datei (IDL) automatisch C-Code zu generieren.
1) Schnittstelle definieren (.x)
Datei calc.x:
/* calc.x – Beispiel */
struct intpair {
int a;
int b;
};
program CALCPROG {
version CALCVERS {
int ADD(intpair) = 1;
int SUB(intpair) = 2;
} = 1;
} = 0x20000001;
2) Code generieren
rpcgen -a calc.x
# erzeugt u. a.: calc.h, calc_xdr.c, calc_clnt.c, calc_svc.c,
# sowie Beispielclient/-server und ein Makefile.*
3) Server-Implementierung (Auszug)
#include "calc.h"
int *add_1_svc(intpair *pair, struct svc_req *req) {
static int result; // static: Speicherdauer über Funktionsaufruf hinaus
result = pair->a + pair->b; // eigentliche Logik
return &result; // Rückgabe per Zeiger
}
int *sub_1_svc(intpair *pair, struct svc_req *req) {
static int result;
result = pair->a - pair->b;
return &result;
}
4) Client-Programm (Auszug)
#include "calc.h"
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char *argv[]) {
if (argc != 4) {
fprintf(stderr, "Usage: %s HOST A B\n", argv[0]);
return 1;
}
CLIENT *clnt = clnt_create(argv[1], CALCPROG, CALCVERS, "tcp");
if (!clnt) {
clnt_pcreateerror(argv[1]);
return 1;
}
intpair p = { atoi(argv[2]), atoi(argv[3]) };
int *res = add_1(&p, clnt); // Stub ruft remote ADD() auf
if (!res) {
clnt_perror(clnt, "RPC error");
clnt_destroy(clnt);
return 1;
}
printf("ADD result: %d\n", *res);
clnt_destroy(clnt);
return 0;
}
5) Datenkodierung (XDR)
rpcgen erzeugt automatisch XDR-Funktionen (z. B. xdr_intpair() in calc_xdr.c), die die plattformneutrale Darstellung sicherstellen.
6) Ablauf
Client: add_1(&p, clnt) → clnt_call() → XDR-ENCODE → TCP/UDP → Server Server: svc_run() → svc_getargs() (XDR-DECODE) → add_1_svc() → Ergebnis XDR-ENCODE → Antwort → Client
Vorteile von ONC RPC
- Wenig Boilerplate durch Codegenerierung
- Portmapper-Registrierung (Service Discovery)
- Effiziente Binärkodierung (XDR)
Hinweise
- Firewall/NAT können RPC mit dynamischen Ports erschweren (Portmapper).
- In modernen Architekturen oft durch gRPC, REST, GraphQL ersetzt – ONC RPC ist aber weiterhin in Systemnähe (z. B. NFS) relevant.
Handgerolltes RPC über TCP (Lernbeispiel)
Hier bauen wir ein minimalistisches „RPC-Protokoll“ selbst:
- fester Request-Header,
- Operationscode (Opcode),
- Payload (z. B. zwei ints),
- Antwort mit Resultat oder Fehlercode.
Protokoll (einfach)
| Feld | Größe | Beschreibung |
|---|---|---|
| length | 4 B (u32, Network Byte Order) | Gesamtlänge der Nachricht (Header+Body) |
| opcode | 1 B | 1=ADD, 2=SUB |
| a, b | 4 B + 4 B (i32) | Operanden |
Antwort:
| Feld | Größe | Beschreibung |
|---|---|---|
| length | 4 B | wie oben |
| status | 1 B | 0=OK, !=0 Fehler |
| result | 4 B | nur bei status=0 |
Gemeinsame Hilfsfunktionen
#include <unistd.h>
#include <stdint.h>
#include <arpa/inet.h>
#include <errno.h>
ssize_t readn(int fd, void *buf, size_t n) {
size_t left = n; char *p = buf;
while (left > 0) {
ssize_t r = read(fd, p, left);
if (r < 0) { if (errno == EINTR) continue; return -1; }
if (r == 0) break; // EOF
left -= r; p += r;
}
return (ssize_t)(n - left);
}
ssize_t writen(int fd, const void *buf, size_t n) {
size_t left = n; const char *p = buf;
while (left > 0) {
ssize_t w = write(fd, p, left);
if (w <= 0) { if (w < 0 && errno == EINTR) continue; return -1; }
left -= w; p += w;
}
return (ssize_t)n;
}
Server (Minimalbeispiel)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <unistd.h>
int handle_request(int cfd) {
uint32_t nlen;
if (readn(cfd, &nlen, 4) != 4) return -1;
uint32_t len = ntohl(nlen);
if (len < 1 + 4 + 4) return -1;
unsigned char buf[1024];
if (len > sizeof(buf)) return -1;
if (readn(cfd, buf, len) != (ssize_t)len) return -1;
uint8_t opcode = buf[0];
int32_t a, b;
memcpy(&a, buf + 1, 4);
memcpy(&b, buf + 5, 4);
a = ntohl(a); b = ntohl(b);
int32_t result = 0; uint8_t status = 0;
switch (opcode) {
case 1: result = a + b; break;
case 2: result = a - b; break;
default: status = 1; break;
}
uint32_t outlen = status == 0 ? (1 + 4) : 1;
uint32_t outlen_n = htonl(outlen);
if (writen(cfd, &outlen_n, 4) != 4) return -1;
if (writen(cfd, &status, 1) != 1) return -1;
if (status == 0) {
int32_t nres = htonl(result);
if (writen(cfd, &nres, 4) != 4) return -1;
}
return 0;
}
int main(void) {
int s = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in sa = { .sin_family=AF_INET, .sin_port=htons(9090),
.sin_addr.s_addr=htonl(INADDR_ANY) };
bind(s, (struct sockaddr*)&sa, sizeof(sa));
listen(s, 8);
printf("RPC-Server (TCP 9090) läuft...\n");
for (;;) {
int c = accept(s, NULL, NULL);
if (c < 0) continue;
handle_request(c);
close(c);
}
close(s);
return 0;
}
Client (Minimalbeispiel)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <unistd.h>
int rpc_call(const char *host, uint8_t opcode, int32_t a, int32_t b, int32_t *out) {
int s = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in sa = { .sin_family=AF_INET, .sin_port=htons(9090) };
inet_pton(AF_INET, host, &sa.sin_addr);
if (connect(s, (struct sockaddr*)&sa, sizeof(sa)) < 0) return -1;
unsigned char buf[1 + 4 + 4];
buf[0] = opcode;
int32_t na = htonl(a), nb = htonl(b);
memcpy(buf + 1, &na, 4);
memcpy(buf + 5, &nb, 4);
uint32_t len = sizeof(buf);
uint32_t nlen = htonl(len);
if (writen(s, &nlen, 4) != 4) return -1;
if (writen(s, buf, len) != (ssize_t)len) return -1;
uint32_t rlen;
if (readn(s, &rlen, 4) != 4) return -1;
rlen = ntohl(rlen);
unsigned char rbuf[8];
if (rlen > sizeof(rbuf)) return -1;
if (readn(s, rbuf, rlen) != (ssize_t)rlen) return -1;
uint8_t status = rbuf[0];
if (status != 0) { close(s); return -2; }
int32_t nres; memcpy(&nres, rbuf + 1, 4);
*out = ntohl(nres);
close(s);
return 0;
}
int main(int argc, char **argv) {
if (argc != 4) { fprintf(stderr,"Usage: %s A B host\n", argv[0]); return 1; }
int32_t a = atoi(argv[1]), b = atoi(argv[2]), out = 0;
if (rpc_call(argv[3], 1, a, b, &out) == 0)
printf("ADD(%d,%d) = %d\n", a, b, out);
else
printf("RPC fehlgeschlagen\n");
return 0;
}
Lerneffekte des Handbeispiels
- Nachrichtenrahmung (Längenfeld) ist unverzichtbar.
- Netzwerkbyteordnung (htonl/ntohl) sorgt für Portabilität.
- Fehler-/Timeoutbehandlung und Wiederholungen gehören in robuste Implementierungen.
- Für echte Projekte sind Generatoren (rpcgen, gRPC) praktischer und sicherer.
Fehler- und Sonderfälle bei RPC
- Timeouts (Server nicht erreichbar / langsam) → Rückgabecodes (z. B.
RPC_TIMEDOUT) und Wiederholungslogik. - Idempotenz bei Wiederholungen (z. B. ADD ist idempotent, Überweisungen nicht).
- Versionierung (Schnittstellen erweitern, ohne alte Clients zu brechen).
- Sicherheit (AuthN/AuthZ, TLS, Firewalls; ONC RPC unterstützt AUTH-Schemen, moderne Systeme nutzen i. d. R. TLS/MTLS).
Vor- und Nachteile
- Vorteile
- Saubere Abstraktion, klare Schnittstellen, Wiederverwendbarkeit, Generatoren sparen Zeit.
- Nachteile
- Komplexität (Portmapper, Firewalls, NAT), Debugging über Prozessgrenzen, enge Kopplung bei synchronen Calls.
Siehe auch
- rpcgen (C-Werkzeug)
- XDR (External Data Representation)
- Stub (Softwareentwicklung)
- Marshalling (Informatik)
- Network File System (NFS)
- gRPC (moderner RPC-Ansatz auf HTTP/2/Protobuf)
- Remote Procedure Call (Allgemein)
Quellen
- Sun/ONC RPC Konzepte und Manpages (rpc, rpcgen, xdr)
- Stevens, W. Richard: UNIX Network Programming, Vol. 1
- RFC 5531 (ONC RPC), RFC 4506 (XDR)