Named Pipes (mit Java und JNA)

Leider gibt es im Internet zu wenige bis keine Beispiele, wie man Named Pipes unter Java verwendet. Das möchte ich mit diesem Beitrag ändern. Aber fangen wir erstmal an, was Named Pipes (bzw. Inter Process Communication – IPC) überhaupt ist. Der folgende Beitrag bezieht sich auf die Verwendung von Java unter Windows

Was sind Named Pipes und wie werden sie verwendet?

Named Pipes sind eine Art von IPC-Mechanismus, der es verschiedenen Prozessen ermöglicht, miteinander zu kommunizieren, indem sie über eine benannte Datei im Dateisystem Daten austauschen. Die Kommunikation über Named Pipes unter Unix-basierten Betriebssystemen ist unidirektional, was bedeutet, dass die Pipe entweder zum Senden oder zum Empfangen von Daten geöffnet werden kann. Wenn beide Prozesse Daten senden und empfangen wollen, müssen zwei Pipes geöffnet werden, eine für jede Richtung. In Windows unterstützen Named Pipes auch die bidirektionale Kommunikation, sodass nur ein Pipe benötigt wird.

Named Pipes sind in Unix-basierten und Windows-Betriebssystemen verfügbar und können für verschiedene Zwecke eingesetzt werden, z.B. für die Kommunikation zwischen verschiedenen Prozessen oder für die Kommunikation zwischen einem Prozess und einem Dienst.

Java als Named Pipe Server (Windows)

Im folgenden Beispiel eine beispielhafte Implementation, bei dem Java den Named Pipe „MeinePipe“ bereitstellt. Leider gibt es unter Java keine direkte Möglichkeit, eine Named Pipe mit den Boardkomponenten von Java zu erstellen. Jedoch gibt es die Java Native APIs (kurz JNA), welche es ermöglichen, von Java aus auf nativen Code zuzugreifen und Methoden in DLLs aufzurufen.

Hierzu muss zunächst einige Maven Dependencies zur pom.xml hinzugefügt werden:

<dependencies>
...
        <dependency>
            <groupId>net.java.dev.jna</groupId>
            <artifactId>jna</artifactId>
            <version>5.10.0</version>
        </dependency>

        <dependency>
            <groupId>net.java.dev.jna</groupId>
            <artifactId>jna-platform</artifactId>
            <version>5.10.0</version>
        </dependency>
...
</dependencies>

Danach steht JNA im Code zur Verfügung. (Von JNA ist mittlerweile eine neuere Version verfügbar, diese habe ich jedoch für diesen Beitrag nicht getestet)

Hier ein kleines Beispiel:

import com.sun.jna.platform.win32.Kernel32;
import com.sun.jna.platform.win32.WinBase;
import com.sun.jna.platform.win32.WinNT;

import java.io.File;

public class Main {
    public static void main(String[] args) {
        String pipeName = "MeinePipe";

        if (isPipeExisting(pipeName)) {
            System.out.println("Pipe already exist. I'm not creating a new pipe");
            System.exit(1);
        }
        System.out.println("Pipe does not exist yet, so creating one");

        //Windows handles pipe names natively as UNC Paths
        String pipeNameUNC = "\\\\.\\pipe\\" + pipeName;
        int bufferSize = 1024;

        while (true) {
            System.out.println("Creating Named Pipe handle");
            WinNT.HANDLE pipeHandle = Kernel32.INSTANCE.CreateNamedPipe(
                    pipeNameUNC,                            // Named Pipe Name
                    WinBase.PIPE_ACCESS_DUPLEX,          // Read/Write Access
                    WinBase.PIPE_TYPE_MESSAGE | WinBase.PIPE_WAIT, // Message Type Pipe
                    1,   // Number of Instances
                    bufferSize,                         // Output Buffer Size
                    bufferSize,                         // Input Buffer Size
                    0,                                  // Default Time-Out (ms)
                    null);                              // Security Attributes

            if (pipeHandle == WinBase.INVALID_HANDLE_VALUE) {
                System.err.println("Error creating pipe: " + Kernel32.INSTANCE.GetLastError());
                return;
            }

            System.out.println("Pipe created successfully, waiting for any client to connect");

            //Wait for client connection
            boolean connected = Kernel32.INSTANCE.ConnectNamedPipe(pipeHandle, null);
            if (!connected) {
                System.err.println("Error connecting to pipe: " + Kernel32.INSTANCE.GetLastError());
                Kernel32.INSTANCE.CloseHandle(pipeHandle);
                continue;
            }

            System.out.println("Connected to pipe");

            // Read from pipe
            byte[] buffer = new byte[bufferSize];
            int bytesToRead = buffer.length;
            boolean success = Kernel32.INSTANCE.ReadFile(pipeHandle, buffer, bytesToRead, null, null);
            if (!success) {
                System.err.println("Error reading from pipe: " + Kernel32.INSTANCE.GetLastError());
                Kernel32.INSTANCE.CloseHandle(pipeHandle);
                continue;
            }

            // Processing input
            String receivedFromPipe = new String(buffer).trim();
            String response = "You sent " + receivedFromPipe;

            System.out.println("Recieved from Pipe: " + receivedFromPipe);
            System.out.println("Answering with: " + response);

            //Send response (write response to pipe)
            byte[] responseBytes = response.getBytes();
            int bytesToWrite = responseBytes.length;
            success = Kernel32.INSTANCE.WriteFile(pipeHandle, responseBytes, bytesToWrite, null, null);
            if (!success) {
                System.err.println("Error writing to pipe: " + Kernel32.INSTANCE.GetLastError());
            } else {
                System.out.println("Response sent to pipe");
            }


            System.out.println("Closing pipe handle");
            Kernel32.INSTANCE.CloseHandle(pipeHandle);

        }


    }


