Package com.javachat.server

Source Code of com.javachat.server.ChatServer

package com.javachat.server;

import com.javachat.shared.Command;
import com.javachat.shared.LargeMessageException;
import com.javachat.shared.Logger;
import com.javachat.shared.MessageHolder;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.text.DecimalFormat;
import java.util.*;

/**
* Java chat server
*
* @author Dmitry Levykin
*/
public class ChatServer {
    // Кодировка сообщений
    public static final String CHARSET_NAME = "UTF-8";
    // Максимальный размер одного сообщения 1 Кб
    private static final int MESSAGE_SIZE_LIMIT = 1024;
    // Максимальный размер одного сообщения 1 Кб
    private static final int MESSAGE_COUNT_LIMIT = 100;
    // Порт
    private static final int PORT = 3000;

    // Буфер приема 10 Кб
    private ByteBuffer buffer = ByteBuffer.allocate(10240);
    // Клиенты
    private final Map<SelectionKey, ClientUser> clientUserMap = new LinkedHashMap<>();
    // Сообщения авторизованных клиентов
    private final List<ChatMessage> clientMessages = new LinkedList<>();
    // Счетчик входящих сообщений и команд
    private int incomeMessagesOrCommandsCount = 0;

    private boolean start = false;
    private Selector selector;
    private ServerSocketChannel serverSocketChannel;

    public static void main(String[] args) throws IOException, InterruptedException {
        Logger.info("Java chat server.");

        // DEBUG OFF
        Logger.setDebugMode(false);

        ChatServer chatServer = new ChatServer();
        int port = PORT;
        if (args != null && args.length == 2 && args[0].equals("-p")) {
            port = Integer.parseInt(args[1]);
        }
        chatServer.bind(port);
        Logger.info("Listen port %d.", port);
        chatServer.start();
    }

    /**
     * Запуск серверного сокета
     */
    public void bind(int port) throws IOException {
        selector = Selector.open();
        serverSocketChannel = ServerSocketChannel.open();
        serverSocketChannel.socket().bind(new java.net.InetSocketAddress(port));
        serverSocketChannel.configureBlocking(false);
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
    }

    /**
     * Установка размера буфера сообщений
     */
    public void setBufferCapacity(int bufferCapacity) {
        Logger.debug("Set buffer capacity %d", bufferCapacity);
        buffer = ByteBuffer.allocate(bufferCapacity);
    }

    /**
     * Запуск основного цикла сервера
     */
    void start() throws IOException, InterruptedException {
        if (start) {
            return;
        }
        start = true;
        while (start) {
            selector.selectNow();
            if (!keyIteration()) {
                // Нет активности
                Thread.sleep(20);
            }
        }
    }

    int select() throws IOException {
        return selector.select();
    }

    /**
     * Итерация основного цикла
     *
     * @return true - была полезная работа (подключение/запись/чтение полные/неполные)
     */
    boolean keyIteration() {
        Set<SelectionKey> keySet = selector.selectedKeys();
        int size = keySet.size();
        if (size == 0) {
            return false;
        }
        Iterator<SelectionKey> keyIterator = keySet.iterator();
        while (keyIterator.hasNext()) {
            SelectionKey key = keyIterator.next();
            if (key.isValid()) {
                if (key.isAcceptable()) {
                    onAccept(key);
                } else if (key.isReadable()) {
                    onRead(key);
                } else if (key.isWritable()) {
                    if (!onWrite(key)) {
                        continue;
                    }
                }
            }
            keyIterator.remove();
        }
        return keySet.isEmpty();
    }

    static String getUserMessageString(String user, String message) {
        if (user == null) {
            return message;
        }
        return  user + ": " + message;
    }

    /**
     * Отправка сообщения. Сообщение помещается в очередь для отправки.
     */
    private void sendMessage(SelectionKey key, ClientUser fromUser, String message) {
        message = getUserMessageString(fromUser == null ? null : fromUser.getName(), message);
        KeyAttach keyAttach = (KeyAttach) key.attachment();
        keyAttach.getMessageQueue().add(message);
    }

