IT rekvalifikace s garancí práce. Seniorní programátoři vydělávají až 160 000 Kč/měsíc a rekvalifikace je prvním krokem. Zjisti, jak na to!
Hledáme nové posily do ITnetwork týmu. Podívej se na volné pozice a přidej se do nejagilnější firmy na trhu - Více informací.

16. diel - Java chat - Klient - Zobrazenie lokálnych serverov

V minulej lekcii Java chat - Klient - Zoznámenie sa s kostrou aplikácie , sme sa zoznámili s kostrou klienta. Dnes vytvoríme implementáciu okna pre správu servera. Čaká nás teda zobrazenie nájdených serverov v lokálnej sieti.

Upravenie triedy LanServerFinder

Než sa pustíme do implementácie klientskych častí, musíme najskôr ľahko upraviť rozhranie atribútu typu OnServerFoundListener v triede LanServerFinder. Metóde onServerFound() pridáme ešte parameter InetAddress address, ktorý bude obsahovať adresu servera, odkiaľ datagram prišiel:

void onServerFound(ServerStatusData data, InetAddress address);

Ďalej musíme opraviť volanie tejto metódy:

serverFoundListener.onServerFound(statusData, datagramPacket.getAddress());

Zdrojovú adresu získame volaním metódy getAddress() nad prijatým datagramom.

Zobrazenie lokálnych serverov

Okno pre zobrazenie serverov už máme vytvorené. Všetky nájdené servery budeme zobrazovať v listview. Budeme zobrazovať tri informácie o serveri:

  • názov servera
  • adresu a port servera
  • obsadenosť servera

Model záznamu servera

Vytvoríme triedu ServerEntry, ktorú vložíme do balíčka model. Táto trieda bude obsahovať vyššie uvedené informácie.

package cz.stechy.chat.model;

public class ServerEntry {

    private final UUID serverID;
    private final InetAddress serverAddress;
    private final StringProperty serverName = new SimpleStringProperty(this, "serverName", null);
    private final IntegerProperty connectedClients = new SimpleIntegerProperty(this, "connectedClients", 0);
    private final IntegerProperty maxClients = new SimpleIntegerProperty(this, "maxClients", Integer.MAX_VALUE);
    private final ObjectProperty<ServerStatus> serverStatus = new SimpleObjectProperty<>(this, "serverStatus", ServerStatus.EMPTY);
    private final BooleanProperty connected = new SimpleBooleanProperty(this, "connected", false);
    private final IntegerProperty port = new SimpleIntegerProperty(this, "port", 0);
    private final AtomicLong lastUpdate = new AtomicLong();

    public ServerEntry(ServerStatusData serverStatusData, InetAddress serverAddress) {
        this.serverID = serverStatusData.serverID;
        this.serverAddress = serverAddress;
        this.serverName.set(serverStatusData.serverName);
        this.connectedClients.set(serverStatusData.clientCount);
        this.maxClients.set(serverStatusData.maxClients);
        this.serverStatus.set(serverStatusData.serverStatus);
        this.port.set(serverStatusData.port);
        this.lastUpdate.set(System.currentTimeMillis());
    }

    public void update(ServerStatusData newServerStatusData) {
        this.serverName.set(newServerStatusData.serverName);
        this.connectedClients.set(newServerStatusData.clientCount);
        this.maxClients.set(newServerStatusData.maxClients);
        this.serverStatus.set(newServerStatusData.serverStatus);
        this.port.set(newServerStatusData.port);
        this.lastUpdate.set(System.currentTimeMillis());
    }

    public boolean hasOldData() {
        final long time = System.currentTimeMillis();
        return time - lastUpdate.get() > 3000;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }
        ServerEntry that = (ServerEntry) o;
        return Objects.equals(serverID, that.serverID);
    }

    @Override
    public int hashCode() {
        return Objects.hash(serverID);
    }

Trieda obsahuje rovnaké premenné, ako trieda ServerStatusData + premenou lastUpdate, ktorá obsahuje informáciu o poslednej aktualizácii záznamov. Metódou update() aktualizujeme údaje. Metóda hasOldData() nám povie, či ak aktuálne inštancie obsahuje zastaraná dáta, alebo nie. Ak dáta budú zastaraná, odstránime záznam zo zoznamu nájdených serverov (zatiaľ nešpecifikovaným spôsobom). Bolo potrebné prepísať metódy equals() a hashCode(), aby sme správne vyhľadávali v HashMap.

Widget záznamu servera

