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:
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é 16x (114.77 kB)
Aplikácia je vrátane zdrojových kódov v jazyku Java