Problem z wątkami w prostym chacie w Javie

0

Witam. Robię w Javie i JavieFX prosty chat i próbuję coś zdziałać z wątkami. Gdy Client wysyła coś do ClientWindow i na odwrót, w ClientWindow jest błąd:

Exception in thread "Receiver" java.lang.IllegalStateException: Not on FX application thread; currentThread = Receiver
    at javafx.graphics/com.sun.javafx.tk.Toolkit.checkFxUserThread(Toolkit.java:291)
    at javafx.graphics/com.sun.javafx.tk.quantum.QuantumToolkit.checkFxUserThread(QuantumToolkit.java:423)
    at javafx.graphics/javafx.scene.Parent$3.onProposedChange(Parent.java:478)
    at javafx.base/com.sun.javafx.collections.VetoableListDecorator.add(VetoableListDecorator.java:206)
    at window.classes.Window.addSomebodysMessage(Window.java:53)
    at logic.Receiver.run(Receiver.java:28)
    at java.base/java.lang.Thread.run(Thread.java:844)

Co mogę z zrobić aby zadziałało tzn. aby MyMessage lub SomebodysMessage pojawiło sie w VBoxie w ScrollPane? Problem pojawił się po dodaniu wyświetlania wiadomości w okienku.

Linki do kodu
-Client: tutaj
-ClientWindow(tutaj wyskakuje ten błąd): tutaj
-Server: tutaj

Najpierw włączyć serwer, później klientów w dowolnej kolejności.

Z góry dzięki wielkie za pomoc.

0

Nie sprawdzałem kodu (bez jaj by szukać w trzech projektach o co chodzi, wklej kod) ale masz odpowiedź na SO - https://stackoverflow.com/questions/17850191/why-am-i-getting-java-lang-illegalstateexception-not-on-fx-application-thread . Nie możesz uaktualniać UI w wątku innym niż tym z JavyFX.

0
package logic;

import window.classes.Window;

import java.io.DataOutputStream;
import java.io.IOException;
import java.net.Socket;
import java.util.Scanner;

public class Sender implements Runnable {

    private Socket socket;
    private Thread thread;
    private Window window;

    public Sender(Socket socket, Window window) {
        this.socket = socket;
        this.window = window;
        thread = new Thread(this, "Sender");
        thread.start();
    }

    @Override
    public void run() {
        System.out.println("trwam ja, SENDER");
        Scanner scanner = new Scanner(System.in);
        String written = "";
        DataOutputStream output;

        while(true) { 
            written = scanner.nextLine();

            try {
                if (written.equals("/disconnect")) {
                    output = new DataOutputStream(socket.getOutputStream());
                    output.writeUTF("/disconnect");
                    this.thread.interrupt();
                } else {
                    output = new DataOutputStream(socket.getOutputStream());
                    window.addMyMessage(written);
                    output.writeUTF(written);
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

Mam wątek, a w wątku metodę window.addMyMessage(written); Ta metoda dodaje dymek ze stringiem (written). Metoda działa ale jak może ją
uruchomić zdarzenie z powyższego wątku?

Metoda:

public void addMyMessage(String message) throws IOException {
        MyMessage myMessage = new MyMessage(message);
        FXMLLoader loader = new FXMLLoader(this.getClass().getResource("/window/fxmls/myMessage.fxml"));
        loader.setController(myMessage);
        AnchorPane anchorPane = loader.load();
        vBoxForMessages.getChildren().add(anchorPane);
        myMessage.setText(message);
    }
1

Według dokumentacji tak tego nie możesz zrobić , tzn. z innego wątku niż główny gmerać w UI JavaFx. Spróbuj może w swojej klasie Window, np. w konstruktorze zrobić coś takiego jak i zobacz co się stanie.

Platform.runLater(new Runnable() {
    @Override
    public void run() {
        System.out.println("trwam ja, SENDER");
        Scanner scanner = new Scanner(System.in);
        String written = "";
        DataOutputStream output;
 
        while(true) { 
            written = scanner.nextLine();
 
            try {
                if (written.equals("/disconnect")) {
                    output = new DataOutputStream(socket.getOutputStream());
                    output.writeUTF("/disconnect");
                    this.thread.interrupt();
                } else {
                    output = new DataOutputStream(socket.getOutputStream());
                    addMyMessage(written);
                    output.writeUTF(written);
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
});

Nie piszę w JavieFX więc tak trochę strzelam.

1

W JavaFX nie możesz dłubać w UI z wątku innego niż główny, ale jest na to workaround - możesz zdefiniować Timeline, który będzie periodycznie wykonywał taska np. odświeżanie okienka czatu. Animacje/Timelines to taki legalny sposób na asynchroniczne/współbieżne (nie jestem pewien jak one tam działały, nie grzebałem w tym od jakiegoś czasu) interakcje z UI, bez zamrażanie go przez jakieś nieskończone pętle. Potrzebowałem kiedyś logować zdarzenia na serwerze do jednego projektu na studia i rozwiązałem to z grubsza w ten sposób (pomysł brzydki jak noc, ale działał i nie powodował błędów):

  • główny wątek standardowo obsługuje zdarzenia w okienku JavaFX
  • osobny wątek nasłuchuje połączeń od klientów i spawnuje wątki dla nowych połączeń
  • zespawnowany wątek obsługuje połączenie
  • wszelkie zdarzenia (nawiązanie połączenia, zamknięcie połączenia, utrata połączenia, odrzucenie połączenia (serwer miał czarną listę), nieudane logowanie) były wrzucane przez te wszystkie wątki do wspólnej, zsynchronizowanej kolejki - nie pamiętam, co to była za kolejka, ale jakaś z biblioteki standardowej, grunt żeby była thread-safe
  • miałem zdefiniowany Timeline, który miał z kolei cyklicznie, w nieskończoność wykonywane zadanie - wyciągał zawartość tej kolejki ze zdarzeniami i na podstawie tego, co wyjął, dorzucał do TextBoxa kolejne log entries dla każdego zdarzenia.

Kod był (jest, dalej gdzieś tam siedzi) brzydki jak noc, ale działał, nie gubił logów, nie miał wyścigów, okienko JavaFX nie zamarażało się z powodu ciągłego odświeżania logów. Poza tym te animacje/timeline'y mają tę zaletę, że można je łatwo włączać/wyłączać w locie, także z UI.

0

Jak mam tego Timeline zaimplementować? Mam wątek przyjmujący wiadomości i dodający do kolejki obiekt Message zawierający samą wiadomość i informacje czy jest ona przychodząca czy wychodząca.

0

Czytanie własnego kodu po latach jest straszne, ale znalazłem dziada... i to nawet za pierwszym podejściem :D

Możesz mniej więcej tak:

public class Controller implements Initializable {
   // ple ple ple tona kupowatego studenckiego kodu
    @Override
    public void initialize(URL location, ResourceBundle resources) {
     // ple ple ple tona kupowatego studenckiego kodu
      Timeline tm=new Timeline(new KeyFrame(Duration.millis(100.0),event -> {
          while(!incomingLogEntryBuffer.isEmpty())
           applyLog(incomingLogEntryBuffer.poll());
      }));
      tm.setCycleCount(Animation.INDEFINITE);
      tm.play();
   }
}

Tutaj akurat obsługę zdarzenia (czyli opróżnianie bufora z wiadomościami) młody durszlak zrobił przekazując obiekt Duration (bodajże odstęp między "klatkami" na timeline tudzież długość trwania jednej, nie pamiętam) oraz lambdę mającą wykonać dumpowanie zawartości bufora na ekran do obiektu KeyFrame, a ten do Timeline. Na koniec ustawiasz liczbę wykonań, tutaj nieograniczoną, i odpalasz.

1 użytkowników online, w tym zalogowanych: 0, gości: 1