    /**
     * Обработка нового подключения
     */
    private void onAccept(SelectionKey key) {
        try {
            ServerSocketChannel serverSocketChannel = (ServerSocketChannel) key.channel();
            SocketChannel clientChannel = serverSocketChannel.accept();
            clientChannel.configureBlocking(false);
            clientChannel.socket().setTcpNoDelay(true);
            // Регистрируем события
            clientChannel.register(selector, SelectionKey.OP_WRITE | SelectionKey.OP_READ, new KeyAttach());
            SelectionKey clientKey = clientChannel.keyFor(selector);
            Logger.debug("New client %s.", clientChannel.getRemoteAddress());
            clientUserMap.put(clientKey, new ClientUser("Guest"));
            // Приветствие
            sendMessage(clientKey, null, "Welcome to Java chat!");
        } catch (IOException e) {
            Logger.info("Client accept error: %s.", e.getMessage());
        }
    }

    /**
     * Чтение из канала в буфер
     */
    private void readFromChannel(SelectionKey key, ByteBuffer buffer) throws IOException {
        SocketChannel clientChannel = (SocketChannel) key.channel();
        if (clientChannel.read(buffer) == -1) {
            throw new IOException("End of client stream!");
        }
        buffer.flip();
    }

    /**
     * Ограничение очереди сообщений до максимельного размера
     */
    private void trimUserQueue(Queue<String> queue) {
        while (queue.size() > MESSAGE_COUNT_LIMIT) {
            // Если клиент не успевает забирать сообщения, то
            Logger.info("Skip message %s.", queue.poll());
        }
    }

    /**
     * Итерация обработки очереди на отправку сообщений в канал
     *
     * @return true - запись была ли запись (полная или неполная)
     */
    private boolean onWrite(SelectionKey key) {
        try {
            SocketChannel channel = (SocketChannel) key.channel();
            KeyAttach keyAttach = (KeyAttach) key.attachment();

            Queue<String> queue = keyAttach.getMessageQueue();
            trimUserQueue(queue);

            ByteBuffer endBuffer = keyAttach.getEndBuffer();
            boolean hasEndData = endBuffer != null;
            if (hasEndData) {
                channel.write(endBuffer);
                if (endBuffer.hasRemaining()) {
                    return true;
                }
            }

            // Новое сообщение из очереди
            String message = keyAttach.getMessageQueue().poll();
            if (message == null) {
                // Нечего писать
                return hasEndData;
            }

            byte[] messageBytes = message.getBytes(CHARSET_NAME);
            ByteBuffer buffer = ByteBuffer.allocate(message.length() + 4);
            buffer.putInt(messageBytes.length);
            buffer.put(messageBytes);
            buffer.flip();
            int write = channel.write(buffer);

            if (buffer.hasRemaining()) {
                keyAttach.setEndBuffer(buffer);
                return write > 0 || hasEndData;
            }

            return true;
        } catch (IOException e) {
            Logger.debug("Send message error: %s.", e.getMessage());
            removeClient(key);
            return true;
        }
    }

    /**
     * Итерация чтения сообщений из канала
     */
    private void onRead(SelectionKey key) {
        try {
            // Чтение из канала в буфер
            readFromChannel(key, buffer);

            KeyAttach keyAttach = (KeyAttach) key.attachment();

            // Накопитель сообщения
            MessageHolder holder = keyAttach.getHolder();

            while (buffer.hasRemaining()) {
                if (holder == null) {
                    // Первое сообщение клиента или предыдущее сообщение полностью вычиталось
                    holder = new MessageHolder(MESSAGE_SIZE_LIMIT, CHARSET_NAME);
                    keyAttach.setHolder(holder);
                }
                String message = holder.readFromBuffer(buffer);
                if (message == null) {
                    // Сообщение еще не вычиталось
                    buffer.clear();
                    return;
                }
                // Сообщение вычиталось, избавляемся от накопителя
                holder = null;
                keyAttach.setHolder(null);
                incomeMessagesOrCommandsCount++;

                // Обработка сообщения
                performMessage(key, message);
            }

            buffer.clear();
        } catch (LargeMessageException | IOException e) {
            Logger.debug("Receive message error: %s.", e.getMessage());
            removeClient(key);
        }
    }

