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 22 - Java chat - Server - Chat plugin

V minulé lekci, Java chat - Klient - Chat service, jsme vytvořili základ chatu.

V dnešním Java tutoriálu si naimplementujeme chat plugin na serveru.

ChatPlugin

Na serveru založíme nový plugin, který bude mít na starosti funkce chatu. Začneme vytvořením nového balíčku chat v balíčku plugins. Vytvoříme třídu ChatPlugin:

package cz.stechy.chat.plugins.chat;

@Singleton
public class ChatPlugin implements IPlugin {

    public static final String PLUGIN_NAME = "chat";

    private void loginEventHandler(IEvent event) {}
    private void logoutEventHandler(IEvent event) {}
    private void chatMessageHandler(IEvent event) {}

    @Override
    public String getName() {
        return PLUGIN_NAME;
    }

    @Override
    public void init() {
        System.out.println("Inicializuji chat plugin.");
    }

    @Override
        public void registerMessageHandlers(IEventBus eventBus) {
        eventBus.registerEventHandler(LoginEvent.EVENT_TYPE, this::loginEventHandler);
        eventBus.registerEventHandler(LogoutEvent.EVENT_TYPE, this::logoutEventHandler);
        eventBus.registerEventHandler(ChatMessage.MESSAGE_TYPE, this::chatMessageHandler);
    }
}

Třída implementuje standardní rozhraní IPlugin. V metodě registerMessageHandlers() registrujeme tři handlery:

  • LoginEvent.EVENT_TYPE - reakce na přihlášení uživatele
  • LogoutEvent.EVENT_TYPE - reakce na odhlášení uživatele
  • ChatMessage.MESSAGE_TYPE - reakce na samotnou chat zprávu

Těla handlerů vyplníme, až budeme mít implementovanou service. Plugin rovnou zaregistrujeme ve výčtu Plugin:

CHAT(ChatPlugin.class);

ChatService

O veškerou logiku se bude starat třída ChatService. Založíme tedy nový balíček service, ve kterém vytvoříme rozhraní IChatService a třídu ChatService implementující toto rozhraní:

package cz.stechy.chat.plugins.chat.service;

@ImplementedBy(ChatService.class)
public interface IChatService {
    void addClient(IClient client, String id, String name);
    void removeClient(String id);
    void sendMessage(String destinationClientId, String sourceClientId, byte[] rawMessage);
    Optional <String> findIdByClient(IClient client);
    void informClientIsTyping(String destinationClientId, String sourceClientId, boolean typing);
}

Metody addClient()/removeClient() budou spravovat záznamy klientů v chatu. Můžete se ptát, proč to dělat takto složitě, když bychom mohli využít AuthService, která již záznamy o klientech obsahuje. Je důležité si uvědomit, že procesy připojení a přihlášení uživatele jsou na sobě nezávislé. Mohla by tedy nastat situace, že připojený uživatel se nebude chtít přihlásit. Metoda sendMessage() přijímá v parametrech destinationClientId a sourceClientId. Tyto parametry reprezentují ID cílového a zdrojového klienta, aby server poznal, od koho je zpráva poslaná a komu zpráva náleží. Metodou informClientIsTyping() budeme informovat cílového klienta, že klient začal/přestal psát.

Implementace rozhraní

Nyní postupně naimplementujeme všechny metody. Třída ChatService() implementuje rozhraní IChatService:

@Singleton
class ChatService implements IChatService {

}

V této třídě vytvoříme soukromou statickou vnitřní třídu ChatClient, která bude sloužit pouze jako přepravka. Třída bude obsahovat instanci rozhraní IClient a jméno klienta:

private static final class ChatClient {
    final IClient client;
    final String name;

    private ChatClient(IClient client, String name) {
        this.client = client;
        this.name = name;
    }
}

Ve třídě ChatService vytvoříme třídní konstantu, která bude obsahovat mapu všech klientů, kteří budou v chatu:

private final Map<String, ChatClient> clients = new HashMap<>();

