Windows-Prozesskommunikation richtig wählen ── Eine Entscheidungstabelle für Named Pipes / TCP / gRPC / Shared Memory / COM

· · Prozesskommunikation, Named Pipes, Windows, .NET, C#, gRPC, Shared Memory, COM, Architektur, Entscheidungstabelle, Technische Beratung

„Wenn wir UI und Dienst trennen, wie sollen die beiden Seiten miteinander sprechen?“ „Ich möchte, dass eine 32-Bit-App Funktionen aus einer 64-Bit-DLL nutzt.“ „Ich möchte Daten von einer in einem eigenen Prozess laufenden Mess-Engine live auf den Bildschirm streamen.“ Eine Anwendung in mehrere Prozesse aufzuteilen, ist eine gängige, pragmatische Antwort auf Robustheit, Rechtetrennung und Bitness-Konflikte — doch sobald man aufteilt, stellt sich unweigerlich die Frage, wie die Teile miteinander kommunizieren sollen.

In diesem Blog haben wir bereits über einzelne Kommunikationsmechanismen geschrieben: Fallstricke von Shared Memory, TCP-Framing, den sicheren Umgang mit Kindprozessen und dateibasierte Kopplung und Sperren. Was noch fehlte, war das größere Bild — welcher Mechanismus sollte überhaupt zuerst gewählt werden? Dieser Artikel ordnet die wichtigsten Optionen für die Windows-Prozesskommunikation (IPC) — dateibasierte Kopplung, Named Pipes, lokales TCP, gRPC, Shared Memory und COM — anhand ihrer Stärken und Fallstricke, im für diesen Blog typischen Format einer Entscheidungstabelle.

1. Zuerst das Fazit

  • Für Anfrage-Antwort auf demselben Rechner (einen Befehl senden, ein Ergebnis erhalten) sind Named Pipes der erste Kandidat. Sie sind ein OS-natives Primitiv, brauchen keinen Port, sind in die Windows-Zugriffskontrolle integriert und lassen sich aus .NET über System.IO.Pipes unkompliziert nutzen.1
  • Besteht auch nur die Möglichkeit, dass die Kommunikation irgendwann das Netzwerk überschreiten muss, setzen Sie von Anfang an auf lokales TCP oder gRPC. Eine spätere Migration von einer Pipe zu einem Socket ist meist mehr als „eine kleine Anpassung“ — es ist ein echter Neuentwurf. Beachten Sie, dass rohes TCP nur ein Bytestrom ist, sodass der Entwurf eines Framing-Schemas zwingend ist.
  • Wächst die Zahl der Dienste oder Aufruftypen und wird die Pflege eines eigenen Protokolls zur Last, greifen Sie zu gRPC. Sie erhalten Schemadefinitionen und Codegenerierung über proto-Dateien sowie bidirektionales Streaming, und seit .NET 8 kann ASP.NET Core (Kestrel) Named Pipes als Transport nutzen.2
  • Verwenden Sie Shared Memory nur für große Datenmengen mit hoher Frequenz (Bildframes, Wellenformdaten). Es ist die schnellste Option, doch Sie entwerfen die gesamte Synchronisierung selbst — die goldene Regel lautet: Legen Sie niemals auch Steuernachrichten in den Shared Memory.3
  • Für lose gekoppelte, asynchrone Kopplung, bei der Sie eine Nachweiskette wünschen, ist dateibasierte Kopplung nach wie vor eine starke Wahl. Erfolg oder Misserfolg hängt hier allerdings vollständig davon ab, wie Sie den wechselseitigen Ausschluss entwerfen.
  • COM (Out-of-Process) ist bei neuen Entwürfen nicht die erste Wahl, bleibt aber im Kontext der Nutzung aus VBA/anderen Sprachen oder als Brücke zwischen 32-Bit und 64-Bit ein aktives Werkzeug.4
  • Für welchen Mechanismus Sie sich auch entscheiden — den gemeinsamen Entwurfsherausforderungen Nachrichtengrenzen, Versionsnummern, Timeouts und Wiederverbindung (Kapitel 6) können Sie nicht entkommen. Investieren Sie hier ebenso viel Zeit wie in die Wahl des Mechanismus selbst.

2. Das Profil der einzelnen Optionen

2.1 Dateibasierte Kopplung ── lose gekoppelt, asynchron, nachweisbar

Dies ist das klassische Muster: „A legt eine Datei in einem Ausgabeordner ab, B holt sie ab und verarbeitet sie.“ Beide Seiten müssen nicht gleichzeitig laufen, der Austausch hinterlässt als Dateien eine Nachweiskette, und im Fehlerfall kann ein Mensch die Datei direkt ansehen, korrigieren und erneut einreichen. Für batchartige Kopplung ohne Echtzeitanforderungen ist dies nach wie vor ein wirkungsvoller Ansatz.

Der Fallstrick lässt sich fast auf einen einzigen Punkt zurückführen: den wechselseitigen Ausschluss. „Eine noch geschriebene Datei lesen“ und „zwei Prozesse streiten sich um dieselbe Datei“ sind die klassischen Unfälle, und Sie müssen Standardpraktiken wie das Schreiben unter einem temporären Dateinamen mit anschließendem Umbenennen befolgen. Die Details stehen in „Best Practices für Dateikopplung und Sperren“ — ziehen Sie den Artikel unbedingt heran, wenn Sie diesen Ansatz wählen. Für interaktive Kommunikation mit gewünschter Antwort oder für Austausch, der dutzende Male pro Sekunde stattfindet, eignet er sich nicht.

2.2 Named Pipes ── die erste Wahl für IPC auf demselben Rechner

