在JavaSwing中,如何在不冻结GUI的情况下与进程进行随机通信?

在JavaSwing中,如何在不冻结GUI的情况下与进程进行随机通信?,java,multithreading,swing,processbuilder,swingworker,Java,Multithreading,Swing,Processbuilder,Swingworker,我正在构建一个国际象棋GUI应用程序,其任务是显示棋盘和棋子,防止非法移动被输入 它还应该具有与国际象棋引擎(如stockfish)通信的功能。这就是我现在正在努力解决的问题。chess引擎是一个exe文件,可使用ProcessBuilder访问: Process chessEngineProcess = new ProcessBuilder(chessEngineUrl).start(); InputStream processInputStream = chessEngineProcess

我正在构建一个国际象棋GUI应用程序,其任务是显示棋盘和棋子,防止非法移动被输入

它还应该具有与国际象棋引擎(如stockfish)通信的功能。这就是我现在正在努力解决的问题。chess引擎是一个exe文件,可使用ProcessBuilder访问:

Process chessEngineProcess = new ProcessBuilder(chessEngineUrl).start();

InputStream processInputStream = chessEngineProcess.getInputStream();
OutputStream processOutputStream = chessEngineProcess.getOutputStream();

BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(processOutputStream));
BufferedReader reader = new BufferedReader(new InputStreamReader(processInputStream));
我想将字符串(UCI协议中的命令)发送到引擎,引擎通过连续输出文本几秒钟或更长时间来响应。这会挂断GUI。我需要根据引擎的输出更新GUI中的textArea(实时)。这不是一次性的操作。每当发生某些GUI事件(例如,用户移动)时,我会随机执行此操作(发送命令并实时更新GUI)

我知道我需要在另一个线程中读取流,我知道SwingWorker,但我无法让它正常工作

我尝试的是: 因为流读取是一个阻塞操作(我们一直等待引擎的输出),所以流读取线程永远不会终止

考虑到这一点,我尝试创建一个类来扩展
SwingWorker
,并将
chessEngineProcess
(以及它的流读取器和写入器)设置为私有成员变量。我实现了
doInBackground
过程
方法。我在这个类中还有一个公共方法,用于向引擎发送命令

public void sendCommandToEngine(String command) {
        try {
            writer.write(command + '\n');
            writer.flush();
        } catch (IOException e) {
            JOptionPane.showMessageDialog(null, e.getMessage());
        }
    }
我在
doInBackground
中读取流,然后在
process
方法中发布输出并更新GUI

当我从GUI类(例如,从事件侦听器)向引擎发送命令时,这会导致非常奇怪的行为。显示的输出是错误的(有时部分错误,有时完全错误),并且经常抛出异常

我不知所措,非常绝望,所以请帮助我!这是一个非常重要的项目。请随意提出您认为可行的任何解决方案

编辑: 我得到一个空指针异常,堆栈跟踪如下:

Exception in thread "AWT-EventQueue-0" java.lang.NullPointerException
    at Moves.Move.isMovePossible(Move.java:84)
    at Moves.Move.executeMove(Move.java:68)
    at gui.ChessBoard.performEngineMove(ChessBoard.java:328)
    at gui.MainFrame.receiveEnginesBestMove(MainFrame.java:180)
    at gui.EngineWorker.process(EngineWorker.java:91)
    at javax.swing.SwingWorker$3.run(SwingWorker.java:414)
    at sun.swing.AccumulativeRunnable.run(AccumulativeRunnable.java:112)
    at javax.swing.SwingWorker$DoSubmitAccumulativeRunnable.run(SwingWorker.java:832)
    at sun.swing.AccumulativeRunnable.run(AccumulativeRunnable.java:112)
    at javax.swing.SwingWorker$DoSubmitAccumulativeRunnable.actionPerformed(SwingWorker.java:842)
    at javax.swing.Timer.fireActionPerformed(Timer.java:313)
    at javax.swing.Timer$DoPostEvent.run(Timer.java:245)
    at java.awt.event.InvocationEvent.dispatch(InvocationEvent.java:311)
    at java.awt.EventQueue.dispatchEventImpl(EventQueue.java:756)
    at java.awt.EventQueue.access$500(EventQueue.java:97)
    at java.awt.EventQueue$3.run(EventQueue.java:709)
    at java.awt.EventQueue$3.run(EventQueue.java:703)
    at java.security.AccessController.doPrivileged(Native Method)
    at java.security.ProtectionDomain$JavaSecurityAccessImpl.doIntersectionPrivilege(ProtectionDomain.java:80)
    at java.awt.EventQueue.dispatchEvent(EventQueue.java:726)
    at java.awt.EventDispatchThread.pumpOneEventForFilters(EventDispatchThread.java:201)
    at java.awt.EventDispatchThread.pumpEventsForFilter(EventDispatchThread.java:116)
    at java.awt.EventDispatchThread.pumpEventsForHierarchy(EventDispatchThread.java:105)
    at java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:101)
    at java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:93)
    at java.awt.EventDispatchThread.run(EventDispatchThread.java:82)
一些细节: 基本上我有一个“MainFrame”类,它是一个包含所有GUI元素的JFrame。这是我向组件添加事件侦听器的地方。在某些事件侦听器中,我调用
sendCommandToEngine
。当发动机开始发送响应时,这将启动阻塞的
doInBackground

如果
过程
方法检测到引擎输出了“最佳移动”,则可以在
棋盘
(显示棋盘的大型机组件)上调用
性能引擎移动

performEnginesMove
功能检查移动是否有效(可能),然后在板上进行移动(在move类的帮助下)


由于某些原因,这不起作用。

我为
进程
ProcessBuilder
类构建了一个委托,以显示其余代码应如何使用。我分别将这些类称为
GameEngineProcess
GameEngineProcessBuilder

GameEngineProcess
正在创建响应,这些响应是简单的
String
s,可以直接附加到玩家GUI的
JTextArea
。它实际上扩展了
线程
,使其异步运行。因此,这个特定类的实现不是您想要的,而是用于模拟
过程
类。我在这个类的响应中添加了一些延迟,以模拟引擎生成它们所需的时间

然后是自定义类
OnUserActionWorker
,它扩展了
SwingWorker
,并异步执行您的请求:它接收来自引擎进程的响应,并将它们转发给GUI,GUI更新其
JTextArea
。该类在每个引擎请求中使用一次,即我们为用户在与GUI交互时创建的每个请求创建并执行该类的新实例。请注意,这并不意味着针对每个请求关闭和重新打开引擎。
GameEngineProcess
启动一次,然后在整个游戏正常运行时间内保持运行

我假设您有一种方法来判断单个引擎请求是否已完成所有响应。为了简单起见,我编写的代码中存在一条消息(类型为
String
),每次在流程流中写入该消息,以指示每个请求的响应结束。这是消息的
END\u
常量。因此,这让
OnUserActionWorker
知道何时终止接收响应,因此稍后将为每个新请求创建下一个实例

最后是GUI,它是一个
JFrame
,由
JTextArea
和一个按钮网格组成,玩家可以与这些按钮交互,并根据按下的按钮向引擎发送请求命令。我再次使用
String
s作为命令,但我猜在这种情况下,这可能也是您需要的

遵循代码:

import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.GridLayout;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;
import java.util.List;
import java.util.Objects;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.SwingWorker;

public class Main {

    //Just a simple 'flag' to indicate end of responses per engine request:
    private static final String END_OF_MESSAGES = "\u0000\u0000\u0000\u0000";

    //A class simulating the 'ProcessBuilder' class:
    private static class GameEngineProcessBuilder {
        private String executionCommand;

        public GameEngineProcessBuilder(final String executionCommand) {
            this.executionCommand = executionCommand;
        }

        public GameEngineProcessBuilder command(final String executionCommand) {
            this.executionCommand = executionCommand;
            return this;
        }

        public GameEngineProcess start() throws IOException {
            final GameEngineProcess gep = new GameEngineProcess(executionCommand);
            gep.setDaemon(true);
            gep.start();
            return gep;
        }
    }

    //A class simulating the 'Process' class:
    private static class GameEngineProcess extends Thread {
        private final String executionCommand; //Actually not used.
        private final PipedInputStream stdin, clientStdin;
        private final PipedOutputStream stdout, clientStdout;

        public GameEngineProcess(final String executionCommand) throws IOException {
            this.executionCommand = Objects.toString(executionCommand); //Assuming nulls allowed.

            //Client side streams:
            clientStdout = new PipedOutputStream();
            clientStdin = new PipedInputStream();

            //Remote streams (of the engine):
            stdin = new PipedInputStream(clientStdout);
            stdout = new PipedOutputStream(clientStdin);
        }

        public OutputStream getOutputStream() {
            return clientStdout;
        }

        public InputStream getInputStream() {
            return clientStdin;
        }

        @Override
        public void run() {
            try {
                final BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(stdout));
                final BufferedReader br = new BufferedReader(new InputStreamReader(stdin));
                String line = br.readLine();
                while (line != null) {
                    for (int i = 0; i < 10; ++i) { //Simulate many responses per request.
                        Thread.sleep(333); //Simulate a delay in the responses.
                        bw.write(line + " (" + i + ')'); //Echo the line with the index.
                        bw.newLine();
                        bw.flush();
                    }
                    bw.write(END_OF_MESSAGES); //Indicate termination of this particular request.
                    bw.newLine();
                    bw.flush();
                    line = br.readLine();
                }
                System.out.println("Process gracefull shutdown.");
            }
            catch (final InterruptedException | IOException x) {
                System.err.println("Process termination with error: " + x);
            }
        }
    }

    //This is the SwingWorker that handles the responses from the engine and updates the GUI.
    private static class OnUserActionWorker extends SwingWorker<Void, String> {
        private final GameFrame gui;
        private final String commandToEngine;

        private OnUserActionWorker(final GameFrame gui,
                                   final String commandToEngine) {
            this.gui = Objects.requireNonNull(gui);
            this.commandToEngine = Objects.toString(commandToEngine); //Assuming nulls allowed.
        }

        //Not on the EDT...
        @Override
        protected Void doInBackground() throws Exception {
            final BufferedWriter bw = gui.getEngineProcessWriter();
            final BufferedReader br = gui.getEngineProcessReader();

            //Send request:
            bw.write(commandToEngine);
            bw.newLine();
            bw.flush();

            //Receive responses:
            String line = br.readLine();
            while (line != null && !line.equals(END_OF_MESSAGES)) {
                publish(line); //Use 'publish' to forward the text to the 'process' method.
                line = br.readLine();
            }

            return null;
        }

        //On the EDT...
        @Override
        protected void done() {
            gui.responseDone(); //Indicate end of responses at the GUI level.
        }

        //On the EDT...
        @Override
        protected void process(final List<String> chunks) {
            chunks.forEach(chunk -> gui.responsePart(chunk)); //Sets the text of the the text area of the GUI.
        }
    }

    //The main frame of the GUI of the user/player:
    private static class GameFrame extends JFrame implements Runnable {
        private final JButton[][] grid;
        private final JTextArea output;
        private BufferedReader procReader;
        private BufferedWriter procWriter;

        public GameFrame(final int rows,
                         final int cols) {
            super("Chess with remote engine");

            output = new JTextArea(rows, cols);
            output.setEditable(false);
            output.setFont(new Font(Font.MONOSPACED, Font.ITALIC, output.getFont().getSize()));

            final JPanel gridPanel = new JPanel(new GridLayout(0, cols));

            grid = new JButton[rows][cols];
            for (int row = 0; row < rows; ++row)
                for (int col = 0; col < cols; ++col) {
                    final JButton b = new JButton(String.format("Chessman %02d,%02d", row, col));
                    b.setPreferredSize(new Dimension(b.getPreferredSize().width, 50));
                    b.addActionListener(e -> sendCommandToEngine("Click \"" + b.getText() + "\"!"));
                    gridPanel.add(b);
                    grid[row][col] = b;
                }

            final JScrollPane outputScroll = new JScrollPane(output);
            outputScroll.setPreferredSize(gridPanel.getPreferredSize());

            final JPanel contents = new JPanel(new BorderLayout());
            contents.add(gridPanel, BorderLayout.LINE_START);
            contents.add(outputScroll, BorderLayout.CENTER);

            super.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
            super.getContentPane().add(contents);
            super.pack();
        }

        //Utility method to enable/disable all the buttons of the grid at once:
        private void gridSetEnabled(final boolean enabled) {
            for (final JButton[] row: grid)
                for (final JButton b: row)
                    b.setEnabled(enabled);
        }

        //This is the method that sends the next request to the engine:
        private void sendCommandToEngine(final String commandToEngine) {
            gridSetEnabled(false);
            output.setText("> Command accepted.");
            new OnUserActionWorker(this, commandToEngine).execute();
        }

        public BufferedReader getEngineProcessReader() {
            return procReader;
        }

        public BufferedWriter getEngineProcessWriter() {
            return procWriter;
        }

        //Called by 'SwingWorker.process':
        public void responsePart(final String msg) {
            output.append("\n" + msg);
        }

        //Called by 'SwingWorker.done':
        public void responseDone() {
            output.append("\n> Response finished.");
            gridSetEnabled(true);
        }

        @Override
        public void run() {
            try {
                //Here you build and start the process:
                final GameEngineProcess proc = new GameEngineProcessBuilder("stockfish").start();

                //Here you obtain the I/O streams:
                procWriter = new BufferedWriter(new OutputStreamWriter(proc.getOutputStream()));
                procReader = new BufferedReader(new InputStreamReader(proc.getInputStream()));

                //Finally show the GUI:
                setLocationRelativeTo(null);
                setVisible(true);
            }
            catch (final IOException iox) {
                JOptionPane.showMessageDialog(null, iox.toString());
            }
        }
    }

    public static void main(final String[] args) {
        new GameFrame(3, 3).run(); //The main thread starts the game, which shows the GUI...
    }
}
导入java.awt.BorderLayout;
导入java.awt.Dimension;
导入java.awt.Font;
导入java.awt.GridLayout;
导入java.io.BufferedReader;
导入java.io.BufferedWriter;
导入java.io.IOException;
导入java.io.InputStream;
导入java.io.InputStreamReader;
导入java.io.OutputStream;
导入java.io.OutputStreamWriter;
导入java.io.PipedInputStream;
导入java.io.PipedOutputStream;
导入java.util.List;
导入java.util.Objects;
导入javax.swing.JButton;
导入javax.swing.JFrame;
导入javax.swing.JOptionPane;
导入javax.swing.JPanel;
导入javax.swing.JScrollPane;
导入javax.swing.JTextArea;
导入javax.swing.SwingWorker;
公共班机{
//只是一个简单的“标志”来表示响应的结束