Remote Procedure Calls (RPC)

Aus dev.kaibel.net
Zur Navigation springen Zur Suche springen

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

  1. Schnittstelle definieren (Signaturen, Datentypen)
  2. (Optional) Codegenerator (z. B. rpcgen) ausführen → Stubs & XDR-Code
  3. Server-Implementierung schreiben
  4. Client-Programm schreiben
  5. 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

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)