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.");
}
}