    public static boolean isPipeExisting(String pipeName) {
        return new File("\\\\.\\pipe\\" + pipeName).exists();
    }
}

Mit der Methode isPipeExisting(„MeinePipe“) wird geprüft, ob die Pipe bereits existiert, falls nicht, dann wird eine neue Pipe erzeugt.

Alle Pipes können in der PowerShell mit dem Befehl [System.IO.Directory]::GetFiles("\\.\\pipe\\") angezeigt werden. Dort wird dann auch \\.\\pipe\\MeinePipe (bei mir relativ weit unten) angezeigt.

Wenn sich ein Client verbindet und z.B. „Test“ sendet, bekommt er die Antwort „You sent Test“ zurück. Danach wird das Handle für die Pipe geschossen. Damit ist die Pipe kurz „weg“, wird aber durch die while-Schleife wieder erzeugt und der Vorgang beginnt erneut.

Der Server erzeugt die folgende Ausgabe:

Pipe does not exist yet, so creating one
Creating Named Pipe handle
Pipe created successfully, waiting for any client to connect

Named Pipe Client in Java

Ein zweites Java Programm verbindet sich im folgenden Beispiel mit der Pipe „MeinePipe“ und empfängt anschließend eine Antwort:

import com.sun.jna.platform.win32.Kernel32;
import com.sun.jna.platform.win32.WinBase;
import com.sun.jna.platform.win32.WinNT;
import com.sun.jna.ptr.IntByReference;

import java.io.IOException;

public class Client {
    public static void main(String[] args) throws IOException {
        String dataToSend = "Hallo Welt";
        String pipeName = "MeinePipe";

        System.out.println("Sending " + dataToSend + " to named pipe " + pipeName);
        String pipeResponse = sendToPipe(dataToSend,pipeName);

        System.out.println("Received: " + pipeResponse);
    }

    public static String sendToPipe(String dataToSend, String pipeName) throws IOException {

        // Open Pipe
        String pipeNameUNC = "\\\\.\\pipe\\" + pipeName;
        WinNT.HANDLE pipeHandle = Kernel32.INSTANCE.CreateFile(
                pipeNameUNC,
                WinNT.GENERIC_READ | WinNT.GENERIC_WRITE,
                0,
                null,
                WinNT.OPEN_EXISTING,
                WinNT.FILE_ATTRIBUTE_NORMAL,
                null);

        if (WinBase.INVALID_HANDLE_VALUE.equals(pipeHandle)) {
            throw new IOException("Error opening pipe");
        }


        // Write our data to Pipe
        String data = dataToSend;
        byte[] dataBytes = data.getBytes();
        int bytesToWrite = dataBytes.length;
        boolean success = Kernel32.INSTANCE.WriteFile(pipeHandle, dataBytes, bytesToWrite, null, null);

        if (!success) {
            throw new IOException("Error writing to pipe");
        }

        // Read response from pipe
        byte[] buffer = new byte[1024];
        IntByReference bytesRead = new IntByReference();
        success = Kernel32.INSTANCE.ReadFile(
                pipeHandle,
                buffer,
                buffer.length,
                bytesRead,
                null);

        if (!success) {
            throw new IOException("Error reading from pipe");
        }

        String response = new String(buffer, 0, bytesRead.getValue());

        // Close pipe handle
        Kernel32.INSTANCE.CloseHandle(pipeHandle);

        return response;
    }
}

Die Funktion sendToPipe("Hallo Welt","MeinePipe") gibt an, dass „Hallo Welt“ an die Pipe „MeinePipe“ gesendet wird. Dann wird die Antwort der Pipe zurückgegeben.

Das Programm erzeugt die folgende Ausgabe:

Sending Hallo Welt to named pipe MeinePipe
Received: You sent Hallo Welt

Und im Server vom vorherigen Beispiel wird nun die folgende Ausgabe erzeugt:

Pipe does not exist yet, so creating one
Creating Named Pipe handle
Pipe created successfully, waiting for any client to connect
Connected to pipe
Recieved from Pipe: Hallo Welt
Answering with: You sent Hallo Welt
Response sent to pipe
Closing pipe handle
Creating Named Pipe handle
Pipe created successfully, waiting for any client to connect

Client mit C#

Natürlich können auch Programme unterschiedlichster Programmiersprachen miteinander über Named Pipes kommunizieren. Unter C# geht das meiner Meinung nach noch einfacher, da C# von Haus aus Klassen für Named Pipes mitbringt. Das gilt natürlich auch für Visual Basic .NET, da sie beide auf der .NET CLR aufbauen.

Der folgende Code ist die Portierung unseres Java Codes auf C#, mit benutzung der Hauseigenen Named Pipe Klasse.

using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Pipes;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace NamedPipeClientExample
{
    class Program
    {
        static void Main(string[] args)
        {

            string pipeName = "MeinePipe";
            string dataToSend = "Hallo Welt";

            Console.WriteLine("Sending " + dataToSend + " to named pipe " + pipeName);


            //Named pipe client
            var client = new NamedPipeClientStream(pipeName);

            //Connect to Named Pipe
            client.Connect();
            StreamReader reader = new StreamReader(client);
            StreamWriter writer = new StreamWriter(client);


            if (String.IsNullOrEmpty(dataToSend)) return;
            writer.WriteLine(dataToSend);
            writer.Flush(); //Send data to pipe


            string pipeResponse = reader.ReadLine();
            Console.WriteLine("Received: " + pipeResponse);

        }
    }
}

Ich hoffe, ich kann mit diesem Beitrag jemandem helfen, der gerade das gleiche Problem hat.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert