Skip to content
This repository has been archived by the owner on Nov 9, 2023. It is now read-only.

Latest commit

 

History

History
188 lines (127 loc) · 11.7 KB

README.md

File metadata and controls

188 lines (127 loc) · 11.7 KB

VK Web Chat

Disclaimer: на эту работу было потрачено огромное количество сил, потому, если в случае отрицательного ответа вы оставите хотя бы отзыв в ишью, будет очень здорово

Как запускать

Этот проект тестировался и запускался на 11 и 16 Java.

Для запуска клиента ./gradlew runFrontend

Для запуска сервера ./gradlew runBackend

Зависимости

В ТЗ было сказано использовать как можно меньше зависимостей. Я это расценил как то что проверяющему хочется оценить более фундаментальные знания нежели чем знания библиотек. По этой причине я притащил в проект только две библиотеки.

  • Gson - все общение клиент-сервер у меня происходит именно через JSON. Json хорошо себя показывает в случае когда нужно соблюдать обратную совместимость. Например, добавить какое-то новое поле в респонс. Что позитивно сказывается на обратной совместимости. Есть конечно бинарные протоколы, но кажется, что они не так хорошо прижились в этом мире, что плохо скажется на адаптации проекта в случае если придется открывать апи.
  • Jetty - http server. Использование HTTP протокола кажется отличным решением из-за того что мы сразу же бесплатно можем использовать все штуки для него придуманные. HTTP прокси, https и прочее.

Больше зависимостей нет. Только в тестах.

Архитектура

Главная цель, которая преследовалась при продумывании архитектуры это устойчивость к большим нагрузкам.

Так как в сервисе из состояний, которые хранятся не в бд, практически ничего нет мы можем довольно неплохо масштабировать сервис вширь. По этой причине я сосредоточил усилия на быстродействии самого сервиса и защите от дудоса на Application уровне.

Дополнительно я работал над удобством использования протокола.

Далее я буду рассказывать про решения, которые принял и реализовал и сразу же объяснять причины.

Строгая типизация API

Что сделано

В моей реализации каждый 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 миллиона раз. Дополнительно помогает другим разработчикам быстрее вкатиться в проект.

В одном из своих проектов я не учел это при проектировании и, по итогу, очень много ошибок попадало на прод и потом стоило больших усилий это переписать.

Защита от DDoS

Что сделано

Сделан бан ip при большом количестве запросов с него за определенный промежуток времени. Реализовано это при помощи скользящего окна и аналога капчи. Я посчитал не очень нужным реализовывать настоящую капчу с картинками потому я просто шлю текстом математическую задачку.

Зачем

Для каждого сервиса можно назвать разумное количество запросов от клиента в какой-то промежуток времени. Так как на каждый запрос нам нужно лезть в базу данных и, как следствие, тратить процессорное время и озу, кажется неплохой идеей уменьшить количество запросов и, тем самым, более эффективно использовать ресурсы серверов. С другой же стороны для злоумышленника накидать кучу запросов и тем самым не оставить ресурсов на нормальных юзеров.

В общем, кажется, что вот такая простая защита от дудоса является очевидно необходимой

Long Poll

Что сделано

Реализован сервер на стеке TCP/IP. Реализация написана на Java NIO, то есть не использует блокирующее IO.

Реализован обмен секретами и, конечно же реализована аутентификация. Так же добавлены защиты от всевозможных направлений атаки и всё апи так же статически типизировано.

Зачем

Прежде всего для реализации чата в реальном времени нам нужно как то доставлять клиенту информацию о новых сообщениях ASAP. Так как мы не можем напрямую отправить клиенту, например, UDP пакет при получении нового сообщения из-за NAT, нам нужно, что бы клиент сам открывал к нам подключение. Так как мы не можем делать каждый раз HTTP запрос потому что это сначала TCP handshake, а потом еще и HTTP хэдер. И не то что бы очень хотим каждый раз открывать TCP подключение потому что handshake это уже достаточно больно. Решением является держать одно подключение постоянно.

Так как по большей части подключение будет простаивать использование потока на каждое подключение кажется очень избыточным в плане ЦПУ и ОЗУ, а потому non-blocking IO.

Статическая типизация LongPoll не менее важна чем типизация Rest API

DB Connections Pool

Что сделано

Простенькое кеширование подключений к базе данных для переиспользования между несколькими запрсосами

Зачем

Подключение к базе данных это на самом деле дорогое занятие. Так как в целом нет никакого смысла делать каждый раз новое подключение почему не переиспользвать старое.

Ограничение на кол-во запрашиваемых сообщений

Что сделано

Запрос сообщений не выдает слайс сообщений с лимитом в 100 штук и с задаваемым смещением по времени.

Зачем

Кажется что передавать сразу всю историю сообщений на каждый запрос забьет канал сети примерно очень быстро.

Что можно добавить

Здесь я перечислю все те хотелки, которые я бы хотел видеть в готовом проекте, но не смог реализовать из-за нехватки времени. Как-никак у меня есть еще учеба.

Уменьшить кол-во запросов от пользователя

Сейчас пользователь после авторизации запрашивает сообщения, список друзей и пользователей. Это точно то что нужно каким-то образом оптимизировать. Например, сразу в ответе на авторизацию выдавать друзей и пользователей

Сменить базу данных с SQLite

Эта база данных была использована для того что бы запуск бекенда был как можно проще, но явно не очень нам подходит

Поднять несколько инстансов сервиса с лоад балансером

Текушая реализация довольно эффективна, и куда имеет смысл ее развивать так это вширь. Это делается довольно легко, если отдать фильтрацию айпишников на откуп балансеру, но кажется для этого уже давно придумали готовые решения.

Поднять фаерволл

На уровне приложения мы вроде как себя обезопасили, но нас все еще неплохо могут задудосить обычным флудом.

Кажется задача, которую тоже относительно неплохо решили.

Сделать веб сокет в Long Poll

Для настоящего веб чата это все таки необходимое условие. Мне не захотелось реализовывать его ручками, а тащить либы читерство потому это в хотелках.

Сделать гораздо больше интеграционных тестов

Ну вот не хватило на них времени да