Search code examples
javamultithreadingjavafxserverclient

How to return objects from inputStream Client to a Javafx Controller?


So basically I have a MainConstroller class that has methods for every button. I have also a server multi-client application. In Client I have a method sendMessage that sends a String and an Object as a parameter to outputStreams to Server.

In the same method I have 2 inputStream for message from server and an object. The problem is that this method runs on a Thread which implements run method and I cannot return the object.

I tried to create a static class that saves these, but the getters are null, when called in Controller class.

What is the best method to achieve this?

public void onSaveButton(javafx.event.ActionEvent actionEvent) throws Exception {


    Parent root = null;
    Boolean type = false;
    String message = null;


    if (adminCheckbox.isSelected()) {
        root = FXMLLoader.load(getClass().getResource("/fxml/admin.fxml"));
        type = true;
        message = "Admin";
    }

    if (competitorCheckbox.isSelected()) {
        root = FXMLLoader.load(getClass().getResource("/fxml/competitor.fxml"));
        message = "Competitor";
    }

    PersonEntity personEntity = new PersonEntity();
    personEntity.setIdTeam(Integer.parseInt(teamField.getText()));
    personEntity.setType(type);
    personEntity.setUsername(usernameField.getText());

    client.sendMessageToServer(message, personEntity);

    System.out.println(Utils.getMessage());}

And the Client method:

 public void sendMessageToServer(String message, Object object) throws Exception {

    new Thread(new Runnable() {
        @Override
        public void run() {
            System.out.println("Say something and the message will be sent to the server: ");

            //For receiving and sending data
            boolean isClose = false;

            while (!isClose) {
                try {

                    ObjectOutputStream outputStream = new ObjectOutputStream(socket.getOutputStream());
                    ObjectInputStream inputStream = new ObjectInputStream(socket.getInputStream());

                    if (message.equals("Bye")) {
                        isClose = true;
                    }

                    outputStream.writeObject(message);
                    outputStream.writeObject(object);

                    String messageFromServer = (String) inputStream.readObject();
                    //System.out.println(messageFromServer);

                    int index = messageFromServer.indexOf(' ');
                    String word = messageFromServer.substring(0, index);

                    if (messageFromServer.equals("Bye")) {
                        isClose = true;
                    }

                    if (!word.equals("LIST")) {
                        Object obj = (Object) inputStream.readObject();
                        Utils.setMessage(messageFromServer);
                        return;
                        //Utils.setObject(obj);
                        //System.out.println("IN FOR " + Utils.getMessage());

                    } else {
                        List<Object> list = (List<Object>) inputStream.readObject();
                        Utils.setMessage(messageFromServer);
                        Utils.setObjects(list);
                        return;

                    }

                } catch (IOException | ClassNotFoundException e) {
                    e.printStackTrace();
                }
            }
            try {
                socket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }).start();
}

Solution

  • I don't know what your server is, nor the rest of your app. I know you are using raw TCP sockets for communication using a custom protocol.

    What I did was take code for a similar example from the Java tutorials. The code is the KnockKnock client server code from: Writing the Server Side of a Socket. The basic server code is unchanged, I just replaced the console-based client with a JavaFX UI.

    Unchanged server code:

    Replaced console client:

    I provide two different client implementations; one makes synchronous client calls on the JavaFX application thread, the other makes asynchronous client calls using a JavaFX task.

    The current asynchronous task-based solution is not robust enough for a full-scale production system as it is technically possible for it to lose messages and it doesn't robustly match request and response messages. But for a demo like this it is OK.

    For simple execution, the UI apps both launch a local server on a pre-defined port when they are run, but you could remove the code to launch the server from the UI apps and run the server from the command line if you want.

    knock-knock

    KnockKnockSyncClient.java

    import java.io.BufferedReader;
    import java.io.IOException;
    import java.io.InputStreamReader;
    import java.io.PrintWriter;
    import java.net.Socket;
    
    public class KnockKnockSyncClient {
        private Socket socket;
        private PrintWriter out;
        private BufferedReader in;
    
        public String connect(String host, int port) {
            try {
                socket = new Socket(host, port);
    
                out = new PrintWriter(
                        socket.getOutputStream(),
                        true
                );
    
                in = new BufferedReader(
                        new InputStreamReader(
                                socket.getInputStream()
                        )
                );
    
                return in.readLine();
            } catch (IOException e) {
                e.printStackTrace();
            }
    
            return null;
        }
    
        public String sendMessage(String request) {
            String response = null;
    
            try {
                out.println(request);
                response = in.readLine();
            } catch (IOException e) {
                e.printStackTrace();
            }
    
            return response;
        }
    
        public void disconnect() {
            try {
                if (socket != null) {
                    socket.close();
                    socket = null;
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
    

    KnockKnockSyncApp

    import javafx.animation.FadeTransition;
    import javafx.application.*;
    import javafx.geometry.Insets;
    import javafx.scene.*;
    import javafx.scene.control.*;
    import javafx.scene.layout.VBox;
    import javafx.stage.Stage;
    import javafx.util.Duration;
    
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    
    public class KnockKnockSyncApp extends Application {
        private static final String HOST = "localhost";
        private static final int PORT = 8809;
    
        public static final String QUIT_RESPONSE = "Bye.";
    
        private ExecutorService serverExecutor;
        private KnockKnockSyncClient client;
    
        private static final String CSS = """
                data:text/css,
                .root {
                    -fx-font-size: 20;
                }
                .list-cell {
                    -fx-alignment: baseline-right;
                    -fx-text-fill: purple;
                }
                .list-cell:odd {
                    -fx-alignment: baseline-left;
                    -fx-text-fill: darkgreen;
                }
                """;
    
        @Override
        public void init() {
            serverExecutor = Executors.newSingleThreadExecutor(r -> new Thread(r, "KKServer"));
            serverExecutor.submit(
                    () -> {
                        try {
                            KKMultiServer.main(new String[]{ PORT + "" });
                        } catch (Exception e) {
                            e.printStackTrace();
                        }
                    }
            );
    
            client = new KnockKnockSyncClient();
        }
    
        @Override
        public void start(Stage stage) {
            ListView<String> messageView = new ListView<>();
    
            TextField inputField = new TextField();
            inputField.setPromptText("Enter a message for the server.");
    
            inputField.setOnAction(event -> {
                String request = inputField.getText();
                messageView.getItems().add(request);
    
                String response = client.sendMessage(request);
                messageView.getItems().add(response);
    
                messageView.scrollTo(Math.max(0, messageView.getItems().size() - 1));
    
                inputField.clear();
    
                if (QUIT_RESPONSE.equals(response)) {
                    closeApp(inputField.getScene());
                }
            });
    
            VBox layout = new VBox(10,
                    messageView,
                    inputField
            );
            layout.setPadding(new Insets(10));
            layout.setPrefWidth(600);
    
            Scene scene = new Scene(layout);
            scene.getStylesheets().add(CSS);
    
            stage.setScene(scene);
            stage.show();
    
            inputField.requestFocus();
    
            String connectResponse = client.connect(HOST, PORT);
            if (connectResponse != null) {
                messageView.getItems().add(connectResponse);
            }
        }
    
        private void closeApp(Scene scene) {
            Parent root = scene.getRoot();
            root.setDisable(true);
            FadeTransition fade = new FadeTransition(Duration.seconds(1), root);
            fade.setToValue(0);
            fade.setOnFinished(e -> Platform.exit());
            fade.play();
        }
    
        @Override
        public void stop() {
            client.disconnect();
            serverExecutor.shutdown();
        }
    
        public static void main(String[] args) {
            launch();
        }
    }
    

    KnockKnockAsyncClient.java

    import javafx.concurrent.Task;
    
    import java.io.BufferedReader;
    import java.io.IOException;
    import java.io.InputStreamReader;
    import java.io.PrintWriter;
    import java.net.Socket;
    import java.util.concurrent.BlockingQueue;
    import java.util.concurrent.LinkedBlockingDeque;
    
    public class KnockKnockAsyncClient extends Task<Void> {
        private final String host;
        private final int port;
    
        private final BlockingQueue<String> messageQueue = new LinkedBlockingDeque<>();
    
        public KnockKnockAsyncClient(String host, int port) {
            this.host = host;
            this.port = port;
        }
    
        @Override
        protected Void call() {
            try (
                    Socket kkSocket = new Socket(host, port);
                    PrintWriter out = new PrintWriter(kkSocket.getOutputStream(), true);
                    BufferedReader in = new BufferedReader(
                            new InputStreamReader(kkSocket.getInputStream()));
            ) {
                String fromServer;
                String fromUser;
    
                while ((fromServer = in.readLine()) != null) {
                    // this is not a completely robust implementation because updateMessage
                    // can coalesce responses and there is no matching in send message calls
                    // to responses.  A more robust implementation may allow matching of
                    // requests and responses, perhaps via a message id.  It could also
                    // replace the updateMessage call with storage of results in a collection
                    // (e.g. an observable list) with thread safe notifications of response
                    // arrivals, e.g. through CompleteableFutures and/or Platform.runLater calls.
                    updateMessage(fromServer);
                    System.out.println("Server: " + fromServer);
                    if (fromServer.equals("Bye."))
                        break;
    
                    fromUser = messageQueue.take();
                    System.out.println("Client: " + fromUser);
                    out.println(fromUser);
                }
            } catch (InterruptedException e) {
                if (isCancelled()) {
                    updateMessage("Cancelled");
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
    
            return null;
        }
    
        public void sendMessage(String request) {
            messageQueue.add(request);
        }
    }
    

    KnockKnockAsyncApp.java

    import javafx.animation.FadeTransition;
    import javafx.application.Application;
    import javafx.application.Platform;
    import javafx.geometry.Insets;
    import javafx.scene.Parent;
    import javafx.scene.Scene;
    import javafx.scene.control.ListView;
    import javafx.scene.control.TextField;
    import javafx.scene.layout.VBox;
    import javafx.stage.Stage;
    import javafx.util.Duration;
    
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    import java.util.concurrent.ThreadFactory;
    import java.util.concurrent.atomic.AtomicInteger;
    
    public class KnockKnockAsyncApp extends Application {
        private static final String HOST = "localhost";
        private static final int PORT = 8809;
    
        public static final String QUIT_RESPONSE = "Bye.";
    
        private ExecutorService serverExecutor;
        private ExecutorService clientExecutor;
    
        private static final String CSS = """
                data:text/css,
                .root {
                    -fx-font-size: 20;
                }
                .list-cell {
                    -fx-alignment: baseline-right;
                    -fx-text-fill: purple;
                }
                .list-cell:odd {
                    -fx-alignment: baseline-left;
                    -fx-text-fill: darkgreen;
                }
                """;
    
        @Override
        public void init() {
            serverExecutor = Executors.newSingleThreadExecutor(r -> new Thread(r, "KKServer"));
            serverExecutor.submit(
                    () -> {
                        try {
                            KKMultiServer.main(new String[]{ PORT + "" });
                        } catch (Exception e) {
                            e.printStackTrace();
                        }
                    }
            );
    
            clientExecutor = Executors.newSingleThreadExecutor(new ThreadFactory() {
                final AtomicInteger threadNum = new AtomicInteger(0);
    
                @Override
                public Thread newThread(Runnable r) {
                    return new Thread(r,"KKClient-" + threadNum.getAndAdd(1));
                }
            });
        }
    
        @Override
        public void start(Stage stage) {
            ListView<String> messageView = new ListView<>();
    
            KnockKnockAsyncClient client = new KnockKnockAsyncClient(HOST, PORT);
            // monitor and action responses from the server.
            client.messageProperty().addListener((observable, oldValue, response) -> {
                if (response != null) {
                    logMessage(messageView, response);
                }
    
                if (QUIT_RESPONSE.equals(response)) {
                    closeApp(messageView.getScene());
                }
            });
    
            TextField inputField = new TextField();
            inputField.setPromptText("Enter a message for the server.");
    
            inputField.setOnAction(event -> {
                String request = inputField.getText();
                logMessage(messageView, request);
    
                client.sendMessage(request);
    
                inputField.clear();
            });
    
            VBox layout = new VBox(10,
                    messageView,
                    inputField
            );
            layout.setPadding(new Insets(10));
            layout.setPrefWidth(600);
    
            Scene scene = new Scene(layout);
            scene.getStylesheets().add(CSS);
    
            stage.setScene(scene);
            stage.show();
    
            inputField.requestFocus();
    
            clientExecutor.submit(client);
        }
    
        private void logMessage(ListView<String> messageView, String request) {
            messageView.getItems().add(request);
            messageView.scrollTo(Math.max(0, messageView.getItems().size() - 1));
        }
    
        private void closeApp(Scene scene) {
            Parent root = scene.getRoot();
            root.setDisable(true);
            FadeTransition fade = new FadeTransition(Duration.seconds(1), root);
            fade.setToValue(0);
            fade.setOnFinished(e -> Platform.exit());
            fade.play();
        }
    
        @Override
        public void stop() {
            clientExecutor.shutdown();
            serverExecutor.shutdown();
        }
    
        public static void main(String[] args) {
            launch();
        }
    }