Dále si vytvoříme pomocnou soukromou metodu, pomocí které budeme rozesílat zprávy všem připojeným klientům:

private void broadcastMessage(IMessage message) {
    clients.values().forEach(chatClient -> chatClient.client.sendMessageAsync(message));
}

Začneme metodou addClient():

@Override
public synchronized void addClient(IClient client, String id, String name) {
    final ChatClient chatClient = new ChatClient(client, name);
    clients.forEach((clientId, entry) ->
        client.sendMessageAsync(new ChatMessage(
            new ChatMessageAdministrationData(
                new ChatMessageAdministrationClientState(
                    ChatAction.CLIENT_CONNECTED, clientId, entry.name)))));
    clients.put(id, chatClient);
    broadcastMessage(new ChatMessage(
        new ChatMessageAdministrationData(
            new ChatMessageAdministrationClientState(
                ChatAction.CLIENT_CONNECTED, id, name))));
}

Když budeme přidávat nového klienta do mapy, tak nejdříve tomuto klientovi odešleme seznam všech připojených klientů. Teprve potom ho přidáme do naší kolekce. Na závěr odešleme všem připojeným klientům (i tomu novému), že se připojil nový klient. Tímto trikem si bude moci každý uživatel psát sám se sebou.

Metoda pro odebrání klienta removeClient() bude následující:

@Override
public synchronized void removeClient(String id) {
    clients.remove(id);
    broadcastMessage(new ChatMessage(
        new ChatMessageAdministrationData(
            new ChatMessageAdministrationClientState(
                ChatAction.CLIENT_DISCONNECTED, id))));
}

Nejdříve odebereme klienta z mapy. Pak odešleme zprávu ostatním klientům, že se někdo odhlásil.

Metoda pro odeslání zprávy bude velmi jednoduchá:

@Override
public void sendMessage(String destinationClientId, String sourceClientId, byte[] rawMessage) {
    clients.get(destinationClientId).client.sendMessageAsync(new ChatMessage(new ChatMessageCommunicationData(sourceClientId, rawMessage)));
}

Z mapy klientů vezme cílového klienta podle jeho ID a odešle mu zprávu, kterou dostane jako parametr.

Předposlední metoda, kterou musíme implementovat, je metoda informClientIsTyping():

@Override
public void informClientIsTyping(String destinationClientId, String sourceClientId, boolean typing) {
    clients.get(destinationClientId).client.sendMessageAsync(
        new ChatMessage(
            new ChatMessageAdministrationData(
                new ChatMessageAdministrationClientTyping(
                    typing ? ChatAction.CLIENT_TYPING : ChatAction.CLIENT_NOT_TYPING, sourceClientId
    ))));
}

Opět získáme z mapy klientů cílového klienta, kterému pošleme zprávu s informací, zda-li klient píše, nebo přestal psát.

Poslední metoda, kterou implementujeme, je metoda findIdByClient(). Pomocí této metody budeme hledat ID klienta, na základě instance rozhraní IClient:

@Override
public Optional <String> findIdByClient(IClient client) {
    final Optional <Entry <String, ChatClient>> entryOptional = clients.entrySet()
        .stream()
        .filter(entry -> entry.getValue().client == client)
        .findFirst();

    return entryOptional.map(Entry::getKey);
}

Ve filtru porovnáváme pomocí ==. To si můžeme dovolit, protože máme jistotu, že taková instance se vyskytuje.

Dokončení pluginu

Nyní se vrátíme ke třídě ChatPlugin, ve které doplníme těla metod. Než se do toho pustíme, vytvoříme novou instanční konstantu typu IChatService a necháme si ji předat v konstruktoru:

private final IChatService chatService;

@Inject
public ChatPlugin(IChatService chatService) {
    this.chatService = chatService;
}

Těla metod loginEventHandler() a logoutEventHandler() budou přidávat/odebírat klienty z chatService:

private void loginEventHandler(IEvent event) {
    final LoginEvent loginEvent = (LoginEvent) event;
    chatService.addClient(loginEvent.client, loginEvent.user.id, loginEvent.user.name);
}

private void logoutEventHandler(IEvent event) {
    final LogoutEvent logoutEvent = (LogoutEvent) event;
    chatService.removeClient(logoutEvent.user.id);
}

Metodu chatMessageHandler() si rozepíšeme podrobněji. Nejdříve získáme data ze zprávy:

final MessageReceivedEvent messageReceivedEvent = (MessageReceivedEvent) event;
final IClient client = messageReceivedEvent.getClient();
final ChatMessage chatMessage = (ChatMessage) messageReceivedEvent.getReceivedMessage();
final IChatMessageData chatMessageData = (IChatMessageData) chatMessage.getData();

Následuje velmi podobný rozhodovací proces, jako tomu bylo na straně klienta. Opět získáme typ zprávy metodou getDataType(). Pokud bude zpráva administrativní, získáme z ní potřebná data a rozhodneme se, jaká akce se má stát. Pokud půjde o odeslání zprávy, odešleme zprávu správnému příjemci. Celý rozhodovací proces je níže:

switch (chatMessageData.getDataType()) {
    case DATA_ADMINISTRATION:
        IChatMessageAdministrationData administrationData = (IChatMessageAdministrationData) chatMessageData.getData();
        switch (administrationData.getAction()) {
            case CLIENT_REQUEST_CONNECT:
            final ChatMessageAdministrationClientRequestConnect clientRequestConnect = (ChatMessageAdministrationClientRequestConnect) administrationData;
            final String clientId = clientRequestConnect.getId();
            final String clientName = clientRequestConnect.getName();
            chatService.addClient(client, clientId, clientName);
            break;
        case CLIENT_DISCONNECTED:
            final ChatMessageAdministrationClientState clientDisconnected = (ChatMessageAdministrationClientState) administrationData;
            final String disconnectedClientId = clientDisconnected.getId();
            chatService.removeClient(disconnectedClientId);
            break;
        case CLIENT_TYPING:
            final ChatMessageAdministrationClientTyping clientIsTyping = (ChatMessageAdministrationClientTyping) administrationData;
            final String typingClientId = clientIsTyping.getId();
            chatService.informClientIsTyping(typingClientId, chatService.findIdByClient(client).orElse(""), true);
            break;
        case CLIENT_NOT_TYPING:
            final ChatMessageAdministrationClientTyping clientIsNotTyping = (ChatMessageAdministrationClientTyping) administrationData;
            final String notTypingClientId = clientIsNotTyping.getId();
            chatService.informClientIsTyping(notTypingClientId, chatService.findIdByClient(client).orElse(""), false);
            break;
        default:
            throw new IllegalArgumentException("Neplatný argument. " + administrationData.getAction());
        }
        break;
    case DATA_COMMUNICATION:
        final ChatMessageCommunicationDataContent communicationDataContent = (ChatMessageCommunicationDataContent) chatMessageData.getData();
        final String destinationClientId = communicationDataContent.getDestination();
        final String sourceClientId = chatService.findIdByClient(client).orElse("");
        final byte[] rawMessage = communicationDataContent.getData();
        chatService.sendMessage(destinationClientId, sourceClientId, rawMessage);
        break;
    default:
        throw new IllegalArgumentException("Neplatný argument." + chatMessageData.getDataType());
}

Tím bychom měli kompletní funkcionalitu serveru.

V následujícím kvízu, Kvíz - Správa uživatelů, chat service a plugin v Javě, si vyzkoušíme nabyté zkušenosti z předchozích lekcí.


 

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 14x (133.94 kB)
Aplikace je včetně zdrojových kódů v jazyce Java

 

Předchozí článek
Java chat - Klient - Chat service
Všechny články v sekci
Server pro klientské aplikace v Javě
Přeskočit článek
(nedoporučujeme)
Kvíz - Správa uživatelů, chat service a plugin v Javě
Č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