Aby sme mohli zobraziť vlastné položku v ListView, je potrebné ju vytvoriť. Ja som už navrhol grafickú reprezentáciu položky. Nachádza sa v súbore connect/server_entry.fxml. My teraz vytvoríme "kontrolér", ktorý bude túto položku ovládať. Vytvoríme nový balík widget, ktorý umiestnime vedľa balíčkov model. V novo vytvorenom balíčku vytvoríme triedu ServerEntryCell, ktorá bude reprezentovať jednu položku v listview. Trieda bude dediť od všeobecnej triedy ListCell<ServerEntry> a bude typová na modelovú triedu ServerEntry:

package cz.stechy.chat.widget;

public class ServerEntryCell extends ListCell<ServerEntry> {

    private static final String FXML_PATH = "/fxml/connect/server_entry.fxml";
    private static final String ADDRESS_PORT_FORMAT = "%s:%d";

    @FXML
    private Label lblName;

    @FXML
    private Label lblClients;

    @FXML
    private Label lblAddress;

    private Parent container;

    public ServerEntryCell() {
        final FXMLLoader loader = new FXMLLoader(getClass().getResource(FXML_PATH));
        loader.setController(this);
        try {
            container = loader.load();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    @Override
    protected void updateItem(ServerEntry item, boolean empty) {
        super.updateItem(item, empty);

        if (empty) {
            setText(null);
            setGraphic(null);
            lblName.textProperty().unbind();
            lblClients.textProperty().unbind();
        } else {
            lblName.textProperty().bind(item.serverNameProperty());
            lblClients.textProperty().bind(item.clientsProperty());
            lblAddress.textProperty().set(String.format(ADDRESS_PORT_FORMAT, item.getServerAddress().getHostAddress(), item.getPort()));
            setGraphic(container);
        }
    }
}

Triedny konštanta FXML_PATH obsahuje cestu k view súboru. V konstruktoru triedy pomocou FXMLLoaderu načítame súbor a metódou setController() prepojíme view a našu triedu. Volanie tejto metódy je veľmi dôležité, pretože inak by sa nenaplnili premenné, ktoré sú oanotované anotácií @FXML. Nakoniec prepíšeme metódu updateItem(), ktorá sa zavolá zakaždým, keď je potreba aktualizovať položku v listview. V metóde najskôr musíme zavolať updateItem() na predkovi, aby sa správne nastavili premenné predka. Nasleduje náš kód. Ak položka neobsahuje žiadny záznam, vymažeme text a grafiku a odstránime binding na názov a počet klientov. Ak položka obsahuje záznam, tak naopak nabindujeme všetky informácie na príslušné kontrolky. Metódou setGraphic() zobrazíme naše kontrolky v jednom riadku v listview.

Služba pre správu lokálnych serverov

Vytvoríme si triedu, pomocou ktorej budeme spravovať nájdené servery v lokálnej sieti. Vedľa balíčkov widget a controller vytvoríme nový balíček s názvom service, v ktorom zadefinujeme novú triedu LocalServerService:

public final class LocalServerService implements OnServerFoundListener {

    private static final String BROADCAST_ADDRESS = "224.0.2.60";
    private static final int BROADCAST_PORT = 56489;

    // Mapa všech nalezených serverů
    private final ObservableMap<UUID, ServerEntry> serverMap = FXCollections.observableMap(new HashMap<>());

    private LanServerFinder serverFinder;

    public LocalServerService() {
        try {
            this.serverFinder = new LanServerFinder(InetAddress.getByName(BROADCAST_ADDRESS), BROADCAST_PORT);
            this.serverFinder.setServerFoundListener(this);
            ThreadPool.COMMON_EXECUTOR.submit(this.serverFinder);
        } catch (IOException e) {
            e.printStackTrace();
        }

    }

    @Override
    public void onServerFound(ServerStatusData data, InetAddress address) {
        ThreadPool.JAVAFX_EXECUTOR.execute(() -> {
            final UUID serverID = data.serverID;
            if (serverMap.containsKey(serverID)) {
                serverMap.get(serverID).update(data);
            } else {
                serverMap.put(serverID, new ServerEntry(data, address));
            }
        });
    }

    public ObservableMap<UUID, ServerEntry> getServerMap() {
        return FXCollections.unmodifiableObservableMap(serverMap);
    }

    public void stop() {
        serverFinder.shutdown();
    }

Trieda implementuje rozhranie OnServerFoundListener, ktoré sa nachádza v triede LanServerFinder. Pomocou tohto rozhrania budeme pridávať novo nájdené servery do mapy serverMap. Trieda ďalej obsahuje konštanty BROADCAST_ADDRESS a BROADCAST_PORT ktorých hodnoty sa musí zhodovať s hodnotami na serveri. Mapa serverMap je typu ObservableMap, čo nám umožní automatické aktualizácie výsledného zoznamu máp v GUI. V konstruktoru vytvoríme novú inštanciu triedy LanServerFinder a nastavíme listener. V triede sa ďalej nachádza getter našej pozorovateľné mapy. Všimnite si volanie metódy unmodifiableObservableMap(), ktorá zabezpečí, že mapu získanú pomocou Getter nebude možné zmeniť zvonku. Metódou stop() ukončíme činnosť LanServerFinder u. Rozhranie OnServerFoundListener nás núti implementovať metódu onServerFound(), ktorá je zavolaná zakaždým, keď nájdeme nový server. V metóde sa pozrieme, či ak už mapa obsahuje záznam. Ak záznam obsahuje, aktualizujú sa údaje, inak sa pridá záznam nový. To sa musí odohrať v JavaFX vlákne, pretože keď by sme zmenili mapu v inom vlákne, tak by došlo k vyhodenie výnimky s popisom: Not on FX application thread. Neskôr totiž prepojíme mapu serverov s grafickou komponentom a tú je možné meniť iba v JavaFX vlákne.

Prepojenie služby s kontrolerom

Konečne máme všetky komponenty hotové. Teraz ich všetky prepojíme dohromady. Budeme upravovať triedu ConnectController. Začneme pridaním rozhrania Initializable a OnCloseListener, ktorých metódy naimplementujeme za okamih.

Premenné lvServers nastavíme konkrétny dátový typ ServerEntry:

@FXML private ListView<ServerEntry> lvServers;

Ďalej vytvoríme novú inštančný konštantu serverService:

private final LocalServerService serverService = new LocalServerService();

V inicializačný metóde initialize() nastavíme továreň grafických komponentov pre servery a nastavíme listener na mapu serverov z našej service:

public void initialize(URL url, ResourceBundle resourceBundle) {
    lvServers.setCellFactory(param -> new ServerEntryCell());
    serverService.getServerMap().addListener(serverMapListener);
}

Premenná serverMapListener bude obsahovať konvertor mapy na list:

private MapChangeListener<? super UUID, ? super ServerEntry> serverMapListener = change -> {
    if (change.wasAdded()) {
        lvServers.getItems().addAll(change.getValueAdded());
    }

    if (change.wasRemoved()) {
        lvServers.getItems().removeAll(change.getValueRemoved());
    }
};

Nakoniec implementujeme metódu onClose(), ktorá sa zavolá pri zatvorení okna:

public void onClose() {
    serverService.stop();
}

Zobrazenie okna pre výber servera

V hlavnom kontroleru MainController pridáme reakciu na menu tlačidlo v metóde handleConnect(), kde zobrazíme okno so správou serverov:

@FXML
private void handleConnect(ActionEvent actionEvent) {
    try {
        showNewWindow("connect/connect", "Připojit k serveru...");
    } catch (IOException e) {
        e.printStackTrace();
    }
}

Využívame pomocnú metódu showNewWindow(), ktorá přijámá ako parameter cestu k fxml dokumentu a názov okna.

Ak ste všetko urobili správne, tak po spustení servera, klienta a zobrazenie okna pre výber servera by sa malo zobraziť nasledujúce okno s jedným nájdeným serverom:

Okno s nájdeným serverom v lokálnej sieti - Server pre klientskej aplikácie v Jave

V budúcej lekcii, Java chat - Klient - Spojenie so serverom 1. časť , vytvoríme triedu, ktorá bude držať spojenie so serverom a konečne sa na server prihlásime.


 

Mal si s čímkoľvek problém? Stiahni si vzorovú aplikáciu nižšie a porovnaj ju so svojím projektom, chybu tak ľahko nájdeš.

Stiahnuť

Stiahnutím nasledujúceho súboru súhlasíš s licenčnými podmienkami

Stiahnuté 15x (114.77 kB)
Aplikácia je vrátane zdrojových kódov v jazyku Java

 

Predchádzajúci článok
Java chat - Klient - Zoznámenie sa s kostrou aplikácie
Všetky články v sekcii
Server pre klientskej aplikácie v Jave
Preskočiť článok
(neodporúčame)
Java chat - Klient - Spojenie so serverom 1. časť
Článok pre vás napísal Petr Štechmüller
Avatar
Užívateľské hodnotenie:
Ešte nikto nehodnotil, buď prvý!
Autor se věnuje primárně programování v Javě, ale nebojí se ani webových technologií.
Aktivity