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

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:

Diagram tried pre reprezentáciu správy chatu - Server pre klientskej aplikácie v Jave

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ľov
  • typingInformations - kolekcia užívateľov, ktorí práve píšu nejakú správu
  • communicator - 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.

OnDataReceived­Listener

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

 

Predchádzajúci článok
Kvíz - Pluginy a zobrazenie lokálnych serverov v Jave
Všetky články v sekcii
Server pre klientskej aplikácie v Jave
Preskočiť článok
(neodporúčame)
Java chat - Server - Chat plugin
Č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