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
LanServerFinder
u. 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