20. diel - Java chat - Server - Správa užívateľov
V minulej lekcii, Java chat - Klient - Spojenie so serverom 3. časť , sme úspešne nadviazali spojenie klienta so serverom V dnešnom Java tutoriálu vytvoríme jednoduchú správu užívateľov. Pre jednoduchosť nebudeme uvažovať žiadne perzistentné úložisko, takže všetkých užívateľov, ktoré prihlásime do chatu, server zabudne v okamihu, keď sa vypne.
Auth plugin
Na serveri vytvoríme nový plugin, ktorý sa bude starať o prihlásenie používateľa do chatu. Správa užívateľov je v tomto prípade dosť nepresná, pretože užívateľa nebudeme registrovať, ani nejako zvlášť spravovať. Keď sa užívateľ bude pripájať na server, musí vyplniť políčko s prezývkou. Táto prezývka sa odošle po nadviazaní spojenia na server. Auth plugin bude mať za úlohu pridať túto prezývku do kolekcie prihlásených užívateľov. Ak prezývka bude existovať, odošle klientovi správu, aby si prezývku zmenil. Znie to jednoducho, tak poďme programovať.
Auth správa
Začneme tým, že vytvoríme triedu, ktorá bude reprezentovať Auth
správu. Triedu vytvoríme v module share
v balíčku
message
:
package cz.stechy.chat.net.message; public class AuthMessage implements IMessage { private static final long serialVersionUID = 2410714674227462122L; public static final String MESSAGE_TYPE = "auth"; private final AuthAction action; private final boolean success; private final AuthMessageData data; public AuthMessage(AuthAction action, AuthMessageData data) { this(action, true, data); } public AuthMessage(AuthAction action, boolean success, AuthMessageData data) { this.action = action; this.success = success; this.data = data; } @Override public String getType() { return MESSAGE_TYPE; } public AuthAction getAction() { return action; } @Override public Object getData() { return data; } @Override public boolean isSuccess() { return success; } public enum AuthAction { LOGIN, LOGOUT } public static final class AuthMessageData implements Serializable { private static final long serialVersionUID = -9036266648628886210L; public final String id; public final String name; public AuthMessageData() { this(""); } public AuthMessageData(String name) { this("", name); } public AuthMessageData(String id, String name) { this.id = id; this.name = name; } } }
Správa implementuje rozhranie IMessage
, aby mohla byť poslaná
pomocou nášho protokolu. Výpočet AuthAction
obsahuje typ akcie,
ktorú práve správa bude reprezentovať. Podľa typu bude správa mať
naplnené rôzne premenné. Trieda AuthMessageData
reprezentuje
samotné dáta. Pre jednoduchosť budeme uvažovať iba ID a meno
používateľa. Teoreticky by sme mohli aj ID odstrániť, ale to by bolo až
príliš jednoduché.
Kostra pluginu
V module server
vytvoríme v balíčku plugins
nový balík auth
, v ktorom budeme implementovať správu
užívateľov. Začneme samotnú triedou AuthPlugin
, ktorej kostra
je k dispozícii nižšie:
@Singleton public class AuthPlugin implements IPlugin { private static final String PLUGIN_NAME = "auth"; private void authMessageHandler(IEvent event) {} private void clientDisconnectedHandler(IEvent event) {} @Override public String getName() { return PLUGIN_NAME; } @Override public void init() { System.out.println("Inicializace pluginu: " + getName()); } @Override public void registerMessageHandlers(IEventBus eventBus) { eventBus.registerEventHandler(AuthMessage.MESSAGE_TYPE, this::authMessageHandler); eventBus.registerEventHandler(ClientDisconnectedEvent.EVENT_TYPE, this::clientDisconnectedHandler); } }
Ako je vidieť, trieda implementuje iba najnutnejšie metódy, ktoré
rozhranie IPlugin
vyžaduje. Ďalej som si dovolil rovno
zaregistrovať odber správ typu AuthMessage
a
ClientDisconnectedEvent
. Telo metód
authMessageHandler()
a clientDisconnectedHandler()
dopíšeme neskôr. Plugin pre istotu už teraz zaregistrujeme vo výpočte
Plugin
tak, že pridáme riadok:
AUTH(AuthPlugin.class)
Reprezentácie užívateľa
Na serveri budeme prihláseného užívateľa reprezentovať triedou
User
, ktorú vytvoríme v balíčku auth
:
package cz.stechy.chat.plugins.auth; public final class User { public final String id; public final String name; public User(String name) { this(UUID.randomUUID().toString(), name); } public User(String id, String name) { this.id = id; this.name = name; } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } User user = (User) o; return Objects.equals(id, user.id) && Objects.equals(name, user.name); } @Override public int hashCode() { return Objects.hash(id, name); } }
Užívateľ bude mať iba dve vlastnosti: id
a
name
. Ďalej som triede prekryl metódy equals()
a
hashCode()
, aby sme mohli v budúcnosti ľahko užívateľa
vyhľadať v kolekcii.
Prihlasovacie udalosti
Iste mi dáte za pravdu, že prihlásenie je veľká akcia, ktorá si
zaslúži generovať novú udalosť. Vytvoríme teda balík event
,
ktorý sa bude nachádzať vedľa triedy AuthPlugin
. V tomto
balíčku založíme dve nové triedy, ktoré budú reprezentovať prihlásenie
/ odhlásenie používateľa.
Trieda LoginEvent
:
package cz.stechy.chat.plugins.auth.event; public class LoginEvent implements IEvent { public static final String EVENT_TYPE = "login"; public final IClient client; public final User user; public LoginEvent(IClient client, User user) { this.client = client; this.user = user; } @Override public String getEventType() { return EVENT_TYPE; } }
A trieda LogoutEvent
:
package cz.stechy.chat.plugins.auth.event; public class LogoutEvent implements IEvent { public static final String EVENT_TYPE = "logout"; public final User user; public LogoutEvent(User user) { this.user = user; } @Override public String getEventType() { return EVENT_TYPE; } }
Auth service
Všetku logiku budeme popisovať pomocou rozhrania IAuthService
.
V balíčku auth
vytvoríme nový balík service
, v
ktorom založíme rozhraní IAuthService
a triedu národné
implementačné toto rozhranie AuthService
. Rozhranie bude
obsahovať metódu login()
, pomocou ktorej sa bude užívateľ
prihlasovať na server a metódu logout()
. Jednu budeme metódu
volať pri príjme správy typu logout a druhú budeme volať v
prípade, že klientovi náhle spadne spojenie.
package cz.stechy.chat.plugins.auth.service; @ImplementedBy(AuthService.class) public interface IAuthService { Optional<User> login(String username); Optional<User> logout(String id); Optional<User> logout(IClient client); }
Metóda login()
prijíma jediný parameter username
a referenciu na klienta. V praxi by sme vyžadovali ešte heslo, aby sme mohli
používateľa overiť. Všetky metódy vracia Optional
typizovanom
na triedu User
. Pokiaľ bude Optional
prázdny, akcie
zlyhala.
Implementácia Auth service
Do triedy AuthService
vložíme nasledujúci kód:
package cz.stechy.chat.plugins.auth.service; @Singleton class AuthService implements IAuthService { private final Map <IClient, User> users = new HashMap<>(); @Override public Optional<User> login(String username, IClient client) { final Optional<User> optionalUser = users.values().stream() .filter(user -> Objects.equals(username, user.name)) .findFirst(); if (optionalUser.isPresent()) { return Optional.empty(); } final User user = new User(username); users.put(client, user); return Optional.of(user); } @Override public Optional <User> logout(String id) { IClient client = null; for (Entry <IClient, User> userEntry: users.entrySet()) { if (Objects.equals(id, userEntry.getValue().id)) { client = userEntry.getKey(); break; } } if (client != null) { return logout(client); } return Optional.empty(); } @Override public Optional <User> logout(IClient client) { final User user = users.get(client); users.remove(client); return Optional.of(user); }
Trieda obsahuje inštančný konštantu users
, ktorá obsahuje
mapu prihlásených užívateľov. V metóde login()
najskôr
zistíme, či ak už používateľ so zadaným menom existuje. Ak takého
užívateľa nájdeme, vrátime metódou empty()
prázdny
výsledok. Tým budeme indikovať, že prihlásenie zlyhalo. Ak sa žiadny
takýto užívateľ nevyskytuje, vytvoríme nový záznam, ten uložíme do mapy
používateľov a nakoniec vrátime naplnený Optional
. Metódou
logout()
odstránime záznam užívateľa z mapy prihlásených
užívateľov. Tento záznam potom vrátime zabalený v triede
Optional
.
Spracovanie prijatej auth správy
Teraz doplníme telo metód authMessageHandler()
a
clientDisconnectedHandler()
v triede AuthPlugin
:
private void authMessageHandler(IEvent event) { assert event instanceof MessageReceivedEvent; final MessageReceivedEvent messageReceivedEvent = (MessageReceivedEvent) event; final AuthMessage authMessage = (AuthMessage) messageReceivedEvent.getReceivedMessage(); final AuthMessageData data = (AuthMessageData) authMessage.getData(); switch (authMessage.getAction()) { case LOGIN: final IClient client = messageReceivedEvent.getClient(); final Optional < User > optionalUser = authService.login(data.name, client); final boolean success = optionalUser.isPresent(); client.sendMessageAsync(authMessage.getResponce(success, success ? optionalUser.get().id : null)); if (success) { eventBus.publishEvent(new LoginEvent(client, optionalUser.get())); } break; case LOGOUT: authService.logout(data.id).ifPresent(user -> eventBus.publishEvent(new LogoutEvent(user))); break; default: throw new RuntimeException("Neplatný parametr"); } }
Na prvých riadkoch metódy "rozbaľujeme" prijaté dáta až na triedu
AuthMessageData
, ktorá obsahuje vlastné dáta. Nasleduje
switch
, pomocou ktorého sa rozhodneme, čo budeme robiť. Pokiaľ
bude akcie typu prihlásenie, zavoláme nad našou service metódu
login()
a odovzdáme jej parameter s prezývkou. Metóda vráti
prázdny Optional
v pripade, že používateľ už existuje, takže
prihlásenie zlyhá. V opačnom prípade odošle užívateľovi odpoveď s
informáciou, že prihlásenie prebehlo v poriadku. Do odpovede sa priloží id
užívateľa. Ak prihlásenie podarí, metódou publishEvent()
vyprodukujeme novú udalosť typu LoginEvent
. Vďaka tomu sa
pluginy, ktoré sú prihlásené na odber udalosti "prihlásenie" dozvie, že sa
prihlásil nový užívateľ. V prípade akcie odhlásenia zavoláme metódu
logout()
a odovzdáme jej parameter s id
užívateľa,
ktorého chceme odhlásiť. Pre odhlásenie opäť vygenerujeme novú udalosť,
aby mohli ostatní pluginy odstrániť prípadné alokovanej zdroje pre
odhláseného používateľa.
Keď obdržíme udalosť typu "klientovi spadlo spojenia", odhlásime daného klienta zo servera a vytvoríme novú udalosť. Tým uvoľníme prezývku na ďalšie použitie:
private void clientDisconnectedHandler(IEvent event) { final ClientDisconnectedEvent disconnectedEvent = (ClientDisconnectedEvent) event; final Client disconnectedClient = disconnectedEvent.getClient(); authService.logout(disconnectedClient).ifPresent(user -> eventBus.publishEvent(new LogoutEvent(user))); }
Tým by sme mali hotovú serverovú časť implementácie správy užívateľov. Teraz prejdeme na klienta.
Prihlásenie klienta
Prihlásenie si rovno otestujeme v klientovi. Presunieme sa teda to
kontroleru ConnectController
, v ktorom upravíme metódu
connect()
.
this.communicator.connect(host, port) .exceptionally(throwable -> { Alert alert = new Alert(AlertType.ERROR); alert.setHeaderText("Chyba"); alert.setContentText("Připojení k serveru se nezdařilo."); alert.showAndWait(); throw new RuntimeException(throwable); }) .thenCompose(ignored -> this.communicator.sendMessageFuture( new AuthMessage(AuthAction.LOGIN, new AuthMessageData(username))) .thenAcceptAsync(responce -> { if (!responce.isSuccess()) { Alert alert = new Alert(AlertType.ERROR); alert.setHeaderText("Chyba"); alert.setContentText("Připojení k serveru se nezdařilo."); alert.showAndWait(); this.communicator.disconnect(); } else { Alert alert = new Alert(AlertType.INFORMATION); alert.setHeaderText("Úspěch"); alert.setContentText("Přihlášení se zdařilo."); alert.showAndWait(); } }, ThreadPool.JAVAFX_EXECUTOR));
¨
V metóde sme upravili reakciu na úspešné nadviazanie spojenia. Teraz
miesto zobrazenie dialógu odošleme správu na server, že chceme prihlásiť
používateľa. S volaním metódy thenCompose()
sme sa už
stretli, ale pre istotu znovu zopakujem, čo sa stane. Táto metóda nám
dovolí zavolať inú "budúcnosť" a vráti jej výsledok. Týmto spôsobom sa
teda dá reťaziť volanie viac "budúcnosťou" za sebou. Po prijatí odpovede
sa pozrieme, či ak sme boli úspešní, alebo nie. V oboch prípadoch
zobrazíme dialóg s výsledkom, či ak sme sa prihlásili, alebo nie. Ak sme sa
neprihlásili, tak sa odpojíme od servera.
V budúcej lekcii, Java chat - Klient - Chat service , začneme implementovať funkcionalitu chatu
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é 17x (125.45 kB)
Aplikácia je vrátane zdrojových kódov v jazyku Java