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("Process response: " + 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("Process response: " + response);

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

        // 结束进程
        process.destroy();
    }
}

代码说明:

  • 我们使用了三条流:读取用户输入(System.in)、向进程写入(process.getOutputStream())以及读取Process response(process.getInputStream())。
  • 每次向进程发送一行后,立即通过 readLine() 读取回应。
  • 如果用户输入 "exit",程序结束循环并销毁进程(process.destroy())。

3. 为什么同时读写很重要?

在真实场景中,进程可能输出大量数据,有时还会连续多次请求输入。如果不及时读取进程输出,它的内部缓冲区可能会被填满,进程就会“卡住”,等待有人来读取消息。同样,如果不往输入写入数据,进程也可能一直等待而停滞。

死锁:当一切都卡住时

Deadlock 是指两个参与方(如线程与进程)相互等待,谁也无法继续的情况。

死锁示例:

  • Java 在等待进程向 stdout 输出。
  • 进程在等待 Java 向它的 stdin 写入。
  • 双方都在等——没人前进。

解决方案:为读写分别使用线程

为了避免死锁,常见做法是使用独立的线程(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("Calculator output: " + result);

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

process.destroy();

在 Windows 上:可以尝试启动 cmd 并向它传递命令,但这会更复杂(语法不同)。演示交互性时通常会使用两大平台都具备的工具,或者自己编写一个小脚本。

5. 问题与坑点

流的缓冲
有时进程不会立刻输出数据,而是先放在内部缓冲里,只有在缓冲满了或者收到换行符("\n")时才“吐出”。这可能导致你迟迟看不到回应,尽管进程早已“准备好”。

建议:

  • 向进程的 stdin 写入时,总是在行末添加 "\n"
  • 如果是你自己的脚本,每次输出后调用 flush()

因错误的流处理导致的死锁
如果不读取 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:因同时等待输入/输出导致的死锁。 如果主线程在等进程输出,而进程在等输入,双方会“卡住”。请为读/写分别使用独立线程。

错误 7:向已结束的进程写入导致 “Stream closed”。 在写入它的 stdin 前请先确认进程仍在运行。出错时会得到 IOException"Stream closed"

评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION