Vánoční nadílka Vánoční nadílka
Vánoční akce! Daruj lepší budoucnost blízkým nebo sobě. Až +50 % zdarma na dárkové poukazy. Více informací

Lekce 16 - Java chat - Klient - Zobrazení lokálních serverů

Java Server pro klientské aplikace Java chat - Klient - Zobrazení lokálních serverů

Unicorn College ONEbit hosting Tento obsah je dostupný zdarma v rámci projektu IT lidem. Vydávání, hosting a aktualizace umožňují jeho sponzoři.

V minulé lekci Java chat - Klient - Seznámení se s kostrou aplikace, jsme se seznámili s kostrou klienta. Dnes vytvoříme implementaci okna pro správu serveru. Čeká nás tedy zobrazení nalezených serverů v lokální síti.

Upravení třídy LanServerFinder

Než se pustíme do implementace klientských částí, musíme nejdříve lehce upravit rozhraní atributu typu OnServerFoundListener ve třídě LanServerFinder. Metodě onServerFound() přidáme ještě parametr InetAddress address, který bude obsahovat adresu serveru, odkud datagram přišel:

void onServerFound(ServerStatusData data, InetAddress address);

Dále musíme opravit volání této metody:

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

Zdrojovou adresu získáme voláním metody getAddress() nad přijatým datagramem.

Zobrazení lokálních serverů

Okno pro zobrazení serverů již máme vytvořené. Veškeré nalezené servery budeme zobrazovat v listView. Budeme zobrazovat tři informace o serveru:

  • název serveru
  • adresu a port serveru
  • obsazenost serveru

Model záznamu serveru

Vytvoříme třídu ServerEntry, kterou vložíme do balíčku model. Tato třída bude obsahovat výše uvedené informace.

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

Třída obsahuje stejné proměnné, jako třída ServerStatusData + proměnou lastUpdate, která obsahuje informaci o poslední aktualizaci záznamů. Metodou update() aktualizujeme údaje. Metoda hasOldData() nám řekne, zda-li aktuální instance obsahuje zastaralá data, či nikoliv. Pokud data budou zastaralá, odstraníme záznam ze seznamu nalezených serverů (zatím nespecifikovaným způsobem). Bylo potřeba přepsat metody equals() a hashCode(), abychom správně vyhledávali v HashMapě.

Widget záznamu serveru

Abychom mohli zobrazit vlastní položku v ListView, je potřeba ji vytvořit. Já jsem již navrhl grafickou reprezentaci položky. Nachází se v souboru connect/server_entry.fxml. My nyní vytvoříme "kontroler", který bude tuto položku ovládat. Vytvoříme nový balíček widget, který umístíme vedle balíčků model. V nově vytvořeném balíčku vytvoříme třídu ServerEntryCell, která bude reprezentovat jednu položku v listView. Třída bude dědit od obecné třídy ListCell<ServerEntry> a bude typovaná na modelovou třídu 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);
        }
    }
}

Třídní konstanta FXML_PATH obsahuje cestu k view souboru. V konstruktoru třídy pomocí FXMLLoaderu načteme soubor a metodou setController() propojíme view a naši třídu. Volání této metody je velmi důležité, protože jinak by se nenaplnily proměnné, které jsou oanotované anotací @FXML. Nakonec přepíšeme metodu updateItem(), která se zavolá pokaždé, když je potřeba aktualizovat položku v listView. V metodě nejdříve musíme zavolat updateItem() na předkovi, aby se správně nastavily proměnné předka. Následuje náš kód. Pokud položka neobsahuje žádný záznam, vymažeme text a grafiku a odstraníme bindingy na název a počet klientů. Pokud položka obsahuje záznam, tak naopak nabindujeme všechny informace na příslušné kontrolky. Metodou setGraphic() zobrazíme naše kontrolky v jedné řádce v listView.

Služba pro správu lokálních serverů

Vytvoříme si třídu, pomocí které budeme spravovat nalezené servery v lokální síti. Vedle balíčků widget a controller vytvoříme nový balíček s názvem service, ve kterém nadefinujeme novou třídu 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();
    }

Třída implementuje rozhraní OnServerFoundListener, které se nachází ve třídě LanServerFinder. Pomocí tohoto rozhraní budeme přidávat nově nalezené servery do mapy serverMap. Třída dále obsahuje konstanty BROADCAST_ADDRESS a BROADCAST_PORT jejichž hodnoty se musí shodovat s hodnotami na serveru. Mapa serverMap je typu ObservableMap, což nám umožní automatické aktualizace výsledného seznamu map v GUI. V konstruktoru vytvoříme novou instanci třídy LanServerFinder a nastavíme listener. Ve třídě se dále nachází getter naší pozorovatelné mapy. Všimněte si volání metody unmodifiableObservableMap(), která zajistí, že mapu získanou pomocí getteru nebude možné změnit zvenčí. Metodou stop() ukončíme činnost LanServerFinderu. Rozhraní OnServerFoundListener nás nutí implementovat metodu onServerFound(), která je zavolána pokaždé, když nalezneme nový server. V metodě se podíváme, zda-li již mapa obsahuje záznam. Pokud záznam obsahuje, aktualizují se údaje, jinak se přidá záznam nový. To se musí odehrát v JavaFX vlákně, protože když bychom změnili mapu v jiném vlákně, tak by došlo k vyhození vyjímky s popisem: Not on FX application thread. Později totiž propojíme mapu serverů s grafickou komponentou a tu lze měnit pouze v JavaFX vlákně.

Propojení služby s kontrolerem

Konečně máme všechny komponenty hotové. Nyní je všechny propojíme dohromady. Budeme upravovat třídu ConnectController. Začneme přidáním rozhraní Initializable a OnCloseListener, jejichž metody naimplementujeme za okamžik.

Proměnné lvServers nastavíme konkrétní datový typ ServerEntry:

@FXML private ListView<ServerEntry> lvServers;

Dále vytvoříme novou instanční konstantu serverService:

private final LocalServerService serverService = new LocalServerService();

V inicializační metodě initialize() nastavíme továrnu grafických komponent pro servery a nastavíme listener na mapu serverů z naší service:

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

Proměnná serverMapListener bude obsahovat 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());
    }
};

Nakonec implementujeme metodu onClose(), která se zavolá při zavření okna:

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

Zobrazení okna pro výběr serveru

V hlavním kontroleru MainController přidáme reakci na menu tlačítko v metodě handleConnect(), kde zobrazíme okno se správou serverů:

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

Využíváme pomocnou metodu showNewWindow(), která přijámá jako parametr cestu k fxml dokumentu a název okna.

Pokud jste vše udělali správně, tak po spuštění serveru, klienta a zobrazení okna pro výběr serveru by se mělo zobrazit následující okno s jedním nalezeným serverem:

Okno s nalezeným serverem v lokální síti

V příští lekci, Java chat - Klient - Spojení se serverem 1. část, vytvoříme třídu, která bude držet spojení se serverem a konečně se na server přihlásíme.


 

Stáhnout

Staženo 3x (116.82 kB)
Aplikace je včetně zdrojových kódů v jazyce Java

 

 

Článek pro vás napsal Petr Štechmüller
Avatar
Jak se ti líbí článek?
Ještě nikdo nehodnotil, buď první!
Autor se věnuje primárně programování v Jave, ale nebojí se ani webových technologií.
Aktivity (2)

 

 

Komentáře

Děláme co je v našich silách, aby byly zdejší diskuze co nejkvalitnější. Proto do nich také mohou přispívat pouze registrovaní členové. Pro zapojení do diskuze se přihlas. Pokud ještě nemáš účet, zaregistruj se, je to zdarma.

Zatím nikdo nevložil komentář - buď první!