Named Pipes sind ein vom Windows-Kernel bereitgestellter bidirektionaler Kommunikationskanal und die erste Wahl für Client-Server-artige IPC auf demselben Rechner. In .NET werden sie über NamedPipeServerStream / NamedPipeClientStream gehandhabt, und mehrere Clients können sich mit einem einzigen Pipe-Namen verbinden.5 Anders als bei TCP gibt es keine Portnummer zu verwalten, und sie werden nicht von Firewalls blockiert.

Hier die Punkte, die in der Praxis zählen.

  • Der Nachrichtenmodus steht zur Verfügung. Die Angabe von PipeTransmissionMode.Message sorgt dafür, dass jeder Schreibvorgang als eigenständige Nachrichtengrenze zugestellt wird. Das ist ein bedeutender praktischer Vorteil — das Betriebssystem übernimmt für Sie die bei TCP zwingende Rahmenbildung (Längenpräfixe usw.) (die Empfängerseite muss eine Nachricht aber weiterhin vollständig mit IsMessageComplete auslesen; siehe Kapitel 5).
  • Die Standard-Zugriffsrechte sind überraschend großzügig. Der Standard-Sicherheitsdeskriptor einer Pipe gewährt LocalSystem, Administratoren und dem Ersteller volle Kontrolle, gewährt aber auch Everyone und anonymen Konten Lesezugriff.6 Für Prozesskommunikation innerhalb desselben Benutzers erzwingt das bloße Hinzufügen von PipeOptions.CurrentUserOnly „nur mit einer Gegenseite verbinden, die vom selben Benutzer erstellt wurde“ — ich empfehle, dies zur Standardpraxis zu machen.7 Für kontoübergreifende Kommunikation (z. B. mit einem Dienst) definieren Sie das ACL explizit über PipeSecurity.
  • Pipe-Namen liegen in einem für alle sichtbaren Namensraum. Pipe-Namen befinden sich in einem einzigen Namensraum unter \\.\pipe\, der auch für Prozesse anderer Benutzer auf demselben Rechner sichtbar ist. Worauf Sie hier achten müssen, ist Namensbesetzung (Squatting). Richtet ein böswilliger Prozess zuerst einen Server unter demselben Namen ein, verbindet sich ein Client stattdessen mit diesem. Auf Rechnern, die von Benutzern mit unterschiedlichen Berechtigungsstufen gemeinsam genutzt werden, sollten Sie Gegenmaßnahmen vorsehen: Auf der Serverseite PipeOptions.FirstPipeInstance angeben, damit es fehlschlägt, wenn bereits eine gleichnamige Pipe existiert; auf der Clientseite nach der Verbindung das Besitzerkonto der Pipe prüfen.8
  • Kommunikation über eine Berechtigungsgrenze hinweg ist möglich. Dies ist ein Standardkanal zwischen einer „UI mit Standardrechten und einem erhöhten Broker“, getrennt durch UAC. Beachten Sie jedoch, dass CurrentUserOnly in dieser Konfiguration nicht verwendbar ist (es erfordert auch bei demselben Benutzer eine übereinstimmende Erhöhungsstufe7). Sie müssen das ACL explizit entwerfen — die Details stehen in „Trennung der Administratorrechte (Broker)“.

Die Schwächen sind, dass sie sich für rechnerübergreifende Kommunikation praktisch nicht eignen (technisch möglich, aber mit zahlreichen betrieblichen Einschränkungen) und dass sie sich für die Zusammenarbeit mit Nicht-Windows-Systemen nicht leicht nutzen lassen. Zeichnen sich solche Anforderungen ab, wählen Sie stattdessen TCP / gRPC.

2.3 Lokales TCP ── Vielseitigkeit über Sprachen und Betriebssysteme hinweg

Eine TCP-Verbindung zu localhost ist so ziemlich der universellste IPC-Mechanismus, den es gibt — er lässt sich aus fast jeder Sprache, jeder Laufzeit und jedem Betriebssystem ansprechen. Bei einer gemischten Konstellation wie „eine Analyse-Engine unter Linux plus eine UI unter Windows“ ist TCP (oder darauf aufbauend HTTP/gRPC) der erste Kandidat.

Drei Punkte, auf die Sie achten sollten:

  • Es ist ein Bytestrom. TCP garantiert nicht, dass Daten „in denselben Einheiten ankommen, in denen sie gesendet wurden“. Es ist völlig normal, dass drei über Send gesendete Nachrichten in einem einzigen Receive zusammengefasst ankommen, oder dass eine einzelne Nachricht über mehrere Lesevorgänge verteilt eintrifft. Sie müssen das Framing (etwa ein Längenpräfix) selbst auf Anwendungsebene entwerfen — Code, der dies auslässt, „funktioniert bloß zufällig“. Siehe „Der Irrglaube, man könne über TCP pro Send genau eine Einheit empfangen“ für Details.
  • Beschränken Sie den Umfang dessen, was Ihr Listener offenlegt. Auch wenn Sie nur rechnerinterne Kommunikation beabsichtigen, erlaubt das Lauschen auf 0.0.0.0 anderen Rechnern im Netzwerk, sich zu verbinden. Für lokale IPC gilt die Regel, an 127.0.0.1 (Loopback) zu binden. Selbst dann kann sich noch ein anderer Benutzer auf demselben Rechner verbinden, fügen Sie also eine Authentifizierung auf Anwendungsebene hinzu, wenn Sie die Gegenseite überprüfen müssen. Das ist eine klare Lücke im Vergleich zu Pipes, die über eine OS-integrierte Zugriffskontrolle verfügen.
  • Portverwaltung kommt automatisch dazu. Ein fester Port kann mit anderer Software kollidieren, und Firewall-Produkte können ihn als „verdächtigen Listener“ einstufen. Bauen Sie von Anfang an eine Möglichkeit ein, die Portnummer zu ändern.

