Lekce 20 - Java chat - Server - Správa uživatelů
V předchozím kvízu, Kvíz - Spojení klienta se serverem v Javě, jsme si ověřili nabyté zkušenosti z předchozích lekcí.
V dnešním Java tutoriálu vytvoříme jednoduchou správu uživatelů. Pro jednoduchost nebudeme uvažovat žádné perzistentní úložiště, takže veškeré uživatele, které přihlásíme do chatu, server zapomene v okamžiku, kdy se vypne.
Auth plugin
Na serveru vytvoříme nový plugin, který se bude starat o přihlášení uživatele do chatu. Správa uživatelů je v tomto případě dost nepřesná, protože uživatele nebudeme registrovat, ani nějak zvlášť spravovat. Když se uživatel bude připojovat na server, musí vyplnit políčko s přezdívkou. Tato přezdívka se odešle po navázání spojení na server. Auth plugin bude mít za úkol přidat tuto přezdívku do kolekce přihlášených uživatelů. Pokud přezdívka bude existovat, odešle klientovi zprávu, aby si přezdívku změnil. Zní to jednoduše, tak pojďme programovat.
Auth zpráva
Začneme tím, že vytvoříme třídu, která bude reprezentovat Auth
zprávu. Třídu vytvoříme v modulu 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; } } }
Zpráva implementuje rozhraní IMessage
, aby mohla být poslána
pomocí našeho protokolu. Výčet AuthAction
obsahuje typ akce,
kterou zrovna zpráva bude reprezentovat. Podle typu bude zpráva mít
naplněné různé proměnné. Třída AuthMessageData
reprezentuje
samotná data. Pro jednoduchost budeme uvažovat pouze ID a jméno uživatele.
Teoreticky bychom mohli i ID odstranit, ale to by bylo až příliš
jednoduché.
Kostra pluginu
V modulu server
vytvoříme v balíčku plugins
nový balíček auth
, ve kterém budeme implementovat správu
uživatelů. Začneme samotnou třídou AuthPlugin
, jejíž kostra
je k dispozici níže:
@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); } }
Jak je vidět, třída implementuje pouze nejnutnější metody, které
rozhraní IPlugin
vyžaduje. Dále jsem si dovolil rovnou
zaregistrovat odběr zpráv typu AuthMessage
a
ClientDisconnectedEvent
. Tělo metod
authMessageHandler()
a clientDisconnectedHandler()
dopíšeme později. Plugin pro jistotu již teď zaregistrujeme ve výčtu
Plugin
tak, že přidáme řádek:
AUTH(AuthPlugin.class)
Reprezentace uživatele
Na serveru budeme přihlášeného uživatele reprezentovat třídou
User
, kterou vytvoří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živatel bude mít pouze dvě vlastnosti: id
a
name
. Dále jsem třídě překryl metody equals()
a
hashCode()
, abychom mohli v budoucnu snadno uživatele vyhledat v
kolekci.
Přihlašovací události
Jistě mi dáte za pravdu, že přihlášení je velká akce, která si
zaslouží generovat novou událost. Vytvoříme tedy balíček
event
, který se bude nacházet vedle třídy
AuthPlugin
. V tomto balíčku založíme dvě nové třídy, které
budou reprezentovat přihlášení/odhlášení uživatele.
Třída 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 třída 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
Veškerou logiku budeme popisovat pomocí rozhraní
IAuthService
. V balíčku auth
vytvoříme nový
balíček service
, ve kterém založíme rozhraní
IAuthService
a třídu implementující toto rozhraní
AuthService
. Rozhraní bude obsahovat metodu login()
,
pomocí které se bude uživatel přihlašovat na server a metodu
logout()
. Metody logout budou 2. Jednu budeme volat při příjmu
zprávy typu logout a druhou v případě, že klientovi náhle
spadně spojení.
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); }
Metoda login()
přijímá jediný parametr username
a referenci na klienta. V praxi bychom vyžadovali ještě heslo, abychom mohli
uživatele ověřit. Všechny metody vrací Optional
typovaný na
třídu User
. Pokud bude Optional
prázdný, akce se
nezdařila.
Implementace Auth service
Do třídy AuthService
vložíme následující 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); }
Třída obsahuje instanční konstantu users
, která obsahuje
mapu přihlášených uživatelů. V metodě login()
nejdříve
zjistíme, zda-li již uživatel se zadaným jménem existuje. Pokud takového
uživatele nalezneme, vrátíme metodou empty()
prázdný
výsledek. Tím budeme indikovat, že přihlášení selhalo. Pokud se žádný
takový uživatel nevyskytuje, vytvoříme nový záznam, ten uložíme do mapy
uživatelů a nakonec vrátíme naplněný Optional
. Metodou
logout()
odstraníme záznam uživatele z mapy přihlášených
uživatelů. Tento záznam pak vrátíme zabalený ve třídě
Optional
.
Zpracování přijaté auth zprávy
Nyní doplníme tělo metod authMessageHandler()
a
clientDisconnectedHandler()
ve třídě
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 prvních řádcích metody "rozbalujeme" přijatá data až na třídu
AuthMessageData
, která obsahuje vlastní data. Následuje
switch
, pomocí kterého se rozhodneme, co budeme dělat. Pokud
bude akce typu přihlášení, zavoláme nad naší service metodu
login()
a předáme jí parametr s přezdívkou. Metoda vrátí
prázdný Optional
v připadě, že uživatel již existuje, takže
přihlášení selže. V opačném případě odešle uživateli odpověď s
informací, že přihlášení proběhlo v pořádku. Do odpovědi se přiloží
id uživatele. Pokud se přihlášení podaří, metodou
publishEvent()
vyprodukujeme novou událost typu
LoginEvent
. Díky tomu se pluginy, které jsou přihlášené k
odběru události "přihlášení" dozví, že se přihlásil nový uživatel. V
případě akce odhlášení zavoláme metodu logout()
a předáme
jí parametr s id
uživatele, kterého chceme odhlásit. Pro
odhlášení opět vygenerujeme novou událost, aby mohly ostatní pluginy
odstranit případné alokované zdroje pro odhlášeného uživatele.
Když obdržíme událost typu "klientovi spadlo spojení", odhlásíme daného klienta ze serveru a vytvoříme novou událost. Tím uvolníme přezdívku k dalšímu použití:
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 bychom měli hotovou serverovou část implementace správy uživatelů. Nyní přejdeme na klienta.
Přihlášení klienta
Přihlášení si rovnou otestujeme v klientovi. Přesuneme se tedy to
kontroleru ConnectController
, ve kterém upravíme metodu
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 metodě jsme upravili reakci na úspěšné navázání spojení. Nyní
místo zobrazení dialogu odešleme zprávu na server, že chceme přihlásit
uživatele. S voláním metody thenCompose()
jsme se již setkali,
ale pro jistotu znovu zopakuji, co se stane. Tato metoda nám dovolí zavolat
jinou "budoucnost" a vrátí její výsledek. Tímto způsobem se tedy dá
řetězit volání více "budoucností" za sebou. Po přijetí odpovědi se
podíváme, zda-li jsme byli úspěšní, či nikoliv. V obou případech
zobrazíme dialog s výsledkem, zda-li jsme se přihlásili, či ne. Pokud jsme
se nepřihlásili, tak se odpojíme od serveru.
V příští lekci, Java chat - Klient - Chat service, začneme implementovat funkcionalitu chatu .
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 16x (125.45 kB)
Aplikace je včetně zdrojových kódů v jazyce Java