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í.

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í/od­hláš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

 

Předchozí článek
Kvíz - Spojení klienta se serverem v Javě
Všechny články v sekci
Server pro klientské aplikace v Javě
Přeskočit článek
(nedoporučujeme)
Java chat - Klient - Chat service
Článek pro vás napsal Petr Štechmüller
Avatar
Uživatelské hodnocení:
Ještě nikdo nehodnotil, buď první!
Autor se věnuje primárně programování v Javě, ale nebojí se ani webových technologií.
Aktivity