-
Notifications
You must be signed in to change notification settings - Fork 38
task_messenger
Описание
Разработка сервера для мессенджера - системы обмена сообщениями. Основной упор на сетевое взаимодействие, многопоточную обработку и работу с базой данных.
Название ветки для разработки
[вашгитхабаккаунт]-messenger
В track.msgtest лежат классы из этого описания. Можно использовать предлагамую архитектуру, можно ее изменить, в таком случае потребуются пояснения.
К первому рубежному контролю (18-20 апреля)
- реализовать многопоточный сервер, который умеет обрабатывать несколько подключений, создавать для них сессии и передавать простые текстовые сообщения между пользователями. В качестве сервера можно использовать jetty или другие библиотеки, которые поднимают сервер. Либо можно написать свою реализацию, взяв за основу многопоточный сервер с занятия. Желательно в таком случае использовать thread pool. Для тех, кто хочет сложностей - использовать NIO
- реализация должна быть устойчива к ошибкам пользователя (ввод неверных команд и неверных параметров)
- продумать, как система будет обрабатывать исключения (когда падает, когда пишет ошибку в лог)
- Продумать и реализовать протокол (механизм) преобразования текстовых сообщений и команд в бинарный вид и обратно (java serialization/JSON/binary)
Что должно работать: 2 клиента подключаются к серверу и авторизуются через логин/пароль. Либо создают регистрационные записи. Затем они попадают в один чат (возможно единственный на данном этапе). Теперь, когда 1 клиент вводит текст, другой получает его онлайн, то есть сообщение приходит сразу всем получателям (участникам чата).
Ко второму контролю (ориентировочно - май)
- реализовать все команды из списка
- хранить всю информацию в базе
- добавить логирование информации
- написать интеграционные тесты
- взаимодействие сервер-клиент происходит с помощью сообщений, с каждым действием связано соответствующее сщщбщение (логин, логаут, добавление в чат и т д). Команда инкапсулирует в себе все необходимы данные для обработки.
- мессенджер поддерживает обработку сообщений от пользователя в специальном формате, начинающемся с /
- в случае ошибки обработки сообщения, сервер возвращает сервисное сообщение со статусом ошибки (неправильные аргументы, не залогинен и т д)
Сообщение - это абстракция, может быть представлена java-объектом с набором полей необходимых команде, например LoginMessage должен содержать поля login и password
Ниже описан консольный интерфейс сообщений. Для кодирования типа сообщения используем enum Type, общий предок всех команд в системе - AbstractMessage Для каждого типа сообщения от клиента к серверу есть тип сообщения с ответом. Например, на MSG_LOGIN в ответ приходит MSG_STATUS. В ответ на запрос истории MSG_CHAT_HIST приходит MSG_CHAT_HIST_RESULT с историей.
public abstract class AbstractMessage implements Serializable {
private Long id;
private Long senderId;
private Type type;
}
// Список кодов сообщений
public enum Type {
// Сообщения от клиента к серверу
MSG_LOGIN("login"), // в ответ MSG_STATUS
MSG_TEXT("text"), // в ответ MSG_STATUS
MSG_INFO("info"), // в ответ MSG_INFO_RESULT
MSG_CHAT_LIST("chat_list"), // в ответ MSG_CHAT_LIST_RESULT,
MSG_CHAT_CREATE("chat_create"), // в ответ MSG_STATUS
MSG_CHAT_HIST("chat_history"), // в ответ 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
отправить сообщение в заданный чат, чат должен быть в списке чатов пользователя (только для залогиненных пользователей)
Для каждой команды в системе должен существовать обработчик - реализация интерфейса CommandHandler, будет полезно разобраться с паттерном Команда вики
public interface CommandHandler {
/**
* Реализация паттерна Команда. Метод execute() вызывает соответствующую реализацию,
* для запуска команды нужна сессия, чтобы можно было сгенерить ответ клиенту и провести валидацию
* сессии.
* @param session - текущая сессия
* @param message - сообщение для обработки
* @throws CommandException - все исключения перебрасываются как CommandException
*/
void execute(Session session, Message message) throws CommandException;
}
Внутри команды описывается логика обработки сообщения, то есть можно сходить в базу, проверить пользователя, изменить состояние объектов системы. Команды отвечают за бизнес-логику.
В рамках проекта будем использовать сокеты (java.net.Socket & java.net.ServerSocket). Необходимо обеспечить асинхронное взаимодействие между клиентом и сервером. Сервер должен уметь обрабатывать несколько соединений одновременно. На каждое новое подключение создается объект сессии track.messenger.net.Session, который инкапсулирует в себе информацию о клиенте и in/out каналы сокета для чтения и записи данных. Сессия должна реализовывать интерфейс
Либо можно использовать jetty как сервер и разобраться, как в нем реализовать многопоточное подключение и обработку запросов.
public class Session {
/**
* Отправить сообщение.
* Требуется обработать 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
Интерфейс протокол описан в track.messenger.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 Также можно использовать spring-beans
Для сервера в конфиге должны быть описаны компоненты, которые существуют в единственном экземпляре - 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