Disclaimer: на эту работу было потрачено огромное количество сил, потому, если в случае отрицательного ответа вы оставите хотя бы отзыв в ишью, будет очень здорово
Этот проект тестировался и запускался на 11 и 16 Java.
Для запуска клиента ./gradlew runFrontend
Для запуска сервера ./gradlew runBackend
В ТЗ было сказано использовать как можно меньше зависимостей. Я это расценил как то что проверяющему хочется оценить более фундаментальные знания нежели чем знания библиотек. По этой причине я притащил в проект только две библиотеки.
- Gson - все общение клиент-сервер у меня происходит именно через JSON. Json хорошо себя показывает в случае когда нужно соблюдать обратную совместимость. Например, добавить какое-то новое поле в респонс. Что позитивно сказывается на обратной совместимости. Есть конечно бинарные протоколы, но кажется, что они не так хорошо прижились в этом мире, что плохо скажется на адаптации проекта в случае если придется открывать апи.
- Jetty - http server. Использование HTTP протокола кажется отличным решением из-за того что мы сразу же бесплатно можем использовать все штуки для него придуманные. HTTP прокси, https и прочее.
Больше зависимостей нет. Только в тестах.
Главная цель, которая преследовалась при продумывании архитектуры это устойчивость к большим нагрузкам.
Так как в сервисе из состояний, которые хранятся не в бд, практически ничего нет мы можем довольно неплохо масштабировать сервис вширь. По этой причине я сосредоточил усилия на быстродействии самого сервиса и защите от дудоса на Application уровне.
Дополнительно я работал над удобством использования протокола.
Далее я буду рассказывать про решения, которые принял и реализовал и сразу же объяснять причины.
В моей реализации каждый endpoint описывается через вот такую конструкцию:
public static final Endpoint<UserPasswordRequest, SessionResponse> SIGN_UP_REQUEST_ENDPOINT
= new Endpoint<>("signup", UserPasswordRequest.class, SessionResponse.class);
На сервере создается сервлет с путем signup
, который принимает UserPasswordRequest
и возвращает SessionResponse
После чего запросы выглядят вот так красиво
HttpClient client = connect();
var response = client.sendRequest(
Endpoint.SIGN_UP_REQUEST_ENDPOINT,
new UserPasswordRequest(username, password)
);
SessionResponse session = assertSuccess(response.response());
validateResponse(session);
В итоге у нас получается, что компилятор сам за нас проверят, что мы правильно общаемся с сервером.
Считаю такие штуки очень важны в долгосрочной перспективе, когда протокол переписывается 3 миллиона раз. Дополнительно помогает другим разработчикам быстрее вкатиться в проект.
В одном из своих проектов я не учел это при проектировании и, по итогу, очень много ошибок попадало на прод и потом стоило больших усилий это переписать.
Сделан бан ip при большом количестве запросов с него за определенный промежуток времени. Реализовано это при помощи скользящего окна и аналога капчи. Я посчитал не очень нужным реализовывать настоящую капчу с картинками потому я просто шлю текстом математическую задачку.
Для каждого сервиса можно назвать разумное количество запросов от клиента в какой-то промежуток времени. Так как на каждый запрос нам нужно лезть в базу данных и, как следствие, тратить процессорное время и озу, кажется неплохой идеей уменьшить количество запросов и, тем самым, более эффективно использовать ресурсы серверов. С другой же стороны для злоумышленника накидать кучу запросов и тем самым не оставить ресурсов на нормальных юзеров.
В общем, кажется, что вот такая простая защита от дудоса является очевидно необходимой
Реализован сервер на стеке TCP/IP. Реализация написана на Java NIO, то есть не использует блокирующее IO.
Реализован обмен секретами и, конечно же реализована аутентификация. Так же добавлены защиты от всевозможных направлений атаки и всё апи так же статически типизировано.
Прежде всего для реализации чата в реальном времени нам нужно как то доставлять клиенту информацию о новых сообщениях ASAP. Так как мы не можем напрямую отправить клиенту, например, UDP пакет при получении нового сообщения из-за NAT, нам нужно, что бы клиент сам открывал к нам подключение. Так как мы не можем делать каждый раз HTTP запрос потому что это сначала TCP handshake, а потом еще и HTTP хэдер. И не то что бы очень хотим каждый раз открывать TCP подключение потому что handshake это уже достаточно больно. Решением является держать одно подключение постоянно.
Так как по большей части подключение будет простаивать использование потока на каждое подключение кажется очень избыточным в плане ЦПУ и ОЗУ, а потому non-blocking IO.
Статическая типизация LongPoll не менее важна чем типизация Rest API
Простенькое кеширование подключений к базе данных для переиспользования между несколькими запрсосами
Подключение к базе данных это на самом деле дорогое занятие. Так как в целом нет никакого смысла делать каждый раз новое подключение почему не переиспользвать старое.
Запрос сообщений не выдает слайс сообщений с лимитом в 100 штук и с задаваемым смещением по времени.
Кажется что передавать сразу всю историю сообщений на каждый запрос забьет канал сети примерно очень быстро.
Здесь я перечислю все те хотелки, которые я бы хотел видеть в готовом проекте, но не смог реализовать из-за нехватки времени. Как-никак у меня есть еще учеба.
Сейчас пользователь после авторизации запрашивает сообщения, список друзей и пользователей. Это точно то что нужно каким-то образом оптимизировать. Например, сразу в ответе на авторизацию выдавать друзей и пользователей
Эта база данных была использована для того что бы запуск бекенда был как можно проще, но явно не очень нам подходит
Текушая реализация довольно эффективна, и куда имеет смысл ее развивать так это вширь. Это делается довольно легко, если отдать фильтрацию айпишников на откуп балансеру, но кажется для этого уже давно придумали готовые решения.
На уровне приложения мы вроде как себя обезопасили, но нас все еще неплохо могут задудосить обычным флудом.
Кажется задача, которую тоже относительно неплохо решили.
Для настоящего веб чата это все таки необходимое условие. Мне не захотелось реализовывать его ручками, а тащить либы читерство потому это в хотелках.
Ну вот не хватило на них времени да