2.4 gRPC ── Schema und Codegenerierung kaufen

Verglichen mit einem rohen Socket plus selbstgebautem Protokoll liefert gRPC weniger die Kommunikation selbst als vielmehr einen Entwicklungsrahmen. Definieren Sie Dienste und Nachrichten in einer proto-Datei, und Serialisierung, Framing sowie Client-/Server-Code werden allesamt für Sie generiert; bidirektionales Streaming (Server-Push-Benachrichtigungen) lässt sich wie ein natives Sprachmerkmal schreiben. Sobald Sie in die Phase kommen, in der „jedes Mal, wenn eine Nachricht hinzukommt, die switch-Anweisung eines eigenen Protokolls und dessen Dokumentation angefasst werden müssen“, zahlt sich dieser Rahmen aus.

Seit .NET 8 unterstützt ASP.NET Core (Kestrel) direkt Unix-Domain-Sockets und Named Pipes als Transporte, zusätzlich zu TCP.8 Auf der Serverseite rufen Sie einfach ListenNamedPipe auf, und Sie können auch die Zugriffskontrolle über PipeSecurity konfigurieren.2 Die Kombination „Transport bleibt eine portlose, ACL-integrierte Pipe, die Protokollschicht liegt auf gRPC“ ist eine starke Option für die Trennung von UI und Dienst (Kapitel 4).

Andererseits neigt es dazu, für kleine Tool-zu-Tool-Kommunikation überdimensioniert zu sein. Einen gRPC-Server aufzusetzen bedeutet, das Hosting-Fundament von ASP.NET Core mitzuschleppen, was Auslieferungsumfang, Abhängigkeiten und Startkosten erhöht. Für ein Werkzeugpaar, das nur zwei oder drei Arten von Befehlen austauscht, sind Named Pipes + JSON (Kapitel 5) insgesamt oft günstiger — behalten Sie diese Grenze fest im Blick. Es ist nicht zu spät, umzusteigen, sobald einer dieser Punkte zutrifft: „die Zahl der gewünschten Nachrichten im proto hat zehn überschritten“, „Streaming-Benachrichtigungen werden wirklich gebraucht“ oder „die Gegenseite ist kein .NET“.

2.5 Shared Memory (Memory-Mapped Files) ── am schnellsten, aber die gesamte Synchronisierung liegt bei Ihnen

Der schnellste IPC-Mechanismus innerhalb eines einzelnen Rechners ist Shared Memory. In .NET erzeugt MemoryMappedFile.CreateNew einen benannten Block gemeinsamen Speichers, den mehrere Prozesse direkt als dieselbe Bytefolge lesen und schreiben können.3 Ohne störendes Kopieren oder Serialisieren liefert es eine Leistung, die für „große und schnelle“ Daten wie Bildframes oder Wellenformdaten praktisch die einzig sinnvolle Wahl ist.

Shared Memory ist jedoch keine „schnellere Pipe“. Sie erhalten lediglich dieselben, auf beiden Seiten sichtbaren Bytes — kein einziges Byte an Synchronisierung wird bereitgestellt. Einen Mechanismus, um zu vermeiden, dass mitten im Schreibvorgang befindliche Daten gelesen werden, die Lebendigkeit der Gegenseite zu erkennen und sich nach einem abnormalen Beenden einer Seite zu erholen — all das müssen Sie selbst entwerfen. Die vollständige Entwurfsdiskussion, einschließlich Ringpuffer-Layouts und Versionierung des Speicherlayouts, ist gesammelt in „Fallstricke von Shared Memory und praktische Best Practices“.

Der praktische Einsatzfall ist klar: legen Sie nur die Datenebene in Shared Memory, und leiten Sie die Steuerungsebene (Start/Stopp/Konfigurationsänderungen) über einen separaten Kanal wie Named Pipes (Konfiguration 3 in Kapitel 4). Wenn Sie feststellen, dass Sie versuchen, auch Steuernachrichten — Start, Stopp, Konfigurationsänderungen — über Shared Memory zu schieben, ist das ein Zeichen, das Design zu überdenken.

2.6 COM (Out-of-Process) ── nicht die erste Wahl bei Neuentwürfen, aber in manchen Umgebungen weiterhin aktiv

COMs Out-of-Process-Server (EXE-Server) sind Windows’ altehrwürdiger Mechanismus, um „ein Objekt in einem anderen Prozess wie eine lokale Funktion aufzurufen“.4 Bei neuer Kommunikation zwischen Anwendungen ist es nicht mehr der erste Kandidat, bleibt aber in folgenden Zusammenhängen praktisch nutzbar:

  • Brücke zwischen 32-Bit und 64-Bit: Sie können eine 64-Bit-DLL nicht in den Prozess einer 32-Bit-App laden (oder umgekehrt), doch Out-of-Process-COM kann diese Grenze überschreiten. Da die COM-Infrastruktur das Marshalling für Sie übernimmt, können Sie den aufrufenden Code nahezu unverändert lassen. Ein Praxisbeispiel finden Sie in „Eine 32-Bit-→-64-Bit-COM-Brücke: Fallstudie“.
  • Nutzung aus VBA oder anderen Sprachen: Wenn eine ältere Laufzeitumgebung wie Excel VBA .NET-Funktionalität aufrufen soll, ist die Bereitstellung als COM auch heute noch der unkomplizierteste Weg.
  • Interoperabilität mit bestehenden COM-Beständen: Kann Ihre Gegenseite nur COM sprechen, ist es der kürzeste Weg, selbst COM zu sprechen (eine Einführung in COM selbst finden Sie in „Was sind COM, ActiveX und OCX?“).

