CodeGym /Kurse /JAVA 25 SELF /Interaktive Kommunikation mit Prozessen

Interaktive Kommunikation mit Prozessen

JAVA 25 SELF
Level 61 , Lektion 3
Verfügbar

1. Einführung

Ein interaktiver Prozess ist ein Programm, das nicht nur etwas selbst tut, sondern darauf wartet, dass Sie mit ihm „sprechen“. Er nimmt Eingaben vom Benutzer entgegen und antwortet darauf.

Klassische Beispiele sind Interpreter wie Python, bash oder PowerShell. Sie warten geduldig auf Befehle, führen sie aus und zeigen das Ergebnis an. Es gibt noch weitere: interaktive Utilities wie ein Taschenrechner, Konsolendatenbanken (psql, sqlite3) oder sogar Texteditoren wie vi und nano. Mitunter kann auch ein gewöhnliches Skript interaktiv werden, wenn es während der Arbeit Parameter oder Antworten von Ihnen anfordert.

Solche Prozesse aus Java zu starten ist kein Spaziergang. Es reicht nicht, einfach „einen Befehl abzusetzen und ein Ergebnis zu erhalten“. Man muss einen echten Dialog einrichten: Daten rechtzeitig senden und Antworten auslesen – wie in einem echten Gespräch.

Stellen Sie sich vor, Sie schreiben einem Freund in einem Messenger. Sie senden eine Nachricht, warten auf die Antwort und schreiben dann erneut. So in etwa sieht die Interaktion von Java mit einem externen Prozess aus, wenn Sie die Arbeit mit den Streams richtig eingerichtet haben.

Aufbau eines bidirektionalen Austauschs

Jeder Prozess hat drei „Kommunikationskanäle“: Eingabe (stdin), Ausgabe (stdout) und Fehlerausgabe (stderr). Über den ersten können Sie dem Prozess etwas übergeben, über den zweiten das Ergebnis erhalten, und der dritte ist für Fehlermeldungen gedacht.

Wichtig ist, den Prozess nicht „einfrieren“ zu lassen. Wenn Sie nur die normale Ausgabe lesen und dabei den Fehlerstrom ignorieren, kann der Prozess hängen bleiben, als würde er darauf warten, dass Sie seine Beschwerden beachten. Genauso umgekehrt: Wenn Sie etwas an die Eingabe schicken, aber keine Antworten lesen, kann der Prozess stecken bleiben, weil er nicht weiß, wohin mit den gesammelten Daten.

Daher ist es bei der Arbeit mit interaktiven Prozessen wichtig, den bidirektionalen Austausch aufrechtzuerhalten – wie in einem normalen Gespräch, in dem beide Seiten zuhören und antworten und nicht ins Leere sprechen.

Schema des interaktiven Austauschs

+---------------------+
|   Java-Programm     |
+---------------------+
   |           ^
   v           |
stdin      stdout/stderr
   |           ^
+---------------------+
| Externer Prozess    |
+---------------------+
  • Java schreibt in den Eingabestrom des Prozesses (stdin des Prozesses).
  • Java liest aus stdout und stderr des Prozesses.
  • All das kann gleichzeitig stattfinden!

2. Praxis: interaktiver Austausch mit einem externen Prozess

Sehen wir uns ein praktisches Beispiel an: Wir starten einen externen Prozess (z. B. den Interpreter Python oder ein einfaches Echo-Skript), senden ihm Zeilen und lesen die Antworten.

Beispiel 1: Start eines Python‑Skripts, das auf Eingabe wartet

Erstellen wir zunächst ein einfaches Python-Skript (nennen wir es echo_bot.py):

# echo_bot.py
while True:
    try:
        line = input()
        if line == "exit":
            print("Bye!")
            break
        print("Echo:", line)
    except EOFError:
        break

Dieses Skript wartet auf Eingaben und antwortet auf jede Zeile mit "Echo: ...". Wenn man "exit" eingibt, beendet es die Arbeit.

