Lekce 16 - Java chat - Klient - Zobrazení lokálních serverů
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í FXMLLoader 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:

V následujícím kvízu, Kvíz - Pluginy a zobrazení lokálních serverů v Javě, si vyzkoušíme nabyté zkušenosti z předchozích lekcí.
Měl jsi s čímkoli problém? Stáhni si vzorovou aplikaci níže a porovnej ji se svým projektem, chybu tak snadno najdeš.
Stáhnout
Stažením následujícího souboru souhlasíš s licenčními podmínkami
Staženo 17x (114.77 kB)
Aplikace je včetně zdrojových kódů v jazyce Java