Die Gründe, bei neuer Arbeit zu zögern, sind der Aufwand der Verteilung mit Registrierungseintragung, die Lernkosten für Schnittstellenentwurf und Referenzzählung sowie die schwierige Fehlersuche im Störungsfall. Vergleichen Sie es, bevor Sie sich festlegen, mit der Alternative aus Kapitel 4, Konfiguration 2: „Ist eine 32/64-Brücke das Ziel, machen Sie die 64-Bit-Seite einfach zu einem gewöhnlichen Hilfsprozess und sprechen über eine Named Pipe mit ihm.“

2.7 Klassische Mechanismen ── keine Wahl für Neuentwürfe

Windows besitzt mehrere weitere IPC-Mechanismen — WM_COPYDATA (Datenübertragung per Fenstermeldung), die Zwischenablage, DDE und Mailslots —, und sie erscheinen nach wie vor auf der offiziellen IPC-Übersichtsseite.4 Sie setzen jedoch entweder die Existenz eines Fensters voraus oder werden schrittweise abgeschafft (Remote-Mailslots befinden sich auf dem Weg zur Deprecation), sodass es im Grunde keinen Grund gibt, sie für einen Neuentwurf zu wählen. Es genügt, sie zu erkennen, wenn man ihnen bei der Pflege einer bestehenden Anwendung begegnet.

3. Entscheidungstabelle

Aspekt Datei Named Pipe Lokales TCP gRPC Shared Memory COM
Kommunikationsreichweite Kann über eine Freigabe rechnerübergreifend sein Praktisch nur derselbe Rechner Kann rechnerübergreifend sein Kann rechnerübergreifend sein Nur derselbe Rechner Praktisch nur derselbe Rechner
Kommunikationsmodell Dateiübergabe (asynchron) Stream + Nachrichtenmodus Bytestrom RPC + Streaming Gemeinsamer Zustand Methodenaufrufe
Benachrichtigung Server → Client ✕ (Polling) ◎ (bidirektionales Streaming) △ (braucht einen separaten Ereignismechanismus) △ (möglich, aber komplex)
Überschreiten von Berechtigungsgrenzen (UAC, Dienste) ○ (Ordner-ACL) ◎ (PipeSecurity) △ (Authentifizierung liegt bei Ihnen) △–○ (◎ mit dem Pipe-Transport) ○ (ACL möglich, Entwurf schwierig)
32/64-Bit und gemischte Sprachen ◎ (proto generiert Code für jede Sprache) △ (ABI-Entwurf erforderlich) ○ (Brückenbildung ist eine Stärke)
Implementierungsaufwand Niedrig Niedrig–mittel Mittel (Framing liegt bei Ihnen) Mittel (Rahmenwerk zu übernehmen) Hoch Hoch (bei Neuentwürfen)
Einfachheit der Fehlersuche ◎ (Inhalt bleibt als Datei erhalten) ○ (kann mitgeschnitten werden) △ (HTTP/2 + Binärformat) △ (Fehler treten oft drastisch auf)
Durchsatz/Latenz Niedrig Mittel–hoch Mittel Mittel Mittel

Ein Hinweis dazu: Statt „die Option mit den meisten ◎“ zu wählen, ist der richtige Ansatz, nur die für Ihre Anforderungen relevanten Zeilen zu betrachten und durch Ausschluss einzugrenzen. Sobald etwa feststeht, dass Sie „30 Bildframes pro Sekunde“ benötigen, ist die Datenebene eine eindeutige Wahl — Shared Memory.

4. Beispiele für Standardkonfigurationen

Bildet man die Entscheidungstabelle auf einzelne Anforderungen ab, tauchen in der Praxis immer wieder drei Konfigurationen auf.

Konfiguration 1: Trennung einer UI-App und eines Windows-Dienstes. Legen Sie residente Verarbeitung oder Arbeit, die erhöhte Rechte erfordert, in den Dienst, und lassen Sie die UI ein normaler Benutzerprozess sein (wie die Dienstseite aufgebaut wird, steht im am selben Tag veröffentlichten Begleitartikel „Artikel zu Windows-Diensten“). Named Pipes sind der bevorzugte Kommunikationskanal; das Protokoll als JSON-Nachrichten plus Nachrichtenmodus (oder Byte-Modus plus Längenpräfix) zu starten, ist eine solide Standardwahl, und es gibt einen Weg, mit wachsender Vielfalt der Aufrufe zum Named-Pipe-Transport von gRPC zu migrieren.2 Da der Dienst unter einem anderen Konto (etwa LocalService) läuft, ist CurrentUserOnly nicht verwendbar — entscheidend ist, ein ACL über PipeSecurity explizit zu definieren, etwa „Users lesen/schreiben erlauben, remote verweigern“.

Konfiguration 2: Brücke von einer 32-Bit-App zu 64-Bit-Funktionalität. Wenn Sie aus einer bestehenden 32-Bit-App eine DLL oder ein Treiber-SDK nutzen möchten, das nur unter 64-Bit läuft, gliedern Sie die 64-Bit-Seite in einen separaten Prozess aus. Es gibt zwei Implementierungswege: (a) einen 64-Bit-Hilfsprozess aufsetzen und über eine Named Pipe mit ihm sprechen, oder (b) ihn zu einem 64-Bit-Out-of-Process-COM-Server machen. Ist die aufrufende Seite VBA oder eine ältere Sprache, ist (b) die natürliche Wahl; sind beide Seiten C#, ist (a) sowohl bei der Verteilung als auch bei der Fehlersuche einfacher. Nutzen Sie für (a) das in „Kindprozesse sicher behandeln“ beschriebene Job-Object-Muster für Start, Überwachung und die gekoppelte Beendigung von Eltern- und Kindprozess des Hilfsprozesses erneut.

Konfiguration 3: Hochfrequente Daten von einer Mess-Engine zur Anzeige. Bei einer Konfiguration, in der eine Mess- oder Bildverarbeitungs-Engine in einen eigenen Prozess ausgegliedert ist und Daten an die UI streamt, ist ein Zweikanal-Entwurf das Standardmuster: Steuerung (Start/Stopp/Konfiguration) über eine Named Pipe, Daten (Frames/Wellenformen) über Shared Memory plus ein benanntes Ereignis. Steuerung findet nur wenige Male pro Sekunde statt, eine Pipe reicht also völlig; Daten kommen über Shared Memory nahe an Zero-Copy heran. Die Trennung der Kanäle erlaubt es, den Entwurf der Datenseite (etwa einen Ringpuffer) unabhängig von den Belangen der Steuerung zu optimieren. Wie der Status einer Engine oder eines externen Geräts angezeigt wird, steht auch in „Den Status externer Geräte anzeigen“.

5. Implementierungsbeispiel ── ein asynchroner Named-Pipe-Server und -Client

Hier ist eine minimale .NET-8-Implementierung, die als Grundlage für eine Named-Pipe-Konfiguration dient. Sie umfasst den Nachrichtenmodus, die Unterstützung mehrerer Clients und die Behandlung von Verbindungsabbrüchen. Das Protokoll lautet „eine UTF-8-JSON-Nachricht pro Schreibvorgang“, und es enthält immer ein Feld version. Das folgende Beispiel setzt Kommunikation innerhalb desselben Benutzers/Kontos voraus und verwendet entsprechend CurrentUserOnly. Sprechen Sie, wie in Konfiguration 1, mit einem Dienst unter einem anderen Konto, lassen Sie CurrentUserOnly weg und definieren Sie das ACL explizit mit PipeSecurity, wie im Folgenden beschrieben.

Zunächst die Serverseite. Sie baut für jede angenommene Verbindung einen neuen Serverstream neu auf und übergibt die Verarbeitung pro Client an eine eigene Task.

using System.IO.Pipes;
using System.Text.Json;

public sealed class PipeServer(string pipeName)
{
    // Web-Standardwerte: camelCase und Groß-/Kleinschreibung wird nicht unterschieden.
    // Die eigenen Standardwerte von System.Text.Json unterscheiden Groß-/Kleinschreibung;
    // ohne diese Angabe würde "version" nicht an Request.Version gebunden, und eine
    // gültige Anfrage würde zu unknown_type durchfallen
    internal static readonly JsonSerializerOptions JsonOptions =
        new(JsonSerializerDefaults.Web);

    public async Task RunAsync(CancellationToken ct)
    {
        while (!ct.IsCancellationRequested)
        {
            var pipe = new NamedPipeServerStream(
                pipeName,
                PipeDirection.InOut,
                NamedPipeServerStream.MaxAllowedServerInstances,
                PipeTransmissionMode.Message,   // ein Schreibvorgang = eine Nachricht
                PipeOptions.Asynchronous | PipeOptions.CurrentUserOnly);

            await pipe.WaitForConnectionAsync(ct);
            _ = Task.Run(() => HandleClientAsync(pipe, ct), ct);  // pro Verbindung isolieren
        }
    }

    // Obergrenze für eine einzelne Nachricht. Schützt den Speicher des Diensts,
    // selbst wenn die Gegenseite eine übergroße Nachricht sendet (auch die
    // IPC-Gegenseite ist externe Eingabe; siehe Kapitel 6)
    private const int MaxMessageBytes = 1024 * 1024;

    private static async Task HandleClientAsync(
        NamedPipeServerStream pipe, CancellationToken ct)
    {
        await using (pipe)
        {
            var buffer = new byte[64 * 1024];
            try
            {
                while (!ct.IsCancellationRequested)
                {
                    // Auch im Nachrichtenmodus ist nicht garantiert, dass ein
                    // einzelnes Read eine ganze Nachricht liest. Bis IsMessageComplete weiterlesen
                    using var ms = new MemoryStream();
                    do
                    {
                        int n = await pipe.ReadAsync(buffer, ct);
                        if (n == 0) return;  // Client hat die Verbindung getrennt
                        ms.Write(buffer, 0, n);
                        if (ms.Length > MaxMessageBytes) return;  // Obergrenze überschritten: trennen
                    } while (!pipe.IsMessageComplete);

                    byte[] response = Dispatch(ms.ToArray());
                    await pipe.WriteAsync(response, ct);
                }
            }
            catch (IOException)
            {
                // Nur der Kanal zu diesem Client ist abgebrochen.
                // Den gesamten Server nicht anhalten — andere Verbindungen weiter bedienen
            }
        }
    }

    private static byte[] Dispatch(byte[] payload)
    {
        Request? req;
        try { req = JsonSerializer.Deserialize<Request>(payload, JsonOptions); }
        catch (JsonException) { req = null; }

        // Fehlerhafte Anfragen, unbekannte Versionen und unbekannte Typen hier ablehnen
        object result = req switch
        {
            null => new { version = 1, error = "bad_request" },
            // Eine nicht angegebene Version (bindet an 0) oder eine unbekannte Version wird hier abgelehnt
            { Version: not 1 } => new { version = 1, error = "version_unsupported" },
            { Type: "getStatus" } => new { version = 1, running = true },
            { Type: "startJob" } => new { version = 1, jobId = StartJob(req) },
            _ => new { version = 1, error = "unknown_type" },
        };
        return JsonSerializer.SerializeToUtf8Bytes(result, JsonOptions);
    }
}