Wie starten und „mit diesem Skript reden“ wir aus Java?

1. Prozess starten
ProcessBuilder builder = new ProcessBuilder("python", "echo_bot.py");
Process process = builder.start();
2. Streams für die Kommunikation vorbereiten
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(process.getOutputStream()));
BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
3. Dialog implementieren
// Senden einer Zeile an den Prozess
writer.write("Hello, process!\n");
writer.flush();

// Antwort lesen
String response = reader.readLine();
System.out.println("Antwort des Prozesses: " + response);
4. Wiederholen, bis es reicht

Verpacken wir das in eine einfache Schleife, sodass der Benutzer in die Konsole schreiben kann, Java es an den Prozess sendet und die Antwort ausgibt.

Vollständiges Beispiel: Java‑Chat mit einem externen Prozess

import java.io.*;

public class InteractiveProcessDemo {
    public static void main(String[] args) throws IOException {
        ProcessBuilder builder = new ProcessBuilder("python", "echo_bot.py");
        Process process = builder.start();

        // Streams für die Kommunikation mit dem Prozess
        BufferedWriter toProcess = new BufferedWriter(new OutputStreamWriter(process.getOutputStream()));
        BufferedReader fromProcess = new BufferedReader(new InputStreamReader(process.getInputStream()));
        BufferedReader userInput = new BufferedReader(new InputStreamReader(System.in));

        System.out.println("Geben Sie Zeilen ein, die an den Prozess gesendet werden sollen (exit zum Beenden):");

        while (true) {
            // Zeile vom Benutzer lesen
            String line = userInput.readLine();
            if (line == null) break;

            // An den Prozess senden
            toProcess.write(line + "\n");
            toProcess.flush();

            // Antwort des Prozesses lesen
            String response = fromProcess.readLine();
            System.out.println("Antwort des Prozesses: " + response);

            if ("exit".equals(line)) break;
        }

        // Prozess beenden
        process.destroy();
    }
}

Erläuterungen zum Code:

  • Wir verwenden drei Streams: zum Lesen vom Benutzer (System.in), zum Schreiben an den Prozess (process.getOutputStream()) und zum Lesen der Antwort (process.getInputStream()).
  • Nach jeder gesendeten Zeile an den Prozess lesen wir sofort die Antwort über readLine().
  • Gibt der Benutzer "exit" ein, beendet das Programm die Schleife und zerstört den Prozess (process.destroy()).

3. Warum sind gleichzeitiges Lesen und Schreiben wichtig?

In realen Aufgaben kann der Prozess viele Daten ausgeben und gelegentlich auch mehrfach nacheinander Eingaben anfordern. Wenn die Ausgabe des Prozesses nicht rechtzeitig gelesen wird, kann sein interner Puffer überlaufen, und der Prozess „hängt“, bis jemand seine Meldungen ausliest. Genauso kann der Prozess warten und „einfrieren“, wenn Eingaben ausbleiben.

Deadlock: wenn alles hängt

Deadlock ist eine Situation, in der zwei Prozesse (oder ein Thread und ein Prozess) aufeinander warten und niemand fortfahren kann.

Beispiel für einen Deadlock:

  • Java wartet darauf, dass der Prozess etwas in stdout schreibt.
  • Der Prozess wartet darauf, dass Java etwas in sein stdin schreibt.
  • Beide warten – niemand arbeitet.

Lösung: separate Threads für Lesen und Schreiben

Um Deadlocks zu vermeiden, verwendet man oft separate Threads (Thread) für das gleichzeitige Lesen von stdout und stderr des Prozesses. Man kann zum Beispiel zwei Threads erstellen: Einer liest stdout, der andere stderr, während der Hauptthread in stdin schreibt.

Minimales Beispiel mit einem separaten Thread für stderr

// stderr in einem separaten Thread lesen
Thread errorThread = new Thread(() -> {
    try (BufferedReader errorReader = new BufferedReader(
            new InputStreamReader(process.getErrorStream()))) {
        String line;
        while ((line = errorReader.readLine()) != null) {
            System.err.println("stderr: " + line);
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
});
errorThread.start();

4. Praxis: interaktiver Austausch mit einem Rechner

Nicht immer steht Python zur Verfügung. Versuchen wir, einen Standardbefehl zu starten, den es fast überall gibt – zum Beispiel einen Rechner in der Kommandozeile. Unter Linux/Mac kann das bc (basic calculator) sein, unter Windows cmd oder powershell.

Beispiel: interaktive Kommunikation mit bc (Linux/Mac)

ProcessBuilder builder = new ProcessBuilder("bc");
Process process = builder.start();

BufferedWriter toProcess = new BufferedWriter(new OutputStreamWriter(process.getOutputStream()));
BufferedReader fromProcess = new BufferedReader(new InputStreamReader(process.getInputStream()));

toProcess.write("2 + 2\n");
toProcess.flush();

String result = fromProcess.readLine();
System.out.println("Antwort des Rechners: " + result);

toProcess.write("quit\n");
toProcess.flush();

process.destroy();

Unter Windows: Man kann versuchen, cmd zu starten und ihr Befehle zu übergeben, doch das ist komplizierter (es ist eine andere Syntax nötig). Für eine Demonstration der Interaktivität nutzt man üblicherweise Tools, die auf beiden Plattformen vorhanden sind, oder schreibt eigene Mini-Skripte.

5. Probleme und Fallstricke

Pufferung der Streams
Manchmal gibt der Prozess Daten nicht sofort aus, sondern sammelt sie in einem internen Puffer und „liefert“ erst, wenn der Puffer gefüllt ist oder wenn ein Zeilenumbruch ("\n") eintrifft. Dadurch kann es passieren, dass Sie lange keine Antwort sehen, obwohl der Prozess sie bereits „vorbereitet“ hat.

Tipp:

  • Beenden Sie beim Schreiben in das stdin des Prozesses Zeilen immer mit dem Zeichen "\n".
  • Wenn Sie eigene Skripte schreiben, verwenden Sie nach jeder Ausgabe flush().

Deadlock bei falscher Organisation der Streams
Wenn stderr nicht gelesen wird, der Prozess dort aber viele Daten schreibt, kann er hängen bleiben, bis jemand die Fehler liest.

Tipp:
Lesen Sie immer sowohl stdout als auch stderr des Prozesses, vorzugsweise in separaten Threads.

Prozess beendet – aber Sie schreiben noch
Wenn der Prozess bereits beendet ist, Sie aber weiterhin in seine Eingabe schreiben, erhalten Sie eine IOException: "Stream closed".

7. Best Practices: interaktive Kommunikation

Verwendung von ExecutorService für paralleles Lesen

Damit Sie nicht manuell Threads für das Lesen von Ausgabe und Fehlern bauen müssen, ist es praktisch, ExecutorService zu verwenden (z. B. einen Pool aus zwei Threads):

import java.util.concurrent.*;

ExecutorService executor = Executors.newFixedThreadPool(2);

executor.submit(() -> {
    try (BufferedReader out = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
        String line;
        while ((line = out.readLine()) != null) {
            System.out.println("stdout: " + line);
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
});

executor.submit(() -> {
    try (BufferedReader err = new BufferedReader(new InputStreamReader(process.getErrorStream()))) {
        String line;
        while ((line = err.readLine()) != null) {
            System.err.println("stderr: " + line);
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
});

Korrektes Schließen der Streams

Nach dem Abschluss der Arbeit mit dem Prozess schließen Sie unbedingt alle Streams (stdin, stdout, stderr), um Ressourcenlecks zu vermeiden.

toProcess.close();
fromProcess.close();
process.destroy();
executor.shutdown();

8. Abschließendes Beispiel: interaktiver Austausch mit einem externen Prozess

Fassen wir alles zusammen. Das folgende Beispiel funktioniert wie ein interaktiver „Chat“ zwischen dem Benutzer und einem externen Prozess (z. B. einem Python‑Skript oder einem Rechner).

import java.io.*;
import java.util.concurrent.*;

public class InteractiveProcessUniversal {
    public static void main(String[] args) throws IOException {
        // Ersetzen Sie dies durch Ihren Befehl – z. B. "python echo_bot.py" oder "bc"
        ProcessBuilder builder = new ProcessBuilder("python", "echo_bot.py");
        Process process = builder.start();

        ExecutorService executor = Executors.newFixedThreadPool(2);

        // stdout des Prozesses lesen
        executor.submit(() -> {
            try (BufferedReader out = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
                String line;
                while ((line = out.readLine()) != null) {
                    System.out.println("[process] " + line);
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        });

        // stderr des Prozesses lesen
        executor.submit(() -> {
            try (BufferedReader err = new BufferedReader(new InputStreamReader(process.getErrorStream()))) {
                String line;
                while ((line = err.readLine()) != null) {
                    System.err.println("[process-err] " + line);
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        });

        // Hauptthread: schreibt in stdin des Prozesses
        try (BufferedWriter toProcess = new BufferedWriter(new OutputStreamWriter(process.getOutputStream()));
             BufferedReader userInput = new BufferedReader(new InputStreamReader(System.in))) {

            System.out.println("Geben Sie Zeilen für den Prozess ein (exit zum Beenden):");
            String line;
            while ((line = userInput.readLine()) != null) {
                toProcess.write(line + "\n");
                toProcess.flush();
                if ("exit".equals(line)) break;
            }
        }

        process.destroy();
        executor.shutdown();
    }
}

9. Typische Fehler bei der interaktiven Arbeit mit Prozessen

Fehler Nr. 1: stderr des Prozesses wird nicht gelesen. Wenn der Prozess viele Fehler schreibt, stderr aber nicht gelesen wird, kann der Prozess hängen bleiben. Auch wenn Sie sicher sind, dass keine Fehler auftreten – lesen Sie stderr!

Fehler Nr. 2: Streams werden nicht geschlossen. Wenn die Streams des Prozesses nicht geschlossen werden, kann es zu Ressourcenlecks kommen und mitunter auch zur Blockierung beim Beenden des Prozesses.

Fehler Nr. 3: Plattformunterschiede bei Befehlen. Befehle und ihre Syntax unterscheiden sich zwischen Windows und Unix‑ähnlichen Systemen. Prüfen Sie das Betriebssystem und wählen Sie den passenden Befehl.

Fehler Nr. 4: Programm „hängt“ – Ausgabe wird nicht geflusht. Wenn flush() nach dem Schreiben in den Stream vergessen wird, können Daten im Puffer „stecken bleiben“ und den Prozess nicht erreichen.

Fehler Nr. 5: Pufferung der Ausgabe des externen Prozesses. Manchmal gibt der Prozess Daten erst aus, wenn genügend Zeichen zusammengekommen sind oder ein "\n" eingeht. Wenn Sie eigene Skripte schreiben – verwenden Sie flush().

Fehler Nr. 6: Deadlock durch gleichzeitiges Warten auf Ein-/Ausgabe. Wenn der Hauptthread auf die Ausgabe des Prozesses wartet, der Prozess aber auf Eingaben, können beide „hängen“. Verwenden Sie separate Threads für Lesen/Schreiben.

Fehler Nr. 7: Ausnahme „Stream closed“ beim Schreiben in einen beendeten Prozess. Prüfen Sie, ob der Prozess noch lebt, bevor Sie in sein stdin schreiben. Im Fehlerfall erhalten Sie eine IOException: "Stream closed".

1
Aufgabe
JAVA 25 SELF, Level 61, Lektion 3
Gesperrt
Geheime Datenübertragung 🔑
Geheime Datenübertragung 🔑
1
Aufgabe
JAVA 25 SELF, Level 61, Lektion 3
Gesperrt
Dialog mit dem digitalen Zwilling 🗣️
Dialog mit dem digitalen Zwilling 🗣️
Kommentare
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION