CodeGym /Các khóa học /JAVA 25 SELF /Giao tiếp tương tác với các tiến trình

Giao tiếp tương tác với các tiến trình

JAVA 25 SELF
Mức độ, Bài học
Có sẵn

1. Giới thiệu

Tiến trình tương tác là một chương trình không chỉ tự làm gì đó, mà còn chờ bạn “trò chuyện” với nó. Nó nhận đầu vào từ người dùng và trả lời lại.

Ví dụ điển hình là các trình thông dịch như Python, bash hoặc PowerShell. Chúng kiên nhẫn chờ lệnh, thực thi và hiển thị kết quả. Còn có những công cụ khác: tiện ích tương tác như máy tính dòng lệnh, hệ quản trị cơ sở dữ liệu ở chế độ console (psql, sqlite3) hay thậm chí là các trình soạn thảo văn bản như vi và nano. Đôi khi, một script bình thường cũng có thể trở nên tương tác nếu nó yêu cầu bạn cung cấp tham số hay câu trả lời trong quá trình chạy.

Khởi chạy những tiến trình như vậy từ Java không phải là chuyện dạo chơi. Không chỉ “ra lệnh và nhận kết quả” là xong. Bạn cần thiết lập một cuộc đối thoại thực sự: gửi dữ liệu đúng lúc và đọc phản hồi kịp thời, như một cuộc trò chuyện trực tiếp.

Hãy hình dung bạn nhắn tin cho bạn bè. Bạn gửi một tin nhắn, chờ trả lời, rồi lại tiếp tục. Tương tác giữa Java và tiến trình bên ngoài cũng tương tự như vậy, nếu bạn cấu hình làm việc với các luồng đúng cách.

Tổ chức trao đổi hai chiều

Mỗi tiến trình có ba “kênh liên lạc”: đầu vào (stdin), đầu ra (stdout) và lỗi (stderr). Qua kênh thứ nhất bạn có thể gửi dữ liệu cho tiến trình, kênh thứ hai — nhận kết quả, còn kênh thứ ba dùng cho thông báo lỗi.

Điều quan trọng là đừng để tiến trình “đứng đơ” vì chờ đợi. Nếu bạn chỉ đọc đầu ra thông thường mà bỏ qua luồng lỗi, tiến trình có thể treo như thể đang chờ bạn để ý tới lời “phàn nàn” của nó. Tương tự theo chiều ngược lại: nếu bạn gửi dữ liệu vào nhưng không đọc phản hồi, tiến trình có thể kẹt, không biết đẩy dữ liệu tích lũy đi đâu.

Vì vậy, khi làm việc với các tiến trình tương tác, cần duy trì trao đổi hai chiều — như trong một cuộc trò chuyện bình thường, nơi cả hai bên đều lắng nghe và trả lời, chứ không phải nói vào khoảng không.

Sơ đồ trao đổi tương tác

+---------------------+
|   Java-programma    |
+---------------------+
   |           ^
   v           |
stdin      stdout/stderr
   |           ^
+---------------------+
| Vneshniy protsess   |
+---------------------+
  • Java ghi vào luồng đầu vào của tiến trình (stdin của tiến trình).
  • Java đọc từ stdout và stderr của tiến trình.
  • Tất cả những việc này có thể diễn ra đồng thời!

2. Thực hành: trao đổi tương tác với tiến trình bên ngoài

Hãy xem một ví dụ thực tế: chúng ta sẽ khởi chạy một tiến trình bên ngoài (ví dụ, trình thông dịch Python hoặc một script echo đơn giản), gửi cho nó các dòng và đọc phản hồi.

Ví dụ 1: Chạy script Python chờ đầu vào

Trước hết, tạo một script Python đơn giản (đặt tên là echo_bot.py):

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

Script này chờ dữ liệu đầu vào và với mỗi dòng sẽ trả lời "Echo: ...". Nếu nhập "exit" — nó sẽ kết thúc.

Làm thế nào để khởi chạy và “trò chuyện” với script này từ Java?

1. Khởi chạy tiến trình
ProcessBuilder builder = new ProcessBuilder("python", "echo_bot.py");
Process process = builder.start();
2. Chuẩn bị các luồng để giao tiếp
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(process.getOutputStream()));
BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
3. Hiện thực đối thoại
// Gửi một dòng tới tiến trình
writer.write("Hello, process!\n");
writer.flush();

// Đọc phản hồi
String response = reader.readLine();
System.out.println("Phản hồi của tiến trình: " + response);
4. Lặp lại cho tới khi bạn dừng

