Разработка мессенджера на RSocket и Spring Boot

Практический курс по созданию реактивного мессенджера с использованием протокола RSocket и фреймворка Spring Boot. Вы изучите основные модели взаимодействия, настройку безопасности и потоковую передачу данных в реальном времени.

1. Введение в RSocket и инициализация проекта Spring Boot

Введение в RSocket и инициализация проекта Spring Boot

Добро пожаловать на курс «Разработка мессенджера на RSocket и Spring Boot». В этом курсе мы пройдем путь от создания пустого проекта до реализации полноценного мессенджера с возможностью обмена сообщениями в реальном времени. Мы не будем использовать устаревшие подходы с постоянным опросом сервера (polling); вместо этого мы построим реактивную систему, способную выдерживать высокие нагрузки.

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

Почему RSocket?

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

RSocket — это бинарный протокол прикладного уровня, который решает эти проблемы «из коробки». Он построен на принципах Reactive Streams и поддерживает семантику обратного давления (Backpressure).

Ключевые особенности RSocket:

* Бинарный формат: Данные передаются в бинарном виде, что экономит трафик и ресурсы процессора на сериализацию/десериализацию по сравнению с текстовыми форматами (например, JSON в HTTP/1.1). * Мультиплексирование: Одно физическое соединение (например, TCP или WebSocket) используется для множества логических потоков. Это избавляет от необходимости открывать новое соединение на каждый запрос. * Транспортная независимость: RSocket может работать поверх TCP, WebSocket и даже Aeron (UDP).

!Сравнение HTTP (множество соединений) и RSocket (мультиплексирование в одном соединении)

Четыре модели взаимодействия