public sealed record Request(int Version, string Type, JsonElement? Body);

Die Clientseite kapselt „verbinden → anfragen → antworten“ in einer einzigen Methode, mit Timeout und Wiederholung von Anfang an eingebaut.

using System.IO.Pipes;
using System.Text.Json;

public sealed class PipeClient(string pipeName)
{
    // idempotent: nur dann wiederholen, wenn der Aufrufer erklärt hat, dass diese
    // Anfrage gefahrlos zweimal ankommen darf. Standard ist "nicht wiederholen"
    public async Task<TResponse?> RequestAsync<TResponse>(
        object request, CancellationToken ct, bool idempotent = false)
    {
        for (int attempt = 1; ; attempt++)
        {
            // Nicht nur für die Verbindung, sondern für den gesamten Anfrage-Antwort-
            // Austausch eine Obergrenze setzen. Das verhindert unendliches Warten,
            // falls der Server die Verbindung annimmt, aber ohne Antwort hängen bleibt
            using var deadline =
                CancellationTokenSource.CreateLinkedTokenSource(ct);
            deadline.CancelAfter(TimeSpan.FromSeconds(10));
            try
            {
                using var pipe = new NamedPipeClientStream(
                    ".", pipeName, PipeDirection.InOut,
                    PipeOptions.Asynchronous | PipeOptions.CurrentUserOnly);

                // Nicht endlos warten. Läuft der Server nicht, wirft dies eine Ausnahme
                await pipe.ConnectAsync(timeout: 3000, deadline.Token);
                pipe.ReadMode = PipeTransmissionMode.Message;

                await pipe.WriteAsync(
                    JsonSerializer.SerializeToUtf8Bytes(
                        request, PipeServer.JsonOptions),
                    deadline.Token);

                using var ms = new MemoryStream();
                var buffer = new byte[64 * 1024];
                do
                {
                    int n = await pipe.ReadAsync(buffer, deadline.Token);
                    if (n == 0) throw new IOException("Der Server hat die Verbindung getrennt.");
                    ms.Write(buffer, 0, n);
                } while (!pipe.IsMessageComplete);

                return JsonSerializer.Deserialize<TResponse>(
                    ms.ToArray(), PipeServer.JsonOptions);
            }
            catch (OperationCanceledException) when (ct.IsCancellationRequested)
            {
                throw;  // Abbruch durch den Aufrufer; nicht wiederholen
            }
            catch (Exception ex) when (
                ex is IOException or TimeoutException or OperationCanceledException
                && idempotent && attempt < 3)
            {
                // Vorübergehende Fehler wiederholen, etwa während eines Server-Neustarts.
                // Im Fall "gesendet, aber die Verbindung brach ab, bevor die Antwort
                // gelesen wurde" kann die Anfrage bereits ausgeführt worden sein,
                // deshalb werden Anfragen mit Nebenwirkungen (wie startJob) standardmäßig
                // nicht wiederholt — entwerfen Sie sie zunächst so, dass Duplikate anhand
                // der Anfrage-ID abgelehnt werden, und übergeben Sie erst dann
                // idempotent: true (siehe Kapitel 6)
                await Task.Delay(500 * attempt, ct);
            }
        }
    }
}

Drei Entwurfsentscheidungen in diesem Code verdienen einen Hinweis:

  • Nehmen Sie version schon in der ersten Veröffentlichung mit auf. Irgendwann werden UI und Dienst getrennt aktualisiert (eine Seite ist neuer als die andere). Wenn die Empfängerseite „jede unbekannte Version ausdrücklich ablehnt“, wird aus stillem Fehlverhalten ein Fehler, den Sie tatsächlich verstehen können.
  • Entscheiden Sie, ob Verbindungen Wegwerfobjekte oder langlebig sind. Das obige Beispiel nutzt den Wegwerf-Stil — für jede Anfrage neu verbinden —, was den Verwaltungsaufwand für Trennung/Wiederverbindung erspart, sich aber nicht für hochfrequente Aufrufe eignet. Brauchen Sie eine dauerhafte Verbindung plus vom Server angestoßene Push-Benachrichtigungen, ist diese zusätzliche Komplexität ein gutes Signal, zum bidirektionalen Streaming von gRPC zu wechseln.
  • Sprechen Sie mit einem Dienst, lassen Sie CurrentUserOnly weg und entwerfen Sie stattdessen PipeSecurity. Das obige Beispiel setzt Kommunikation zwischen Prozessen desselben Benutzers voraus. Ist die Gegenseite ein Dienst unter einem anderen Konto, erzeugen Sie den Serverstream mit ACL über NamedPipeServerStreamAcl.Create und geben Sie explizit an, welche Gruppe die Verbindung aufnehmen darf.6

6. Gemeinsame Belange beim Protokollentwurf ── die Arbeit, die nach der Wahl des Mechanismus bleibt

