-
Notifications
You must be signed in to change notification settings - Fork 53
Задание 2 Messenger
Описание
Разработка сервера для мессенджера - системы обмена сообщениями. Основной упор на сетевое взаимодействие, многопоточную обработку и работу с базой данных.
Название ветки для разработки
[вашгитхабаккаунт]-messenger
- взаимодействие сервер-клиент происходит с помощью сообщений, с каждым действием связано соответствующее сообщение (логин, логаут, добавление в чат и т д). Сообщение инкапсулирует в себе все необходимы данные для обработки.
- мессенджер поддерживает обработку сообщений от пользователя в специальном формате, начинающемся с /
- в случае ошибки обработки сообщения, сервер возвращает сервисное сообщение со статусом ошибки (неправильные аргументы, не залогинен и т д)
Ниже описан консольный интерфейс команд. Для кодирования типа сообщения используем enum Type, общий предок всех сообщений в системе Message
public abstract class Message implements Serializable {
private Long id;
private Long senderId;
private Type type;
}
// Список кодов сообщений
public enum Type {
// Сообщения от клиента к серверу
MSG_LOGIN, // в ответ MSG_STATUS
MSG_TEXT, // в ответ MSG_STATUS
MSG_INFO, // в ответ MSG_INFO_RESULT
MSG_CHAT_LIST, // в ответ MSG_CHAT_LIST_RESULT,
MSG_CHAT_CREATE, // в ответ MSG_STATUS
MSG_CHAT_HIST, // в ответ MSG_CHAT_HIST_RESULT,
// Сообщения от сервера клиенту
MSG_STATUS,
MSG_CHAT_LIST_RESULT,
MSG_CHAT_HIST_RESULT,
MSG_INFO_RESULT
}
У каждой команды со стороны клиента есть текстовое название (для консольного интерфейса)
/help
показать список команд и общий хэлп по месседжеру. Реализуется полностью на клиенте
/login <логин_пользователя> <пароль>
/login arhangeldim qwerty
залогиниться (если логин не указан, то авторизоваться). В случае успеха приходит вся инфа о пользователе
/info [id]
/info инфа о себе
/info 3 - инфа о пользователе id=3
получить всю информацию о пользователе, без аргументов - о себе (только для залогиненных пользователей)
/chat_list
получить список чатов пользователя(только для залогиненных пользователей). От сервера приходит список id чатов
/chat_create <user_id list>
/chat_create 1,2,3,4 - создать чат с пользователями id=1, id=2, id=3, id=4
/chat_create 3 - создать чат с пользователем id=3, если такой чат уже существует, вернуть существующий
создать новый чат, список пользователей приглашенных в чат (только для залогиненных пользователей).
/chat_history <chat_id>
/chat_history 2 - сообщения из чата id=2
список сообщений из указанного чата (только для залогиненных пользователей)
/text <id> <message>
/text 3 Hello, it's pizza time! - отправить указанное сообщение в чат id=3
отправить сообщение в заданный чат, чат должен быть в списке чатов пользователя (только для залогиненных пользователей)
Для каждой команды в системе должен существовать обработчик - реализация интерфейса Command, будет полезно разобраться с паттерном Команда вики
public interface Command {
/**
* Реализация паттерна Команда. Метод execute() вызывает соответствующую реализацию,
* для запуска команды нужна сессия, чтобы можно было сгенерить ответ клиенту и провести валидацию
* сессии.
* @param session - текущая сессия
* @param message - сообщение для обработки
* @throws CommandException - все исключения перебрасываются как CommandException
*/
void execute(Session session, Message message) throws CommandException;
}
Внутри команды описывается логика обработки сообщения, то есть можно сходить в базу, проверить пользователя, изменить состояние объектов системы. Команды отвечают за бизнес-логику.
В рамках проекта будем использовать сокеты (java.net.Socket & java.net.ServerSocket). Необходимо обеспечить асинхронное взаимодействие между клиентом и сервером. Сервер должен уметь обрабатывать несколько соединений одновременно. На каждое новое подключение создается объект сессии arhangel.dim.core.net.Session, который инкапсулирует в себе информацию о клиенте и in/out каналы сокета для чтения и записи данных. Сессия должна реализовывать интерфейс
public interface ConnectionHandler {
/**
* Отправить сообщение.
* Требуется обработать 2 типа ошибок
* @throws ProtocolException - ошибка протокола (не получилось кодировать/декодировать)
* @throws IOException - ошибка чтения/записи данных в сеть
*/
void send(Message msg) throws ProtocolException, IOException;
/**
* Реакция на сообщение, пришедшее из сети
*/
void onMessage(Message msg);
/**
* Молча (без проброса ошибок) закрываем соединение и освобождаем ресурсы
*/
void close();
}
Чтобы передавать по сети сложные данные (объекты, коллекции) нужно разработать протокол общения. Протокол может быть текстовым или бинарным. Текстовый протокол проще для отладки, можно использовать формат json для представления объекта в виде строки. Итоговая строка конвертируется в byte[] - байтовый массив и отправляется в сокет. Бинарый протокол лучше, с точки зрения производительности и трафика, но сложнее в отладке. можно разработать свой протокол, можно воспользоваться встроенным механизмом сериализации.
JSON http://www.mkyong.com/java/jackson-2-convert-java-object-to-from-json/ (ссылка в репозиторий maven и примеры там есть)
Serializable http://skipy.ru/technics/serialization.html http://www.ccfit.nsu.ru/~deviv/courses/oop/java_ser_rus.html
Интерфейс протокол описан в arhangel.dim.core.net.Protocol (Для выполнения задания реализуйте BinaryProtocol в том же пакете)
public interface Protocol {
Message decode(byte[] bytes) throws ProtocolException;
byte[] encode(Message msg) throws ProtocolException;
}
Материалы по сериализации
http://habrahabr.ru/post/60317/ http://skipy.ru/technics/serialization.html http://www.codeproject.com/Tips/991180/Java-Sockets-and-Serialization
Сериализация через сокет выглядит очень просто, достаточно обернуть stream
socket = new Socket(InetAddress.getLocalHost(), portNumber);
// Обернем стримы и получим стримы для чтения и записи объектов
ObjectInputStream ois = new ObjectInputStream(socket.getInputStream());
ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream());
Message msg = new LoginMessage();
// На одной стороне пишем
oos.writeObject(msg);
oos.flush();
// С другой стороны читаем
// Класс Message должен implements Serializable
Message msg = (Message) ois.readObject();
На стороне сервера можно выделить следующие сущности -
- User(пользователь)
- Message(Сообщение)
- Chat(Разговор, чат)
У каждой сущности есть уникальный идентификатор (Long id). Данные на сервере хранятся в хранилище (базе данных), за взаимодействие с бд отвечают интерфейсы UserStore (пользователи) и MessageStore (сообщения и чаты). С помощью этих интерфейсов можно получить объекты из БД.
Примерная диаграмма классов
Примерная схема потока данных (данные идут как по стрелкам клиент-сервер, так и в обратную сторону)
Общение пользователей происходит в чатах. Чат - это разговор 2х и более пользователей. Каждый залогиненый пользователь может создать чат командой /chat_create <ids>, где <ids> - это список участников чата.
При попытке создать диалог (2 участника), возвращается существующий чат (как личные сообщения в соц сетях). При создании мультичата (> 2 участников) - создается новый, даже если существует чат с таким же набором участников.
Клиент может получить список чатов, в которых он принимал участие и просмотреть историю переписки (все данные запрашиваются с сервера). Все текстовые сообщения сохраняются на сервере, служебные сообщения не нужно сохранять в истории.
В качестве утилиты для запуска используем написанный ранее контейнер. Можно посмотреть реализацию клиента client.xml
Для сервера в конфиге должны быть описаны компоненты, которые существуют в единственном экземпляре - Protocol, UserStore, MessageStore, Server. В конфигурацию нужно вынести все настройки, такие как порт, адрес БД, логин/пароль пользователя БД - то есть все, что можно сконфигурить.
Код должен состоять из независимых модулей, общающихся друг с другом через интерфейсы. Модуль должен быть вынесен в отдельный package. Не забывайте, что при хорошем ООП дизайне, каждый класс должен решать определенную задачу и, желательно, только её.
Также большое внимание уделите оформлению кода и его чистоте: Code Style Guide Полный гайд от гугла на англ. Некоторые рекоммендации на рус.
В коде нужно правильно обрабатывать исключительные ситуации и уметь объяснить, почему обработка происходит таким образом. Чтобы определиться с исключениями, подумайте какую задачу вы решаете и как должна себя вести система в том или ином случае. Исключение - это обработка именно нештатных ситуаций, неожидаемое поведение.
Для обработки и хранения данных правильно используйте коллекции. Нужно уметь объяснить свой выбор той или иной структуры данных, знать основные свойства (контракт, скорость доступа, добавление, удаление элементов).
Для хорошего решения задачи нужно также продумать устройство базы данных, структуру таблиц и их связь.
Большое внимание уделите работе с потоками и корректной их остановке.
Итого, на оценку влияет
- архитектура приложения (разделение на модули и классы, распределение ответсвенности между ними)
- использование интерфейсов и наследования
- ошибки разработчика (незакрытые ресурсы, необработанные исключения, неправильная проверка условий)
- выбор способа хранения данных (правильно ли выбран коллекция для задачи, правильно ли выбран алгоритм, структура таблиц базы данных)
- читаемость кода и его оформление
Задание разбиваем на 2 части - прототип и полная версия.
Включаем сервер, запускаем 2 клиент, один создает чат со вторым и отправляет сообщение. Второй клиент видит пришедшее сообщение. Сообщение сохранилось в базе. Не падает от невалидного ввода пользователя, уметь обрабатывать исключительные ситуации (Случаи, когда можно упасть - невалидный конфиг запуска, не работает сеть);
-
уметь обрабатывать подключение от нескольких клиентов в разных потоках; обрабатывать сообщения асинхронно;
-
создавать/логинить пользователя, сохранять информацию о нем в базе;
-
отправлять текстовое сообщение на сервер. Сервер в свою очередь отправляет это сообщение всем в чате
-
использовать для запуска утилиту container (напишите конфиг в файле server.xml);
-
реализуйте бизнес логику в соответствие с интерфейсами UserStore/MessageStore, работу с JDBC - подключение, получение Connection, вынесите в отдельный класс, чтобы не смешивать с логикой обработки данных. Изучите паттерны DAO/QueryExecutor и используйте один из них;
-
используйте PreparedStatement где возможно;
-
в прототипе еще можно использовать StringProtocol.
-
Реализовать все пары сообщение-команда из списка выше.
-
Для преобразования Message Object -> byte[] и обратно использовать сериализация (Java/JSON serialization)
-
Для отладочной информации использовать логирование (библиотека log4j например)
-
Написать юнит тесты на логику команд и на протокол
-
Использовать ConnectionPool к базе данных
-
Для управления потоками на сервере использовать ThreadPool