21. diel - Java chat - Klient - Chat service
V predchádzajúcom kvíze, Kvíz - Pluginy a zobrazenie lokálnych serverov v Jave, sme si overili nadobudnuté skúsenosti z predchádzajúcich lekcií.
V minulej lekcii, Kvíz - Pluginy a zobrazenie lokálnych serverov v Jave , sme vytvorili správu užívateľov na serveri. V dnešnom Java tutoriálu už začneme tvoriť základné stavebné kamene pre chat. Najskôr si vytvoríme triedu, pomocou ktorej budeme posielať správy týkajúce sa samotného chatu. Ďalej navrhneme a implementujeme rozhranie pre chat service, pomocou ktorej budeme pristupovať k funkciám chatu.
Správa pre chat
V module share
, v balíčku message
, založíme
novú triedu ChatMessage
, pomocou ktorej sa budú posielať všetky
správy, ktoré sa budú týkať chatu. Trieda je veľká, najprv si uvedieme
jej zdrojový kód a potom si ju vizualizuje na diagrame:
package cz.stechy.chat.net.message; public class ChatMessage implements IMessage { private static final long serialVersionUID = -7817515518938131863L; public static final String MESSAGE_TYPE = "chat"; private final IChatMessageData data; public ChatMessage(IChatMessageData data) { this.data = data; } @Override public String getType() { return MESSAGE_TYPE; } @Override public Object getData() { return data; } public interface IChatMessageData extends Serializable { ChatMessageDataType getDataType(); Object getData(); } public enum ChatMessageDataType { DATA_ADMINISTRATION, DATA_COMMUNICATION } public static final class ChatMessageAdministrationData implements IChatMessageData { private static final long serialVersionUID = 8237826895694688852L; private final IChatMessageAdministrationData data; public ChatMessageAdministrationData(IChatMessageAdministrationData data) { this.data = data; } @Override public ChatMessageDataType getDataType() { return ChatMessageDataType.DATA_ADMINISTRATION; } @Override public Object getData() { return data; } public enum ChatAction { CLIENT_REQUEST_CONNECT, // Požadavek na připojení k chatovací službě CLIENT_CONNECTED, CLIENT_DISCONNECTED, // Akce klientů CLIENT_TYPING, CLIENT_NOT_TYPING, // Informace o tom, zda-li někdo píše } public interface IChatMessageAdministrationData extends Serializable { ChatAction getAction(); } public static final class ChatMessageAdministrationClientRequestConnect implements IChatMessageAdministrationData { private static final long serialVersionUID = 642524654412490721L; private final String id; private final String name; public ChatMessageAdministrationClientRequestConnect(String id, String name) { this.id = id; this.name = name; } public String getId() { return id; } public String getName() { return name; } @Override public ChatAction getAction() { return ChatAction.CLIENT_REQUEST_CONNECT; } } public static final class ChatMessageAdministrationClientState implements IChatMessageAdministrationData { private static final long serialVersionUID = -6101992378764622660L; private final ChatAction action; private final String id; private final String name; public ChatMessageAdministrationClientState(ChatAction action, String id) { this(action, id, ""); } public ChatMessageAdministrationClientState(ChatAction action, String id, String name) { this.id = id; this.name = name; assert action == ChatAction.CLIENT_CONNECTED || action == ChatAction.CLIENT_DISCONNECTED; this.action = action; } public String getId() { return id; } public String getName() { return name; } @Override public ChatAction getAction() { return action; } } public static final class ChatMessageAdministrationClientTyping implements IChatMessageAdministrationData { private static final long serialVersionUID = 630432882631419944L; private final ChatAction action; private final String id; public ChatMessageAdministrationClientTyping(ChatAction action, String id) { assert action == ChatAction.CLIENT_TYPING || action == ChatAction.CLIENT_NOT_TYPING; this.action = action; this.id = id; } public String getId() { return id; } @Override public ChatAction getAction() { return action; } } } public static final class ChatMessageCommunicationData implements IChatMessageData { private static final long serialVersionUID = -2426630119019364058L; private final ChatMessageCommunicationDataContent data; public ChatMessageCommunicationData(String id, byte[] data) { this.data = new ChatMessageCommunicationDataContent(id, data); } @Override public ChatMessageDataType getDataType() { return ChatMessageDataType.DATA_COMMUNICATION; } @Override public Object getData() { return data; } public static final class ChatMessageCommunicationDataContent implements Serializable { private static final long serialVersionUID = -905319575968060192L; private final String destination; private final byte[] data; ChatMessageCommunicationDataContent(String destination, byte[] data) { this.destination = destination; this.data = data; } public String getDestination() { return destination; } public byte[] getData() { return data; } } } }
Triedu si pre jej veľkosť vizualizuje na obrázku:
ChatContact
Teraz založíme triedu, ktorá bude na klientovi reprezentovať jeden
kontakt. Triedu nazvime ChatContact
a umiestnime jej do balíčka
model
:
public class ChatContact { private final ObservableList <ChatMessageEntry> messages = FXCollections.observableArrayList(); private final StringProperty name = new SimpleStringProperty(this, "name", null); private final ObjectProperty <Color> contactColor = new SimpleObjectProperty<>(this, "contactColor", null); private final IntegerProperty unreadedMessages = new SimpleIntegerProperty(this, "unreadedMessages", 0); private final BooleanProperty typing = new SimpleBooleanProperty(this, "typing", false); private final String id; public ChatContact(String id, String name) { this.id = id; this.name.set(name); contactColor.set(Color.color(Math.random(), Math.random(), Math.random())); } public void addMessage(ChatContact chatContact, String message) { messages.add(new ChatMessageEntry(chatContact, message)); unreadedMessages.set(unreadedMessages.get() + 1); } public void resetUnreadedMessages() { unreadedMessages.set(0); } public void setTyping() { typing.set(true); } public void resetTyping() { typing.set(false); } public ObservableList <ChatMessageEntry> getMessages() { return messages; } @Override public String toString() { return getName(); } }
Inštančný konštanta messages
obsahuje kolekciu všetkých
správ, ktoré si užívateľ napísal s daným kontaktom. Zmysel ostatných
konštánt by mal byť odvoditeľný z ich názvu. BooleanProperty
typing
bude indikovať, či ak kontakt v aktuálnej chvíli píše
správu, alebo nie. Metódou addMessage()
pridáme novú správu do
kolekcie správ. Metódami setTyping()
a resetTyping()
budeme nastavovať, či ak kontakt píše, alebo nie. Ďalšie Getter a setter
tú nebudem vypisovať.
V triede sme použili doteraz nestanovenej triedu
ChatMessageEntry
, poďme ju pridať.
ChatMessageEntry
Táto trieda bude reprezentovať samotnú správu. Jej telo bude vyzerať takto:
package cz.stechy.chat.model; public final class ChatMessageEntry { private final ChatContact chatContact; private final String message; ChatMessageEntry(ChatContact chatContact, String message) { this.chatContact = chatContact; this.message = message; } public ChatContact getChatContact() { return chatContact; } public String getMessage() { return message; } }
Trieda obsahuje len dve vlastnosti: chatContact
a
message
.
ChatService
Teraz si vytvoríme rozhranie, ktoré bude definovať metódy pre chat.
public interface IChatService { void saveUserId(String id); void sendMessage(String id, String message); void notifyTyping(String id, boolean typing); ObservableMap <String, ChatContact> getClients(); }
Rozhranie definuje najzákladnejšie funkcie. Metóda
setUserId()
slúži na uloženie Id používateľa, ktorý sa
prihlásil k serveru. Metódou sendMessage()
budeme odosielať
správu. Metódou notifyTyping()
budeme oznamovať protiľahlej
strane, že sme začali písať. Metóda getClients()
vráti
pozorovateľnú mapu všetkých přilhášených užívateľov.
Implementácia rozhrania
Vedľa rozhrania vytvoríme triedu ChatService
, ktorá
implementuje vyššie spomínané rozhranie:
package cz.stechy.chat.service; public final class ChatService implements IChatService { private final ObservableMap <String, ChatContact> clients = FXCollections.observableHashMap(); private final List <String> typingInformations = new ArrayList<>(); private final IClientCommunicationService communicator; private String thisUserId; public ChatService(IClientCommunicationService communicator) { this.communicator = communicator; this.communicator.connectionStateProperty().addListener((observable, oldValue, newValue) -> { switch (newValue) { case CONNECTED: this.communicator.registerMessageObserver(ChatMessage.MESSAGE_TYPE, this.chatMessageListener); break; case CONNECTING: break; case DISCONNECTED: this.communicator.unregisterMessageObserver(ChatMessage.MESSAGE_TYPE, this.chatMessageListener); break; } }); } private ChatContact getContactById(String id) { return clients.get(id); } @Override public void saveUserId(String id) { this.thisUserId = id; } @Override public void sendMessage(String id, String message) { final ChatContact chatContact = clients.get(id); if (chatContact == null) { throw new RuntimeException("Klient nebyl nalezen."); } byte[] messageData = (message + " ").getBytes(); communicator.sendMessage( new ChatMessage( new ChatMessageCommunicationData(id, messageData))); chatContact.addMessage(clients.get(thisUserId), message); } @Override public void notifyTyping(String id, boolean typing) { if (typing && typingInformations.contains(id)) { return; } communicator.sendMessage(new ChatMessage( new ChatMessageAdministrationData( new ChatMessageAdministrationClientTyping( typing ? ChatAction.CLIENT_TYPING : ChatAction.CLIENT_NOT_TYPING, id)))); if (typing) { typingInformations.add(id); } else { typingInformations.remove(id); } } @Override public ObservableMap <String, ChatContact> getClients() { return clients; } private final OnDataReceivedListener chatMessageListener = message -> {}; }
Trieda obsahuje tri inštančné konštanty:
clients
- pozorovateľná mapa všetkých prihlásených užívateľovtypingInformations
- kolekcia užívateľov, ktorí práve píšu nejakú správucommunicator
- služba sprostredkujúce komunikáciu so serverom.
V konstruktoru získame komunikátor a jeho referenciu si uložíme. Ďalej
nastavíme listener na zmenu stavu pripojenia. Chceme totiž reagovať
na prichádzajúce správy iba v prípade, že sme pripojení. V metóde
sendMessage()
vytvoríme novú správu so zadaným obsahom a
pomocou komunikátora ju odošleme na server. Ďalej túto správu pridáme do
zoznamu "prijatých" správ. Metóda notifyTyping()
slúži na
informovanie, či ak sme my informovali užívateľa na druhej strane, že sme
začali / prestali písať. Využívame tu práve register
typingInformations
, aby sme neposielali správu zakaždým, keď
napíšeme znak. Metódou getClients()
vraciame pozorovateľnú
mapu všetkých prihlásených klientov. Nakoniec zostáva premenná
chatMessageListener
, ktorá obsahuje anonymný funkciu typu
OnDataReceivedListener()
. Túto funkciu teraz spolu vyplníme.
OnDataReceivedListener
Začneme tým, že přetypujeme prijatú správu na triedu
ChatMessage
a metódou getData()
získame rozhranie
IChatMessageData
:
final ChatMessage chatMessage = (ChatMessage) message; final IChatMessageData messageData = (IChatMessageData) chatMessage.getData();
Z premenné messageData
získame metódou
getDataType()
typ dát. Nad týmto typom dát urobíme
switch
, pomocou ktorého sa rozhodneme, ako dáta spracovať:
switch (messageData.getDataType()) { case DATA_ADMINISTRATION: break; case DATA_COMMUNICATION: break; default: throw new IllegalArgumentException("Neplatný parametr."); }
Metóda getDataType()
vráti jednu z dvoch hodnôt vo výpočte
ChatMessageDataType
. Pokiaľ sa bude jednať o administratívne
dáta, budeme spracovávať správy typu:
CLIENT_CONNECTED
/CLIENT_DISCONNECTED
CLIENT_TYPING
/CLIENT_NOT_TYPING
Ak prídu dáta typu DATA_COMMUNICATION
, vieme, že prišla
správa, ktorú treba zobraziť.
DATA_ADMINISTRATION
case DATA_ADMINISTRATION: final ChatMessageAdministrationData administrationData = (ChatMessageAdministrationData) messageData; final IChatMessageAdministrationData data = (IChatMessageAdministrationData) administrationData.getData(); switch (data.getAction()) { case CLIENT_CONNECTED: final ChatMessageAdministrationClientState messageAdministrationClientConnected = (ChatMessageAdministrationClientState) data; final String connectedClientID = messageAdministrationClientConnected.getId(); final String connectedClientName = messageAdministrationClientConnected.getName(); Platform.runLater(() -> clients.putIfAbsent(connectedClientID, new ChatContact(connectedClientID, connectedClientName))); break; case CLIENT_DISCONNECTED: final ChatMessageAdministrationClientState messageAdministrationClientDiconnected = (ChatMessageAdministrationClientState) data; final String disconnectedClientID = messageAdministrationClientDiconnected.getId(); Platform.runLater(() -> clients.remove(disconnectedClientID)); break; case CLIENT_TYPING: final ChatMessageAdministrationClientTyping messageAdministrationClientTyping = (ChatMessageAdministrationClientTyping) data; final String typingClientId = messageAdministrationClientTyping.getId(); final ChatContact typingClient = getContactById(typingClientId); Platform.runLater(typingClient::setTyping); break; case CLIENT_NOT_TYPING: final ChatMessageAdministrationClientTyping messageAdministrationClientNoTyping = (ChatMessageAdministrationClientTyping) data; final String noTypingClientId = messageAdministrationClientNoTyping.getId(); final ChatContact noTypingClient = getContactById(noTypingClientId); Platform.runLater(noTypingClient::resetTyping); break; default: throw new IllegalArgumentException("Neplatny argument."); } break;
Najskôr vytiahneme informácie o administratívnych dátach. Metódou
getAction()
získame akciu, ktorú správa predstavuje. Na základe
tejto akcie sa vo switch
aj rozhodneme, ako budeme správu
spracovávať. Väčšinu kódu zaberie rozbalení vlastných dát. Samotná
akcia, ktorá sa má vykonať, je potom volaná pomocou
Platform.runLater
.
Keď príde komunikačné správa, zobrazíme ju užívateľovi:
case DATA_COMMUNICATION: final ChatMessageCommunicationData communicationData = (ChatMessageCommunicationData) messageData; final ChatMessageCommunicationDataContent communicationDataContent = (ChatMessageCommunicationDataContent) communicationData.getData(); final String destination = communicationDataContent.getDestination(); final byte[] messageRaw = communicationDataContent.getData(); final String messageContent = new String(messageRaw, StandardCharsets.UTF_8); Platform.runLater(() -> { if (clients.containsKey(destination)) { final ChatContact chatContact = clients.get(destination); chatContact.addMessage(chatContact, messageContent); } }); break;
Všimnite si, že sa vôbec nestaráme, ako sa správa užívateľovi zobrazí. Iba pridáme novú správu vybranému kontaktu. O zvyšok sa postará iná vrstva.
To by bolo pre dnešné lekciu všetko. Nabudúce, v lekcii Java chat - Server - Chat plugin , sa
opäť presunieme na server a vytvoríme plugin, ktorý sa bude starať o
komunikáciu s našou ChatService
.
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é 15x (130.04 kB)
Aplikácia je vrátane zdrojových kódov v jazyku Java