Für welchen IPC-Mechanismus Sie sich auch entscheiden, es gibt gemeinsame Herausforderungen beim Entwurf des Kommunikationsinhalts. Tatsächlich sind diese eine größere Quelle für Vorfälle als die Wahl des Mechanismus selbst.

  • Nachrichtengrenzen (Framing): Außer wenn das Betriebssystem sie für Sie abgrenzt, wie beim Nachrichtenmodus einer Pipe, liegt „wo beginnt und endet eine Nachricht“ in der Verantwortung der Anwendung. Datensatzgrenzen in Shared Memory sind dasselbe Problem. Setzen Sie standardmäßig auf ein Längenpräfix-Schema.
  • Versionierung: Nehmen Sie in jede Nachricht eine Formatversion auf, und lassen Sie die Empfängerseite „unbekannte Versionen ausdrücklich ablehnen“. Es gibt keine Garantie, dass beide Prozesse immer im Gleichschritt aktualisiert werden, selbst wenn sie mit demselben Installer ausgeliefert werden.
  • Timeouts: Entwerfen Sie unter der Annahme, dass „die Gegenseite antwortet nicht“ definitiv eintreten wird. Setzen Sie für Verbindungsaufbau, Anfrage und Antwort jeweils eine Obergrenze, und lassen Sie keinen Code unbegrenzt warten. Synchrones Warten auf dem UI-Thread kommt überhaupt nicht infrage.
  • Wiederverbindung und Idempotenz: Wiederholen bedeutet, die Möglichkeit zu schaffen, dass dieselbe Anfrage zweimal ankommt. Klassifizieren Sie jede Art von Anfrage danach, ob sie „gefahrlos zweimal ausgeführt werden kann“, und versehen Sie diejenigen, die es nicht können (wie das Einreichen eines Auftrags), mit einer Anfrage-ID, um Duplikate abzulehnen.
  • Behandeln Sie auch die Gegenseite als externe Eingabe: Bei rechnerinterner Kommunikation ist es verlockend, die Validierung zu überspringen, weil „die Gegenseite ist doch unsere eigene App“ — doch sobald eine Berechtigungsgrenze im Spiel ist, ist die IPC-Gegenseite nicht vertrauenswürdige Eingabe, genau wie etwas, das über das Netzwerk ankommt. Ein erhöhter Broker oder ein Dienst darf niemals blind einen Pfad oder Befehl ausführen, der von einem Client mit Standardrechten stammt. Sowohl „wer“ als auch „was“ zu misstrauen — einschließlich der Namensbesetzung von Pipes (Abschnitt 2.2) — ist die Disziplin von IPC, die eine Berechtigungsgrenze überschreitet.
  • Hinterlassen Sie eine Möglichkeit zur Beobachtung: Kommunikationsprotokolle ausgeben zu können (mindestens Nachrichtentyp, Gegenseite und Ergebnis) verändert die Untersuchungszeit bei „keine Verbindung“- oder „keine Antwort“-Problemen um eine Größenordnung.

7. Zusammenfassung

Sie liegen selten falsch, wenn Sie die IPC-Wahl in dieser Reihenfolge durchdenken. Fragen Sie zuerst, ob rein rechnerinterne Kommunikation ausreicht. Ist das der Fall: Named Pipes für Anfrage-Antwort, Shared Memory nur für große Datenmengen mit hoher Frequenz, dateibasierte Kopplung für lose gekoppelte Batch-Vorgänge. Zeichnen sich rechnerübergreifende Kommunikation oder gemischte Sprachen ab, setzen Sie auf lokales TCP oder gRPC, und wählen Sie gRPC, sobald die Pflege eines eigenen Protokolls in großem Maßstab zur Last wird. Halten Sie COM als Werkzeug für 32/64-Brücken und VBA-Interoperabilität in Reserve statt als ersten Kandidaten für neue Arbeit — die Entscheidungstabelle in diesem Artikel ist schlicht dieser Ablauf in Tabellenform.

Und sobald ein Mechanismus feststeht, stellen Sie sicher, dass die gemeinsamen Entwurfsbelange aus Kapitel 6 — Framing, Versionierung, Timeouts, Wiederverbindung und die Validierung der Gegenseite — bereits in die erste Veröffentlichung einfließen. In der Praxis liegt IPC-Ärger überwältigend häufiger daran, dass „der Protokollentwurf übersprungen wurde“, als an einer „falschen Wahl des Mechanismus“. Beim Überdenken einer Prozessaufteilung oder eines Kommunikationsschemas empfehle ich, zunächst zu inventarisieren, welche Prozesse unter wessen Rechten was wie oft austauschen.

Verwandte Artikel

Verwandte Beratungsbereiche

Die Komura Soft LLC übernimmt Design-Reviews für Prozessaufteilungen und Kommunikationsschemata, Kopplungsentwürfe mit bestehenden Beständen einschließlich Brücken zwischen 32-Bit und 64-Bit sowie die Ursachenermittlung bei Kommunikationsstörungen wie „keine Verbindung / keine Antwort“.

  1. Microsoft Learn, How to: Use Named Pipes for Network Interprocess Communication. Ein Implementierungsbeispiel für einen mehrclientfähigen Server und Client mit NamedPipeServerStream / NamedPipeClientStream. 

  2. Microsoft Learn, Inter-process communication with gRPC and Named pipes. Dazu, wie gRPC seit .NET 8 über Kestrels ListenNamedPipe auf Named Pipes läuft, sowie zur Zugriffskontrolle über PipeSecurity.  2 3

  3. Microsoft Learn, MemoryMappedFile Class. Die .NET-API für Memory-Mapped Files, und dazu, wie über CreateNew erzeugter, an keine Datei gebundener gemeinsamer Speicher für IPC-Zwecke gut geeignet ist.  2

  4. Microsoft Learn, Interprocess communications. Eine Liste der von Windows bereitgestellten IPC-Mechanismen (Zwischenablage, COM, WM_COPYDATA, DDE, Dateizuordnung, Mailslots, Pipes, RPC, Windows-Sockets) und Hinweise zur jeweiligen Verwendung.  2 3

  5. Microsoft Learn, NamedPipeServerStream Class. Der serverseitige Stream für Named Pipes und die Konstruktoren zur Angabe von PipeTransmissionMode und PipeSecurity. 

  6. Microsoft Learn, Named Pipe Security and Access Rights. Dazu, wie der Standard-Sicherheitsdeskriptor einer Named Pipe Everyone und anonymen Konten Lesezugriff gewährt, sowie zum Aufbau der Zugriffsrechte.  2

  7. Microsoft Learn, PipeOptions Enum. Dazu, wie CurrentUserOnly Verbindungen nur mit einer vom selben Benutzer erstellten Gegenseite zulässt, und dazu, dass unter Windows zusätzlich zum Benutzerkonto auch die Erhöhungsstufe geprüft wird.  2

  8. Microsoft Learn, Inter-process communication with gRPC. Transporte für die Nutzung von gRPC als IPC (Unix-Domain-Sockets, Named Pipes) sowie Sicherheitsaspekte wie die Überprüfung des Server-Besitzers und der Schutz vor Impersonation.  2