В отличие от HTTP, который ограничен моделью «Запрос-Ответ» (Request-Response), RSocket предлагает четыре модели взаимодействия. Это делает его «швейцарским ножом» для сетевого общения.

  • Request-Response (Запрос-Ответ): Стандартная модель. Клиент отправляет один запрос и получает один ответ. Аналог HTTP GET/POST.
  • Fire-and-Forget (Отправил и забыл): Клиент отправляет данные и не ждет подтверждения. Идеально для отправки метрик, логов или некритичных уведомлений.
  • Request-Stream (Запрос-Поток): Клиент отправляет один запрос и получает поток данных в ответ. Примеры: котировки акций, лента новостей, загрузка больших файлов частями.
  • Channel (Двунаправленный канал): Самая мощная модель. И клиент, и сервер могут отправлять потоки данных друг другу одновременно. Именно эта модель станет основой нашего чата.
  • !Визуализация четырех моделей взаимодействия в RSocket

    Понятие Backpressure (Обратное давление)

    Одной из главных проблем распределенных систем является ситуация, когда быстрый продюсер (отправитель) «затапливает» медленного консьюмера (получателя) данными. В классических подходах это приводит к переполнению буфера и падению приложения с ошибкой OutOfMemoryError.

    RSocket решает это с помощью Backpressure. Получатель явно сообщает отправителю, сколько сообщений он готов обработать в данный момент.

    Математически условие стабильной системы без потери данных можно выразить так:

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

    Инициализация проекта Spring Boot

    Теперь перейдем к практике. Мы создадим серверную часть нашего мессенджера.

    Шаг 1: Spring Initializr

    Для создания проекта мы воспользуемся стандартным генератором Spring. Перейдите на сайт start.spring.io и выберите следующие параметры:

    * Project: Maven (или Gradle, если вам удобнее) * Language: Java * Spring Boot: Последняя стабильная версия (например, 3.2.x) * Packaging: Jar * Java: 17 или 21

    В разделе Dependencies добавьте:

  • RSocket (spring-boot-starter-rsocket) — основная библиотека.
  • Lombok — для сокращения шаблонного кода (опционально, но рекомендуется).
  • Spring Reactive Web — для поддержки реактивного стека (WebFlux).
  • Шаг 2: Настройка порта

    После генерации и распаковки проекта откройте файл src/main/resources/application.properties. По умолчанию RSocket не запускает сервер, пока мы не укажем порт.

    Добавьте следующую строку:

    Это укажет Spring Boot запустить RSocket сервер на порту 7000 поверх TCP.

    Шаг 3: Создание первого контроллера

    В Spring Boot работа с RSocket очень похожа на создание REST-контроллеров, но вместо @RequestMapping мы используем @MessageMapping.

    Создайте класс MessageController.java:

    Разберем этот код: * @Controller: Помечает класс как компонент Spring, обрабатывающий сообщения. * @MessageMapping("greet"): Указывает, что этот метод будет обрабатывать запросы, отправленные на маршрут greet. * Mono<String>: Реактивный тип данных из Project Reactor. Mono означает, что мы вернем ноль или один результат. Это соответствует модели взаимодействия Request-Response.

    Запуск и проверка

    Запустите метод main в вашем классе Application. Если все прошло успешно, в логах вы увидите строку, похожую на:

    NettyRsocketWebServer: Netty RSocket started on port(s): 7000

    Теперь у нас есть работающий RSocket сервер, готовый принимать соединения. В следующих статьях мы создадим клиентскую часть, реализуем потоковую передачу сообщений и подключим базу данных.

    > Важно: RSocket — это не просто замена HTTP. Это смена парадигмы мышления с синхронного «запросил-получил» на асинхронный потоковый обмен данными.

    На этом инициализация завершена. Мы заложили фундамент для нашего будущего мессенджера.

    2. Модели взаимодействия: Request-Response и Fire-and-Forget для отправки сообщений

    Модели взаимодействия: Request-Response и Fire-and-Forget для отправки сообщений

    В предыдущей статье мы инициализировали наш проект на Spring Boot и запустили RSocket сервер на порту 7000. Мы даже создали простейший контроллер, который отвечает на приветствие. Однако, чтобы построить полноценный мессенджер, нам нужно понимать, как именно данные могут перемещаться между клиентом и сервером.

    RSocket — это гибкий протокол, и он не ограничивает нас одной схемой общения. Сегодня мы детально разберем две фундаментальные модели взаимодействия: Request-Response (Запрос-Ответ) и Fire-and-Forget (Отправил и забыл). Мы научимся применять их в контексте нашего чат-приложения.

    Request-Response: Классический диалог

    Модель Request-Response вам уже знакома, если вы работали с HTTP. Это стандартный синхронный (с логической точки зрения) паттерн: клиент отправляет запрос, сервер его обрабатывает и возвращает один ответ.

    В контексте RSocket это выглядит так: поток данных (Stream) состоит из одного сообщения в одну сторону и одного сообщения в обратную. Даже если происходит ошибка, она возвращается как ответ.

    !Диаграмма модели Запрос-Ответ

    Когда использовать в мессенджере?

    В нашем приложении эта модель идеально подходит для действий, требующих подтверждения или получения конкретных данных:

    * Получение профиля пользователя. * Проверка статуса авторизации. * Запрос истории конкретного чата (если мы запрашиваем её как единый список, хотя для больших историй лучше подойдет Stream).

    Реализация на сервере

    Давайте расширим наш MessageController. Представим, что нам нужно получить информацию о пользователе по его ID.

    Обратите внимание на возвращаемый тип Mono<UserDto>. В Project Reactor Mono означает контейнер для 0 или 1 элемента. Это прямое отражение семантики Request-Response.

    Fire-and-Forget: Скорость превыше всего

    Модель Fire-and-Forget (FnF) — это оптимизация, которой так не хватает в стандартном HTTP. Клиент отправляет запрос серверу и не ждет никакого ответа. Сервер, в свою очередь, не обязан подтверждать получение.

    Это похоже на отправку обычного бумажного письма без уведомления о вручении: вы бросили его в ящик и забыли. Письмо может дойти, а может потеряться (хотя в TCP потеря маловероятна, но на уровне приложения подтверждения нет).

    !Диаграмма модели Отправил-и-Забыл

    Математика эффективности

    Давайте сравним задержки (latency) двух подходов с помощью простой формулы времени выполнения операции.

    Для модели Request-Response полное время складывается из времени отправки, обработки и получения ответа:

    Где — общее время блокировки логического потока на клиенте, — время передачи запроса по сети, — время обработки на сервере, — время передачи ответа по сети.

    Для модели Fire-and-Forget клиент освобождается практически мгновенно после отправки данных в сетевой буфер:

    Где — время, затраченное клиентом, а — время записи в локальный сетевой буфер (пренебрежимо мало по сравнению с сетевыми задержками).

    Когда использовать в мессенджере?

    FnF идеален для событий, потеря которых не критична, или где скорость важнее гарантий:

  • Статус "Печатает...": Если один пакет с этим статусом потеряется, пользователь этого даже не заметит, так как через секунду придет новый.
  • Отправка метрик и логов: Клиент может отправлять данные о производительности на сервер.
  • Уведомление о прочтении: Иногда это реализуют через FnF для снижения нагрузки.
  • Реализация на сервере

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

    Ключевое отличие здесь — возвращаемый тип Mono<Void>. Мы возвращаем пустой Mono, сигнализируя фреймворку, что никаких данных отправлять обратно не нужно. Spring Boot, видя Mono<Void>, автоматически выбирает фрейм взаимодействия Fire-and-Forget на уровне протокола RSocket.

    Настройка Клиента (RSocketRequester)

    До сих пор мы писали только серверный код. Чтобы проверить работу наших моделей, нам нужен клиент. В Spring Boot для этого используется RSocketRequester.

    Создадим конфигурационный класс ClientConfiguration для создания бина реквестера.

    Теперь мы можем инъектировать RSocketRequester в любой компонент и отправлять запросы. Создадим простой компонент ChatRunner, который выполнит запросы при старте приложения, чтобы мы могли увидеть результат в консоли.

    Разбор методов клиента

  • .route("..."): Указывает маршрут, аналогичный @MessageMapping на сервере.
  • .data(...): Полезная нагрузка (payload), которую мы отправляем.
  • .retrieveMono(...): Явно говорит клиенту ожидать ответ (Request-Response).
  • .send(): Говорит клиенту отправить данные и не ждать ответа (Fire-and-Forget).
  • Обработка ошибок

    Важно помнить, что в модели Fire-and-Forget ошибки, возникшие на сервере, не возвращаются клиенту. Если в методе setTypingStatus выбросится исключение, клиент об этом никогда не узнает.

    В модели Request-Response ошибки пробрасываются. Если сервер выбросит RuntimeException, клиент получит сигнал onError в своем Mono.

    Заключение

    Мы рассмотрели два базовых кирпичика RSocket:

  • Request-Response для надежного получения данных.
  • Fire-and-Forget для быстрой отправки событий без подтверждения.
  • В контексте нашего мессенджера мы будем использовать Request-Response для входа в систему и загрузки профилей, а Fire-and-Forget — для технических уведомлений.

    Однако, мессенджер — это прежде всего поток сообщений в реальном времени. Как получить список сообщений, который постоянно обновляется? Как подписаться на обновления чата? Для этого нам понадобятся более мощные модели: Request-Stream и Channel, которые мы разберем в следующей статье.