    /**
     * Отключение клиента
     */
    private void removeClient(SelectionKey key) {
        try {
            SocketChannel clientChannel = (SocketChannel) key.channel();
            key.cancel();
            if (clientChannel != null) {
                clientChannel.close();
            }
            clientUserMap.remove(key);
            Logger.debug("Close channel.");
        } catch (IOException e) {
            // Ignore
            e.printStackTrace();
        }
    }

    /**
     * Имя пользователя для сообщений
     */
    private String getUserName(SelectionKey key) {
        ClientUser clientUser = clientUserMap.get(key);
        return clientUser == null ? "Guest" : clientUser.getName();
    }

    /**
     * Обработка команды из сообщения
     *
     * @return true - команда присутствовала, false - команды не было
     */
    private boolean performCommand(SelectionKey key, String message) throws IOException {
        // По списку доступных команд
        for (Command command : Command.values()) {
            if (message.startsWith(command.getText())) {
                switch (command) {
                    case LOGIN:
                        String name = message.substring(command.getText().length()).trim();
                        ClientUser user = clientUserMap.get(key);
                        user.setName(name);
                        if (user.isGuest()) {
                            // Первый логин в сессии
                            sendLastMessages(key);
                        }
                        user.setGuest(false);
                        break;
                    case INFO:
                        // Отправка количества клиентов, сообщений и пр.
                        long usedMemory = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory();
                        sendMessage(key, null, String.format("Server info:\n" +
                                "Client count = %d\n" +
                                "Messages count = %d\n" +
                                "Used memory = %s\n",
                                getClientCount(),
                                clientMessages.size(),
                                new DecimalFormat("#.##").format(usedMemory / 1024f / 1024f)));
                        break;
                    default:
                        Logger.info("Unknown command %s.", command.name());
                        break;
                }
                return true;
            }
        }
        return false;
    }

    /**
     * Отправка последних MESSAGE_COUNT_LIMIT сообщений
     */
    private void sendLastMessages(SelectionKey key) throws IOException {
        if (clientMessages.isEmpty()) {
            return;
        }
        int size = clientMessages.size();
        int start = size >= MESSAGE_COUNT_LIMIT ? size - MESSAGE_COUNT_LIMIT : 0;
        for (int i = start; i < size; i++) {
            ChatMessage message = clientMessages.get(i);
            sendMessage(key, message.getSender(), message.getText());
        }
    }

    /**
     * Обработка сообщения или команды
     */
    private void performMessage(SelectionKey key, String message) throws IOException {
        Logger.debug("%s say: %s", getUserName(key), message);

        // Поиск команды в сообщении
        if (performCommand(key, message)) {
            return;
        }

        ClientUser currentUser = clientUserMap.get(key);

        // Клиент не указал свое имя
        if (currentUser.isGuest()) {
            // Предлагаем представиться, иначе не рассылаем сообщения
            sendMessage(key, null, "Enter your name!");
            return;
        }

        // В список сообщений
        clientMessages.add(new ChatMessage(currentUser, message));

        // Отослать всем в чате
        for (SelectionKey otherKey : clientUserMap.keySet()) {
            if (otherKey != key && !clientUserMap.get(otherKey).isGuest()) {
                sendMessage(otherKey, currentUser, message);
            }
        }
    }

    /**
     * Список сообщений авторизованных клиентов
     */
    List<ChatMessage> getClientMessages() {
        return clientMessages;
    }

    /**
     * Число подключенных клиентов
     */
    int getClientCount() {
        return clientUserMap.size();
    }

    /**
     * Счетчик входящих сообщений и команд
     */
    public int getIncomeMessagesOrCommandsCount() {
        return incomeMessagesOrCommandsCount;
    }

    /**
     * Остановка
     */
    public void stop() throws IOException {
        start = false;
        serverSocketChannel.socket().close();
        serverSocketChannel.close();
        selector.close();
        Logger.debug("Server stopped.");
    }
}
TOP

Related Classes of com.javachat.server.ChatServer

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.