CodeGym /課程 /JAVA 25 SELF /與行程的互動式溝通

與行程的互動式溝通

JAVA 25 SELF
等級 61 , 課堂 3
開放

1. 簡介

所謂互動式行程,是指不只是自己執行,而是等待你與它「對話」的程式。它會接收使用者輸入並給出回應。

典型例子包括解譯器如 PythonbashPowerShell。它們會耐心等待指令、執行並顯示結果。還有其他類型:像是互動式工具(計算器)、主控台資料庫(psqlsqlite3),甚至像 vinano 這類文字編輯器。有時一般腳本在執行過程中若向你詢問參數或答案,也會變成互動式。

從 Java 啟動這類行程並不輕鬆。這遠不只是「送出指令並取得結果」這麼簡單。你需要建立真正的對話:及時送出資料並讀取回應,就像真人對話一樣。

想像你在即時通訊軟體和朋友聊天:你送出訊息、等待回覆,然後再回覆。若正確設定串流,Java 與外部行程的互動大致相同。

如何建立雙向通訊

每個行程都有三個「通道」:輸入(stdin)、輸出(stdout)與錯誤(stderr)。透過第一個你能傳資料給行程,透過第二個取得結果,而第三個則用於錯誤訊息。

關鍵是不要讓行程僵在等待。如果你只讀取標準輸出而忽略錯誤輸出,行程可能會卡住,彷彿在等你注意它的抱怨。反過來說,若你一直寫入輸入端卻不讀取回應,行程也可能因為累積的資料無處可去而停滯。

因此,在處理互動式行程時,必須維持雙向交換——就像正常的對話,雙方都在聽也在回應,而不是對著空氣說話。

互動式通訊示意圖

+---------------------+
|   Java program      |
+---------------------+
   |           ^
   v           |
stdin      stdout/stderr
   |           ^
+---------------------+
| External process    |
+---------------------+
  • Java 會寫入行程的輸入串流(行程的 stdin)。
  • Java 會讀取行程的 stdoutstderr
  • 這些動作可以同時發生!

2. 實作:與外部行程進行互動

來看實作範例:我們將啟動外部行程(例如 Python 直譯器或簡單的 echo 腳本),向它傳送字串並讀取回應。

範例 1:啟動等待輸入的 Python 腳本

先建立一個簡單的 Python 腳本(命名為 echo_bot.py):

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

此腳本會等待輸入,並對每一行回應 "Echo: ..."。如果輸入 "exit" —— 就會結束。

如何在 Java 中啟動並與此腳本「對話」?

1. 啟動行程
ProcessBuilder builder = new ProcessBuilder("python", "echo_bot.py");
Process process = builder.start();
2. 準備通訊用的串流
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(process.getOutputStream()));
BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
3. 實作對話
// 將字串送給行程
writer.write("Hello, process!\n");
writer.flush();

// 讀取回應
String response = reader.readLine();
System.out.println("行程回應: " + response);
4. 重複直到不想再做為止

我們把它包成一個簡單的迴圈,讓使用者可在主控台輸入,Java 將其送往行程並顯示回應。

完整範例:與外部行程的 Java 聊天

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();

        // 與行程通訊用的串流
        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("輸入要發送給行程的字串 (輸入 exit 以結束):");

        while (true) {
            // 讀取使用者輸入的一行
            String line = userInput.readLine();
            if (line == null) break;

            // 將它傳送給行程
            toProcess.write(line + "\n");
            toProcess.flush();

            // 讀取行程的回應
            String response = fromProcess.readLine();
            System.out.println("行程回應: " + response);

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

        // 結束行程
        process.destroy();
    }
}

程式碼說明:

  • 我們使用三個串流:從使用者讀取(System.in)、寫給行程(process.getOutputStream())以及讀取回應(process.getInputStream())。
  • 每次送出一行給行程後,立刻透過 readLine() 讀取回應。
  • 若使用者輸入 "exit",程式會結束迴圈並銷毀行程(process.destroy())。

3. 為什麼同時讀寫很重要?

在實務中,行程可能輸出大量資料,有時還會連續要求輸入。如果沒有及時讀取行程輸出,它的內部緩衝區可能會被塞滿,行程便會「卡住」,等待有人讀走訊息。反之亦然:若你寫入而不讀取回應,行程可能因為累積的資料無處可去而停滯。

Deadlock:當一切都卡住

Deadlock 指的是兩方(或執行緒與行程)互相等待,誰也無法繼續工作的情況。

Deadlock 範例:

  • Java 等待行程在 stdout 輸出。
  • 行程等待 Java 寫入它的 stdin
  • 雙方都在等待——沒有人繼續工作。

解法:將讀寫分到不同執行緒

為了避免 deadlock,常常會使用獨立的執行緒(Thread)同時讀取行程的 stdoutstderr。例如建立兩個執行緒:一個讀 stdout,另一個讀 stderr,而主執行緒則負責寫入 stdin

最小示例:為 stderr 啟用獨立執行緒

// 在獨立執行緒中讀取 stderr
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. 實作:與計算器互動

不一定隨處都有 Python。我們可以試著啟動幾乎到處都有的指令列計算器。例如在 Linux/Mac 上是 bc(basic calculator),在 Windows 上則是 cmdpowershell

範例:與 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("計算器回應: " + result);

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

process.destroy();

在 Windows 上:可以嘗試啟動 cmd 並傳遞指令,但這會更複雜(語法不同)。通常為了示範互動性,會選擇兩邊都有的工具,或自己撰寫小型腳本。

5. 問題與陷阱

串流的緩衝
有時行程不會立即輸出,而會把資料累積在內部緩衝區,只有當緩衝滿了或者收到換行字元("\n")時才輸出。這可能導致你看不到回應,即便行程其實已經「準備好了」。

建議:

  • 寫入行程的 stdin 時,務必以 "\n" 結尾。
  • 如果你撰寫自己的腳本,請在每次輸出後呼叫 flush()

串流處理不當導致的 deadlock
若不讀取 stderr,而行程又大量寫入其中,行程可能會卡住,等待有人讀取錯誤輸出。

建議:
務必同時讀取行程的 stdoutstderr,最好分別置於不同的執行緒中。

行程已結束——你卻還在寫入
如果行程已經結束,而你仍持續寫入它的輸入端,會得到 IOException"Stream closed"

7. 最佳實務:互動式通訊

使用 ExecutorService 進行並行讀取

為了避免手動打造讀取輸出與錯誤的執行緒,使用 ExecutorService 會更方便(例如建立兩個執行緒的池):

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();
    }
});

正確關閉串流

在結束與行程的互動後,請務必關閉所有串流(stdinstdoutstderr),以避免資源外洩。

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

8. 綜合範例:與外部行程的互動式通訊

讓我們把一切整合。下面的範例就像使用者與外部行程(例如 Python 腳本或計算器)之間的互動式「聊天」。

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

public class InteractiveProcessUniversal {
    public static void main(String[] args) throws IOException {
        // 請替換為你的指令——例如 "python echo_bot.py" 或 "bc"
        ProcessBuilder builder = new ProcessBuilder("python", "echo_bot.py");
        Process process = builder.start();

        ExecutorService executor = Executors.newFixedThreadPool(2);

        // 讀取行程的 stdout
        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
        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();
            }
        });

        // 主執行緒:寫入行程的 stdin
        try (BufferedWriter toProcess = new BufferedWriter(new OutputStreamWriter(process.getOutputStream()));
             BufferedReader userInput = new BufferedReader(new InputStreamReader(System.in))) {

            System.out.println("輸入要傳給行程的字串 (輸入 exit 以結束):");
            String line;
            while ((line = userInput.readLine()) != null) {
                toProcess.write(line + "\n");
                toProcess.flush();
                if ("exit".equals(line)) break;
            }
        }

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

9. 互動式處理行程的常見錯誤

錯誤 1:沒有讀取行程的 stderr。 若行程將大量錯誤寫到 stderr,但你沒有讀取,它可能會卡住。即使你確信不會有錯誤——也請讀取 stderr

錯誤 2:沒有關閉串流。 若不關閉行程的串流,可能造成資源外洩,有時甚至會阻礙行程結束。

錯誤 3:不同平台的指令差異。 指令與語法在 Windows 與類 Unix 系統間有所差異。請檢查作業系統並選擇適合的指令。

錯誤 4:程式「卡住」——忘了對輸出呼叫 flush。 若忘了在寫入串流後呼叫 flush(),資料可能卡在緩衝區而無法送到行程。

錯誤 5:外部行程的輸出緩衝。 有時行程在累積到足夠字元或收到 "\n" 前不會輸出。若你撰寫自己的腳本——請使用 flush()

錯誤 6:同時等待輸入/輸出造成的 deadlock。 若主執行緒在等輸出,而行程在等輸入,雙方可能「卡住」。請將讀寫放到不同的執行緒中。

錯誤 7:對已結束的行程寫入而出現「Stream closed」。 在寫入它的 stdin 前,先確認行程仍在。否則你會得到 IOException"Stream closed"

留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION