Remote Procedure Calls (RPC): Unterschied zwischen den Versionen

Aus dev.kaibel.net
Zur Navigation springen Zur Suche springen
Die Seite wurde neu angelegt: „= rpcgen (C-Werkzeug) = Das Programm '''rpcgen''' (*Remote Procedure Call Generator*) ist ein Hilfswerkzeug der C-Programmierumgebung, das Quellcode für die Implementierung von '''Remote Procedure Calls (RPC)''' automatisch erzeugt. Es ist Teil des SunRPC-Systems (heute als ONC RPC bekannt) und wird auf UNIX- und Linux-Systemen häufig zur Entwicklung verteilter Anwendungen verwendet. --- == Zweck == '''rpcgen''' automatisiert die Erstellung der not…“
 
Keine Bearbeitungszusammenfassung
 
Zeile 1: Zeile 1:
= rpcgen (C-Werkzeug) =
= Remote Procedure Calls (RPC) =


Das Programm '''rpcgen''' (*Remote Procedure Call Generator*) ist ein Hilfswerkzeug der C-Programmierumgebung, das Quellcode für die Implementierung von '''Remote Procedure Calls (RPC)''' automatisch erzeugt.   
'''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.   
Es ist Teil des SunRPC-Systems (heute als ONC RPC bekannt) und wird auf UNIX- und Linux-Systemen häufig zur Entwicklung verteilter Anwendungen verwendet.
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.


== Zweck ==
RPC kapselt diese wiederkehrenden Aufgaben in Stubs, Laufzeitbibliotheken und standardisierte Datenformate.
'''rpcgen''' automatisiert die Erstellung der notwendigen Client- und Server-Kommunikationsroutinen für verteilte Programme. 
Statt alle Netzwerkfunktionen (z. B. Socket-Erstellung, Serialisierung, Übertragung) manuell zu implementieren, kann der Entwickler nur die '''Funktionsschnittstellen''' in einer speziellen Beschreibungssprache (*.x*-Datei) definieren – den Rest erledigt rpcgen.


Beispielhafter Ablauf:
== Grundkonzept ==
<pre>
<pre>
.x-Datei → rpcgen → generierter C-Code → Kompilierung zu Client & Server
Client (Stub) --(encode)--> Transport (TCP/UDP) --(decode)--> Server (Stub + Implementierung)
</pre>
</pre>


---
* '''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)]]).


== Funktionsweise ==
== Typische Schritte ==
Der Entwickler beschreibt die zu übertragenden Datentypen und Prozeduren in einer '''RPC-Interface-Definition-Datei''' mit der Endung <code>.x</code>. 
# Schnittstelle definieren (Signaturen, Datentypen)
Diese Datei wird anschließend von rpcgen verarbeitet, um automatisch:
# (Optional) Codegenerator (z. B. '''rpcgen''') ausführen → Stubs & XDR-Code
* Header-Dateien (.h)
# Server-Implementierung schreiben
* Client-Stubs (.c)
# Client-Programm schreiben
* Server-Stubs (.c)
# Start: Server registriert sich (Portmapper/Dispatcher), Client ruft Funktionen auf
* Serialisierungsfunktionen (XDR-Code)


zu erzeugen.
----


---
= ONC RPC (SunRPC) mit rpcgen in C =


== Aufruf ==
Das klassische ONC RPC (ehem. SunRPC) verwendet '''rpcgen''', um aus einer <code>.x</code>-Datei (IDL) automatisch C-Code zu generieren.
<syntaxhighlight lang="bash">
rpcgen [Optionen] datei.x
</syntaxhighlight>
 
**Wichtige Optionen:**
{| class="wikitable"
! Option !! Beschreibung
|-
| <code>-C</code> || Erzeugt ANSI-C-kompatiblen Code (Standard bei modernen Systemen)
|-
| <code>-a</code> || Erzeugt alle notwendigen Dateien (Client, Server, Header, Makefile)
|-
| <code>-c</code> || Erzeugt nur die XDR-Konvertierungsfunktionen
|-
| <code>-h</code> || Erzeugt nur die Header-Datei
|-
| <code>-l</code> || Erzeugt nur die Server-Seiten-Stubs
|-
| <code>-m</code> || Erzeugt nur das Hauptprogramm des Servers
|-
| <code>-t</code> || Erzeugt nur die Client-Seiten-Stubs
|-
| <code>-M</code> || Verhindert Mehrfachdefinitionen bei mehreren <code>rpcgen</code>-Durchläufen
|}
 
---
 
== Beispiel: RPC-Definition ==
Beispielhafte Datei <code>calc.x</code> für einen einfachen RPC-Rechnerdienst:


== 1) Schnittstelle definieren (.x) ==
Datei <code>calc.x</code>:
<syntaxhighlight lang="c">
<syntaxhighlight lang="c">
/* calc.x – Beispiel für rpcgen */
/* calc.x – Beispiel */
struct intpair {
    int a;
    int b;
};


program CALCPROG {
program CALCPROG {
Zeile 69: Zeile 50:
     } = 1;
     } = 1;
} = 0x20000001;
} = 0x20000001;
struct intpair {
    int a;
    int b;
};
</syntaxhighlight>
</syntaxhighlight>


---
== 2) Code generieren ==
 
== Generierung ==
Mit dem Befehl:
<syntaxhighlight lang="bash">
<syntaxhighlight lang="bash">
rpcgen -a calc.x
rpcgen -a calc.x
# erzeugt u. a.: calc.h, calc_xdr.c, calc_clnt.c, calc_svc.c,
# sowie Beispielclient/-server und ein Makefile.*
</syntaxhighlight>
</syntaxhighlight>


werden folgende Dateien erzeugt:
== 3) Server-Implementierung (Auszug) ==
{| class="wikitable"
! Datei !! Zweck
|-
| <code>calc.h</code> || Enthält Konstanten, Datentypdefinitionen und Funktionsprototypen
|-
| <code>calc_clnt.c</code> || Client-seitige Stub-Funktionen
|-
| <code>calc_svc.c</code> || Server-seitige Stub-Funktionen
|-
| <code>calc_xdr.c</code> || Funktionen zur Serialisierung (XDR)
|-
| <code>calc_client.c</code> || Beispielclient (wenn -a verwendet wird)
|-
| <code>calc_server.c</code> || Beispielserver (wenn -a verwendet wird)
|-
| <code>Makefile.calc</code> || Beispiel-Makefile zum Kompilieren
|}
 
---
 
== Beispiel: Client- und Serverprogramm ==
 
=== Server (Auszug aus calc_server.c) ===
<syntaxhighlight lang="c">
<syntaxhighlight lang="c">
#include "calc.h"
#include "calc.h"


int *add_1_svc(intpair *pair, struct svc_req *req) {
int *add_1_svc(intpair *pair, struct svc_req *req) {
     static int result;
     static int result;           // static: Speicherdauer über Funktionsaufruf hinaus
     result = pair->a + pair->b;
     result = pair->a + pair->b;   // eigentliche Logik
     return &result;
     return &result;               // Rückgabe per Zeiger
}
}


Zeile 124: Zeile 76:
</syntaxhighlight>
</syntaxhighlight>


---
== 4) Client-Programm (Auszug) ==
 
=== Client (Auszug aus calc_client.c) ===
<syntaxhighlight lang="c">
<syntaxhighlight lang="c">
#include "calc.h"
#include "calc.h"
#include <stdio.h>
#include <stdio.h>
#include <stdlib.h>


int main(int argc, char *argv[]) {
int main(int argc, char *argv[]) {
    CLIENT *clnt;
    intpair p;
    int *result;
     if (argc != 4) {
     if (argc != 4) {
         printf("Verwendung: %s host zahl1 zahl2\n", argv[0]);
         fprintf(stderr, "Usage: %s HOST A B\n", argv[0]);
         exit(1);
         return 1;
     }
     }


     clnt = clnt_create(argv[1], CALCPROG, CALCVERS, "udp");
     CLIENT *clnt = clnt_create(argv[1], CALCPROG, CALCVERS, "tcp");
     if (clnt == NULL) {
     if (!clnt) {
         clnt_pcreateerror(argv[1]);
         clnt_pcreateerror(argv[1]);
         exit(1);
         return 1;
     }
     }


     p.a = atoi(argv[2]);
     intpair p = { atoi(argv[2]), atoi(argv[3]) };
    p.b = atoi(argv[3]);
     int *res = add_1(&p, clnt);         // Stub ruft remote ADD() auf
 
     if (!res) {
     result = add_1(&p, clnt);
         clnt_perror(clnt, "RPC error");
     if (result == NULL)
        clnt_destroy(clnt);
         clnt_perror(clnt, "Fehler bei RPC");
        return 1;
     else
     }
        printf("Ergebnis: %d\n", *result);
    printf("ADD result: %d\n", *res);


     clnt_destroy(clnt);
     clnt_destroy(clnt);
Zeile 161: Zeile 108:
</syntaxhighlight>
</syntaxhighlight>


---
== 5) Datenkodierung (XDR) ==
rpcgen erzeugt automatisch XDR-Funktionen (z. B. <code>xdr_intpair()</code> in <code>calc_xdr.c</code>), die die plattformneutrale Darstellung sicherstellen.
 
== 6) Ablauf ==
<pre>
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
</pre>
 
== Vorteile von ONC RPC ==
* Wenig Boilerplate durch Codegenerierung
* Portmapper-Registrierung (Service Discovery)
* Effiziente Binärkodierung (XDR)


== XDR (External Data Representation) ==
== Hinweise ==
rpcgen erzeugt automatisch Funktionen zur Umwandlung der Datenstrukturen in ein plattformunabhängiges Format ('''XDR'''), um die Kommunikation zwischen unterschiedlichen Systemarchitekturen zu ermöglichen.
* 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.


Beispiel aus <code>calc_xdr.c</code>:
----
 
= 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) ==
{| class="wikitable"
! 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:
{| class="wikitable"
! 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 ==
<syntaxhighlight lang="c">
<syntaxhighlight lang="c">
bool_t xdr_intpair(XDR *xdrs, intpair *objp) {
#include <unistd.h>
     if (!xdr_int(xdrs, &objp->a)) return FALSE;
#include <stdint.h>
     if (!xdr_int(xdrs, &objp->b)) return FALSE;
#include <arpa/inet.h>
     return TRUE;
#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;
}
}
</syntaxhighlight>
</syntaxhighlight>


---
== Server (Minimalbeispiel) ==
<syntaxhighlight lang="c">
#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;
    }


== Ablauf eines RPC-Aufrufs ==
    uint32_t outlen = status == 0 ? (1 + 4) : 1;
<pre>
    uint32_t outlen_n = htonl(outlen);
Client          Server
    if (writen(cfd, &outlen_n, 4) != 4) return -1;
  |              |
    if (writen(cfd, &status, 1) != 1) return -1;
  |  Anfrage ---> |
    if (status == 0) {
  | <--- Antwort  |
        int32_t nres = htonl(result);
</pre>
        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;
}
</syntaxhighlight>
 
== Client (Minimalbeispiel) ==
<syntaxhighlight lang="c">
#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;
}


Der Client ruft die generierte Stub-Funktion (z. B. <code>add_1()</code>) auf, die über das Netzwerk die Anfrage an den Server sendet. 
int main(int argc, char **argv) {
Der Server empfängt die Anfrage, ruft die Implementierung (z. B. <code>add_1_svc()</code>) auf und sendet das Ergebnis zurück.
    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;
}
</syntaxhighlight>


---
== 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.


== Vorteile ==
----
* Automatische Codegenerierung für RPC-Kommunikation 
* Plattformunabhängige Datenübertragung (XDR) 
* Vereinfachte Entwicklung verteilter Systeme 
* Trennung von Schnittstelle und Implementierung 


== Nachteile ==
= Fehler- und Sonderfälle bei RPC =
* Abhängigkeit von der RPC-Laufzeitumgebung 
* '''Timeouts''' (Server nicht erreichbar / langsam) → Rückgabecodes (z. B. <code>RPC_TIMEDOUT</code>) und Wiederholungslogik.
* Komplexer bei Firewalls/NAT 
* '''Idempotenz''' bei Wiederholungen (z. B. ADD ist idempotent, Überweisungen nicht).
* Weniger verbreitet in modernen Netzwerken (ersetzt durch gRPC, REST, etc.)
* '''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 ==
= Siehe auch =
* [[Remote Procedure Call (RPC)]]
* [[rpcgen (C-Werkzeug)]]
* [[XDR (External Data Representation)]]
* [[XDR (External Data Representation)]]
* [[Stub (Softwareentwicklung)]]
* [[Stub (Softwareentwicklung)]]
* [[Marshalling (Informatik)]]
* [[Marshalling (Informatik)]]
* [[Verteilte Systeme]]
* [[Network File System (NFS)]]
* [[getaddrinfo (C-Funktion)]]
* [[gRPC]] (moderner RPC-Ansatz auf HTTP/2/Protobuf)
 
* [[Remote Procedure Call (Allgemein)]]
---


== Quellen ==
= Quellen =
* Sun Microsystems: ''rpcgen User’s Guide'' 
* Sun/ONC RPC Konzepte und Manpages (rpc, rpcgen, xdr)
* ISO/IEC 9899:2018 (C18) – Programming Language C 
* Stevens, W. Richard: ''UNIX Network Programming, Vol. 1''
* Stevens, W. Richard: ''UNIX Network Programming, Vol. 1'', Prentice Hall. 
* RFC 5531 (ONC RPC), RFC 4506 (XDR)
* Linux man page: <code>man rpcgen</code> 
* GNU C Library Documentation

Aktuelle Version vom 21. Oktober 2025, 14:22 Uhr

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)