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živateleLogoutEvent.EVENT_TYPE
- reakce na odhlášení uživateleChatMessage.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 15x (133.94 kB)
Aplikace je včetně zdrojových kódů v jazyce Java