Hãy đóng gói lại thành một vòng lặp đơn giản để người dùng có thể gõ trong console, Java gửi đến tiến trình và in phản hồi.

Ví dụ đầy đủ: chat Java với tiến trình bên ngoài

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

        // Luồng để trao đổi với tiến trình
        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("Nhập các dòng để gửi tới tiến trình (exit để thoát):");

        while (true) {
            // Đọc một dòng từ người dùng
            String line = userInput.readLine();
            if (line == null) break;

            // Gửi nó tới tiến trình
            toProcess.write(line + "\n");
            toProcess.flush();

            // Đọc phản hồi của tiến trình
            String response = fromProcess.readLine();
            System.out.println("Phản hồi của tiến trình: " + response);

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

        // Kết thúc tiến trình
        process.destroy();
    }
}

Ghi chú cho mã:

  • Chúng ta dùng ba luồng: để đọc từ người dùng (System.in), để ghi tới tiến trình (process.getOutputStream()) và để đọc phản hồi (process.getInputStream()).
  • Sau mỗi lần gửi một dòng tới tiến trình, ta đọc phản hồi ngay qua readLine().
  • Nếu người dùng nhập "exit", chương trình thoát vòng lặp và hủy tiến trình (process.destroy()).

3. Vì sao việc đọc và ghi đồng thời lại quan trọng?

Trong thực tế, tiến trình có thể in ra rất nhiều dữ liệu và đôi khi còn yêu cầu nhập vào nhiều lần liên tiếp. Nếu không đọc đầu ra của tiến trình kịp thời, bộ đệm nội bộ của nó có thể đầy, và tiến trình sẽ “treo”, chờ ai đó đọc các thông báo của nó. Tương tự, nếu không gửi dữ liệu vào đúng lúc, tiến trình có thể tiếp tục chờ và cũng “đứng im”.

Deadlock: khi mọi thứ treo cứng

Deadlock là tình huống khi hai thực thể (hoặc luồng và tiến trình) chờ nhau và không ai có thể tiếp tục.

Ví dụ về deadlock:

  • Java chờ tiến trình ghi gì đó vào stdout.
  • Tiến trình chờ Java ghi gì đó vào stdin của nó.
  • Cả hai đều chờ — không ai làm việc.

Giải pháp: các luồng riêng cho đọc và ghi

Để tránh deadlock, người ta thường dùng các luồng riêng (Thread) để đồng thời đọc stdout và stderr của tiến trình. Chẳng hạn, tạo hai luồng: một luồng đọc stdout, luồng kia — stderr, còn luồng chính thì ghi vào stdin.

Ví dụ tối thiểu với một luồng riêng cho stderr

// Đọc stderr trong một luồng riêng
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. Thực hành: tương tác với máy tính dòng lệnh

Không phải lúc nào chúng ta cũng có Python. Hãy thử khởi chạy một lệnh chuẩn có mặt gần như ở mọi nơi — ví dụ, máy tính dòng lệnh. Trên Linux/Mac có thể là bc (basic calculator), trên Windows — cmd hoặc powershell.

Ví dụ: tương tác với 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("Kết quả của máy tính: " + result);

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

process.destroy();

Trên Windows: có thể thử khởi chạy cmd và truyền cho nó các lệnh, nhưng sẽ phức tạp hơn (cú pháp khác). Thông thường để minh họa tính tương tác, người ta chọn các tiện ích có trên cả hai nền tảng, hoặc tự viết các mini-script.

5. Vấn đề và cạm bẫy

Đệm (buffering) luồng
Đôi khi tiến trình không in dữ liệu ngay mà tích lũy trong bộ đệm nội bộ và chỉ “xuất” khi bộ đệm đầy hoặc khi nhận được ký tự xuống dòng ("\n"). Điều này có thể dẫn tới việc bạn không thấy phản hồi trong thời gian dài, dù tiến trình đã “chuẩn bị” xong.

Mẹo:

  • Luôn kết thúc dòng bằng ký tự "\n" khi ghi vào stdin của tiến trình.
  • Nếu bạn viết script của riêng mình, hãy gọi flush() sau mỗi lần in ra.

Deadlock do tổ chức luồng không đúng
Nếu không đọc stderr, mà tiến trình ghi rất nhiều dữ liệu vào đó — nó có thể treo, chờ ai đó đọc lỗi.

