IT rekvalifikace s garancí práce. Seniorní programátoři vydělávají až 160 000 Kč/měsíc a rekvalifikace je prvním krokem. Zjisti, jak na to!
Hledáme nové posily do ITnetwork týmu. Podívej se na volné pozice a přidej se do nejagilnější firmy na trhu - Více informací.

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

 

Predchádzajúci článok
Java chat - Klient - Spojenie so serverom 3. časť
Všetky články v sekcii
Server pre klientskej aplikácie v Jave
Preskočiť článok
(neodporúčame)
Java chat - Klient - Chat service
Článok pre vás napísal Petr Štechmüller
Avatar
Užívateľské hodnotenie:
Ešte nikto nehodnotil, buď prvý!
Autor se věnuje primárně programování v Javě, ale nebojí se ani webových technologií.
Aktivity