Aktuelle Artikel mit denselben Schlagwörtern führen zu verwandten Themen weiter.

Diese Seiten ordnen den Artikel in einen größeren Leistungs- und Entscheidungskontext ein.

Dieser Artikel ist direkt mit den folgenden Leistungen verbunden.

Häufige Fragen

Fragen, die in Beratungen zu diesem Artikelthema häufig gestellt werden.

Welcher IPC-Mechanismus eignet sich am besten für zwei Windows-Prozesse auf demselben Rechner?
Für Anfrage-Antwort-Kommunikation — einen Befehl senden, ein Ergebnis zurückerhalten — sind Named Pipes der erste Kandidat: Sie sind ein OS-natives Primitiv, benötigen keine Portnummer, sind in die Windows-Zugriffskontrolle integriert und lassen sich aus .NET über System.IO.Pipes unkompliziert nutzen. Der Nachrichtenmodus (PipeTransmissionMode.Message) übernimmt sogar die Rahmenbildung, die man bei rohem TCP selbst entwerfen muss. Reservieren Sie Shared Memory für große Datenmengen mit hoher Frequenz, dateibasierte Kopplung für lose gekoppelte Batch-Vorgänge, und setzen Sie von Anfang an auf lokales TCP oder gRPC, falls die Kommunikation jemals das Netzwerk überschreiten könnte.
Wann sollte ich gRPC statt einer einfachen Named Pipe wählen?
Wechseln Sie, sobald die Pflege eines selbstgebauten Protokolls zur Last wird: grobe Anzeichen sind, dass die Zahl der Nachrichtentypen zehn überschritten hat, dass Streaming-Benachrichtigungen wirklich benötigt werden oder dass die Gegenseite kein .NET ist. Was gRPC bietet, ist ein Entwicklungsrahmen — proto-Dateien liefern Schemadefinitionen und generierten Client-/Server-Code sowie bidirektionales Streaming —, und seit .NET 8 kann Kestrel von ASP.NET Core Named Pipes als gRPC-Transport nutzen, wodurch die portlose, ACL-integrierte Pipe darunter erhalten bleibt. Für ein Werkzeugpaar, das nur zwei oder drei Befehle austauscht, ist gRPC überdimensioniert, und Named Pipes plus JSON sind insgesamt günstiger.
Ist Shared Memory schneller als Named Pipes oder TCP?
Ja — Shared Memory (Memory-Mapped Files) ist der schnellste IPC-Mechanismus innerhalb eines einzelnen Rechners, weil mehrere Prozesse dieselben Bytes direkt lesen und schreiben, ohne Kopieren oder Serialisierung, was es für Daten wie 30 Bildframes pro Sekunde oder Wellenformströme praktisch zur einzig sinnvollen Wahl macht. Es ist aber keine schnellere Pipe: Kein einziges Byte an Synchronisierung wird bereitgestellt, sodass das Vermeiden zerrissener Lesevorgänge, die Erkennung, ob die Gegenseite noch lebt, und die Wiederherstellung nach einem Absturz vollständig selbst entworfen werden müssen. Das praktische Muster besteht darin, nur die Datenebene über Shared Memory zu führen und Steuernachrichten (Start, Stopp, Konfiguration) über einen separaten Kanal wie eine Named Pipe zu leiten.
Was muss ich unabhängig vom gewählten IPC-Mechanismus entwerfen?
Vier gemeinsame Belange verursachen mehr Vorfälle als die Wahl des Mechanismus selbst: Nachrichtengrenzen (Framing, meist ein Längenpräfix, sofern das Betriebssystem die Nachrichten nicht selbst abgrenzt), eine Formatversion in jeder Nachricht, wobei der Empfänger unbekannte Versionen ausdrücklich ablehnt, Timeouts bei Verbindungsaufbau, Anfrage und Antwort, damit nichts unbegrenzt wartet, sowie Wiederverbindung mit Idempotenz — Wiederholungen bedeuten, dass dieselbe Anfrage zweimal ankommen kann, versehen Sie daher alles, was nicht gefahrlos zweimal ausgeführt werden darf, mit einer Anfrage-ID. Behandeln Sie außerdem, wenn eine Berechtigungsgrenze im Spiel ist, die IPC-Gegenseite wie nicht vertrauenswürdige Eingabe, genau wie Netzwerkverkehr, und hinterlassen Sie Kommunikationsprotokolle, damit Untersuchungen zu „keine Verbindung“ nicht bei null beginnen müssen.

Autorenprofil

Profilseite des Artikelautors.

Go Komura

Geschäftsführer von KomuraSoft LLC

Spezialisiert auf Windows-Softwareentwicklung, technische Beratung und Fehleranalyse, insbesondere bei bestehenden Systemen und schwer reproduzierbaren Störungen.

Zurück zum Blog