Mẹo:
Luôn đọc cả stdout và stderr của tiến trình, tốt nhất là trong các luồng riêng.

Tiến trình đã kết thúc — nhưng bạn vẫn đang ghi
Nếu tiến trình đã kết thúc mà bạn vẫn tiếp tục ghi vào đầu vào của nó — bạn sẽ nhận ngoại lệ IOException: "Stream closed".

7. Best practices: giao tiếp tương tác

Sử dụng ExecutorService để đọc song song

Để khỏi tự tay tạo và quản lý các luồng chuyên đọc đầu ra và lỗi, bạn có thể dùng ExecutorService (chẳng hạn, một pool hai luồng):

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

Đóng luồng đúng cách

Sau khi hoàn tất làm việc với tiến trình, nhất định phải đóng tất cả các luồng (stdin, stdout, stderr) để tránh rò rỉ tài nguyên.

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

8. Ví dụ tổng hợp: trao đổi tương tác với tiến trình bên ngoài

Hãy ghép tất cả lại. Ví dụ dưới đây hoạt động như một “chat” tương tác giữa người dùng và tiến trình bên ngoài (ví dụ, script Python hoặc máy tính dòng lệnh).

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

public class InteractiveProcessUniversal {
    public static void main(String[] args) throws IOException {
        // Thay bằng lệnh của bạn — ví dụ: "python echo_bot.py" hoặc "bc"
        ProcessBuilder builder = new ProcessBuilder("python", "echo_bot.py");
        Process process = builder.start();

        ExecutorService executor = Executors.newFixedThreadPool(2);

        // Đọc stdout của tiến trình
        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();
            }
        });

        // Đọc stderr của tiến trình
        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();
            }
        });

        // Luồng chính: ghi vào stdin của tiến trình
        try (BufferedWriter toProcess = new BufferedWriter(new OutputStreamWriter(process.getOutputStream()));
             BufferedReader userInput = new BufferedReader(new InputStreamReader(System.in))) {

            System.out.println("Nhập các dòng cho tiến trình (exit để thoát):");
            String line;
            while ((line = userInput.readLine()) != null) {
                toProcess.write(line + "\n");
                toProcess.flush();
                if ("exit".equals(line)) break;
            }
        }

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

9. Các lỗi thường gặp khi làm việc tương tác với tiến trình

Lỗi №1: Không đọc stderr của tiến trình. Nếu tiến trình ghi nhiều lỗi, còn stderr không được đọc, tiến trình có thể treo. Kể cả khi bạn tin rằng sẽ không có lỗi — vẫn hãy đọc stderr!

Lỗi №2: Không đóng các luồng. Nếu không đóng các luồng của tiến trình, có thể xảy ra rò rỉ tài nguyên, thậm chí chặn việc kết thúc tiến trình.

Lỗi №3: Khác biệt lệnh theo nền tảng. Lệnh và cú pháp khác nhau giữa Windows và các hệ Unix-like. Hãy kiểm tra hệ điều hành và chọn lệnh phù hợp.

Lỗi №4: Chương trình “treo” — không gọi flush() cho đầu ra. Nếu quên gọi flush() sau khi ghi vào luồng, dữ liệu có thể “kẹt” trong bộ đệm và không tới được tiến trình.

Lỗi №5: Đệm đầu ra của tiến trình bên ngoài. Đôi khi tiến trình không in ra dữ liệu cho tới khi đủ số ký tự hoặc nhận "\n". Nếu bạn viết script của riêng mình — hãy dùng flush().

Lỗi №6: Deadlock do đồng thời chờ nhập/xuất. Nếu luồng chính chờ đầu ra của tiến trình, còn tiến trình chờ đầu vào, cả hai có thể “treo”. Hãy dùng các luồng riêng cho đọc/ghi.

Lỗi №7: Ngoại lệ “Stream closed” khi ghi vào tiến trình đã kết thúc. Hãy kiểm tra tiến trình còn sống không trước khi ghi vào stdin của nó. Khi lỗi xảy ra, bạn sẽ nhận IOException: "Stream closed".

1
Nhiệm vụ
JAVA 25 SELF, mức độ, bài học
Đã khóa
Truyền dữ liệu bí mật 🔑
Truyền dữ liệu bí mật 🔑
1
Nhiệm vụ
JAVA 25 SELF, mức độ, bài học
Đã khóa
Đối thoại với Bản sao kỹ thuật số 🗣️
Đối thoại với Bản sao kỹ thuật số 🗣️
Bình luận
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION