Использование Spring Boot для реализации WebSocket с помощью STOMP

Опубликовано: 2022-03-11

Протокол WebSocket — это один из способов заставить ваше приложение обрабатывать сообщения в реальном времени. Наиболее распространенными альтернативами являются длительные опросы и события, отправленные сервером. Каждое из этих решений имеет свои преимущества и недостатки. В этой статье я собираюсь показать вам, как реализовать WebSockets с помощью Spring Boot Framework. Я расскажу о настройке как на стороне сервера, так и на стороне клиента, и мы будем использовать STOMP по протоколу WebSocket для связи друг с другом.

Серверная часть будет написана исключительно на Java. Но в случае с клиентом я покажу фрагменты, написанные как на Java, так и на JavaScript (SockJS), поскольку обычно клиенты WebSockets встраиваются во внешние приложения. Примеры кода продемонстрируют, как рассылать сообщения нескольким пользователям с использованием модели pub-sub, а также как отправлять сообщения только одному пользователю. В следующей части статьи я кратко расскажу о защите WebSockets и о том, как мы можем гарантировать, что наше решение на основе WebSocket останется работоспособным, даже если среда не поддерживает протокол WebSocket.

Обратите внимание, что тема защиты WebSockets здесь будет затронута лишь вкратце, так как это достаточно сложная тема для отдельной статьи. Из-за этого и нескольких других факторов, которые я затрагиваю в WebSocket в производстве? раздел в конце, я рекомендую внести изменения, прежде чем использовать эту настройку в производственной среде, прочитайте до конца, чтобы узнать о готовой к производственной эксплуатации настройке с принятыми мерами безопасности.

Протоколы WebSocket и STOMP

Протокол WebSocket позволяет реализовать двунаправленную связь между приложениями. Важно знать, что HTTP используется только для начального рукопожатия. После этого HTTP-соединение обновляется до вновь открытого соединения TCP/IP, которое используется WebSocket.

Протокол WebSocket — это довольно низкоуровневый протокол. Он определяет, как поток байтов преобразуется в кадры. Фрейм может содержать текст или бинарное сообщение. Поскольку само сообщение не предоставляет никакой дополнительной информации о том, как его направить или обработать, сложно реализовать более сложные приложения без написания дополнительного кода. К счастью, спецификация WebSocket позволяет использовать подпротоколы, работающие на более высоком прикладном уровне. Одним из них, поддерживаемым Spring Framework, является STOMP.

STOMP — это простой протокол обмена текстовыми сообщениями, изначально созданный для языков сценариев, таких как Ruby, Python и Perl, для подключения к корпоративным брокерам сообщений. Благодаря STOMP клиенты и брокеры, разработанные на разных языках, могут отправлять и получать сообщения друг от друга. Протокол WebSocket иногда называют TCP для Интернета. Аналогично, STOMP называется HTTP для Интернета. Он определяет несколько типов фреймов, которые отображаются на фреймы WebSockets, например, CONNECT , SUBSCRIBE , UNSUBSCRIBE , ACK или SEND . С одной стороны, эти команды очень удобны для управления коммуникацией, а с другой — позволяют реализовывать решения с более сложными функциями, такими как подтверждение сообщений.

На стороне сервера: Spring Boot и WebSockets

Для создания серверной части WebSocket мы будем использовать среду Spring Boot, которая значительно ускоряет разработку автономных и веб-приложений на Java. Spring Boot включает модуль spring-WebSocket , который совместим со стандартом Java WebSocket API (JSR-356).

Реализация серверной части WebSocket с помощью Spring Boot не является очень сложной задачей и включает всего пару шагов, которые мы пройдем один за другим.

Шаг 1. Во-первых, нам нужно добавить зависимость библиотеки WebSocket.

 <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-websocket</artifactId> </dependency>

Если вы планируете использовать формат JSON для передаваемых сообщений, вы можете также включить зависимость GSON или Jackson. Вполне вероятно, что вам может понадобиться дополнительный фреймворк безопасности, например, Spring Security.

Шаг 2. Затем мы можем настроить Spring для включения обмена сообщениями WebSocket и STOMP.

 Configuration @EnableWebSocketMessageBroker public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { @Override public void registerStompEndpoints(StompEndpointRegistry registry) { registry.addEndpoint("/mywebsockets") .setAllowedOrigins("mydomain.com").withSockJS(); } @Override public void configureMessageBroker(MessageBrokerRegistry config){ config.enableSimpleBroker("/topic/", "/queue/"); config.setApplicationDestinationPrefixes("/app"); } }

Метод configureMessageBroker делает две вещи:

  1. Создает брокер сообщений в памяти с одним или несколькими пунктами назначения для отправки и получения сообщений. В приведенном выше примере определены два префикса назначения: topic и queue . Они следуют соглашению о том, что места назначения для сообщений, которые должны быть переданы всем подписанным клиентам через модель pub-sub, должны начинаться с префикса topic . С другой стороны, адресаты для личных сообщений обычно имеют префикс queue .
  2. Определяет app префикса, которое используется для фильтрации пунктов назначения, обрабатываемых методами, аннотированными с помощью @MessageMapping , которые вы реализуете в контроллере. Контроллер, обработав сообщение, отправит его брокеру.

Spring Boot WebSocket: как сообщения обрабатываются на стороне сервера

Как сообщения обрабатываются на стороне сервера (источник: документация Spring)


Возвращаясь к приведенному выше фрагменту — возможно, вы заметили вызов метода withSockJS() — он включает резервные варианты SockJS. Короче говоря, это позволит нашим WebSockets работать, даже если протокол WebSocket не поддерживается интернет-браузером. Чуть дальше я рассмотрю эту тему более подробно.

Есть еще один момент, который требует уточнения — почему мы вызываем метод setAllowedOrigins() на конечной точке. Это часто требуется, потому что поведение WebSocket и SockJS по умолчанию заключается в том, чтобы принимать запросы только одного происхождения. Таким образом, если ваш клиент и сервер используют разные домены, необходимо вызвать этот метод, чтобы разрешить связь между ними.

Шаг 3 . Реализуйте контроллер, который будет обрабатывать запросы пользователей. Он будет транслировать полученное сообщение всем пользователям, подписавшимся на данную тему.

Вот пример метода, который отправляет сообщения в пункт назначения /topic/news .

 @MessageMapping("/news") @SendTo("/topic/news") public void broadcastNews(@Payload String message) { return message; }

Вместо аннотации @SendTo вы также можете использовать SimpMessagingTemplate , который вы можете автоматически связать внутри вашего контроллера.

 @MessageMapping("/news") public void broadcastNews(@Payload String message) { this.simpMessagingTemplate.convertAndSend("/topic/news", message) }

На более поздних этапах вы можете добавить некоторые дополнительные классы для защиты конечных точек, например ResourceServerConfigurerAdapter или WebSecurityConfigurerAdapter из среды Spring Security. Кроме того, часто полезно реализовать модель сообщений, чтобы передаваемый JSON можно было сопоставить с объектами.

Создание клиента WebSocket

Внедрение клиента — еще более простая задача.

Шаг 1. Autowire Spring STOMP client.

 @Autowired private WebSocketStompClient stompClient;

Шаг 2. Откройте соединение.

 StompSessionHandler sessionHandler = new CustmStompSessionHandler(); StompSession stompSession = stompClient.connect(loggerServerQueueUrl, sessionHandler).get();

Как только это будет сделано, можно отправить сообщение адресату. Сообщение будет отправлено всем пользователям, подписавшимся на тему.

 stompSession.send("topic/greetings", "Hello new user");

Также можно подписаться на сообщения.

 session.subscribe("topic/greetings", this); @Override public void handleFrame(StompHeaders headers, Object payload) { Message msg = (Message) payload; logger.info("Received : " + msg.getText()+ " from : " + msg.getFrom()); }

Иногда необходимо отправить сообщение только выделенному пользователю (например, при реализации чата). Затем клиент и сервер должны использовать отдельный пункт назначения, предназначенный для этого частного разговора. Имя пункта назначения может быть создано путем добавления уникального идентификатора к общему имени пункта назначения, например, /queue/chat-user123 . Для этой цели можно использовать идентификаторы сеанса HTTP или STOMP.

Spring значительно упрощает отправку личных сообщений. Нам нужно только аннотировать метод контроллера с помощью @SendToUser . Затем этот пункт назначения будет обрабатываться UserDestinationMessageHandler , который зависит от идентификатора сеанса. На стороне клиента, когда клиент подписывается на место назначения с префиксом /user , это место назначения преобразуется в место назначения, уникальное для этого пользователя. На стороне сервера место назначения пользователя разрешается на основе Principal пользователя.

Пример серверного кода с аннотацией @SendToUser :

 @MessageMapping("/greetings") @SendToUser("/queue/greetings") public String reply(@Payload String message, Principal user) { return "Hello " + message; }

Или вы можете использовать SimpMessagingTemplate :

 String username = ... this.simpMessagingTemplate.convertAndSendToUser(); username, "/queue/greetings", "Hello " + username);

Давайте теперь посмотрим, как реализовать клиент JavaScript (SockJS), способный получать личные сообщения, которые могут быть отправлены кодом Java в приведенном выше примере. Стоит знать, что WebSockets являются частью спецификации HTML5 и поддерживаются большинством современных браузеров (Internet Explorer поддерживает их, начиная с версии 10).

 function connect() { var socket = new SockJS('/greetings'); stompClient = Stomp.over(socket); stompClient.connect({}, function (frame) { stompClient.subscribe('/user/queue/greetings', function (greeting) { showGreeting(JSON.parse(greeting.body).name); }); }); } function sendName() { stompClient.send("/app/greetings", {}, $("#name").val()); }

Как вы, наверное, заметили, чтобы получать личные сообщения, клиент должен подписаться на общий пункт назначения /queue/greetings с префиксом /user . Ему не нужно возиться с какими-либо уникальными идентификаторами. Однако перед этим клиент должен войти в приложение, поэтому объект Principal на стороне сервера инициализируется.

Защита веб-сокетов

Многие веб-приложения используют аутентификацию на основе файлов cookie. Например, мы можем использовать Spring Security, чтобы ограничить доступ к определенным страницам или контроллерам только для зарегистрированных пользователей. Контекст безопасности пользователя затем поддерживается через сеанс HTTP на основе файлов cookie, который позже связывается с сеансами WebSocket или SockJS, созданными для этого пользователя. Конечные точки WebSockets могут быть защищены, как и любые другие запросы, например, в WebSecurityConfigurerAdapter Spring.

В настоящее время веб-приложения часто используют REST API в качестве серверной части и токены OAuth/JWT для аутентификации и авторизации пользователей. Протокол WebSocket не описывает, как серверы должны аутентифицировать клиентов во время рукопожатия HTTP. На практике для этой цели используются стандартные заголовки HTTP (например, Authorization). К сожалению, он поддерживается не всеми клиентами STOMP. Клиент Spring Java STOMP позволяет устанавливать заголовки для рукопожатия:

 WebSocketHttpHeaders handshakeHeaders = new WebSocketHttpHeaders(); handshakeHeaders.add(principalRequestHeader, principalRequestValue);

Но клиент JavaScript SockJS не поддерживает отправку заголовка авторизации с запросом SockJS. Однако он позволяет отправлять параметры запроса, которые можно использовать для передачи токена. Этот подход требует написания пользовательского кода на стороне сервера, который будет считывать токен из параметров запроса и проверять его. Также важно убедиться, что токены не регистрируются вместе с запросами (или журналы хорошо защищены), так как это может привести к серьезному нарушению безопасности.

Резервные варианты SockJS

Интеграция с WebSocket не всегда может проходить гладко. Некоторые браузеры (например, IE 9) не поддерживают WebSockets. Более того, ограничительные прокси-серверы могут сделать невозможным обновление HTTP или обрезать соединения, которые открыты слишком долго. В таких случаях на помощь приходит SockJS.

Транспорты SockJS делятся на три основные категории: WebSockets, HTTP Streaming и HTTP Long Polling. Связь начинается с того, что SockJS отправляет GET /info для получения базовой информации с сервера. Основываясь на ответе, SockJS принимает решение об используемом транспорте. Первый выбор — WebSockets. Если они не поддерживаются, то по возможности используется Streaming. Если и этот вариант невозможен, то в качестве метода транспорта выбирается Polling.

WebSocket в производстве?

Хотя эта установка работает, она не является «лучшей». Spring Boot позволяет вам использовать любую полноценную систему обмена сообщениями с протоколом STOMP (например, ActiveMQ, RabbitMQ), а внешний брокер может поддерживать больше операций STOMP (например, подтверждения, квитанции), чем простой брокер, который мы использовали. STOMP Over WebSocket предоставляет интересную информацию о WebSockets и протоколе STOMP. В нем перечислены системы обмена сообщениями, которые поддерживают протокол STOMP и могут быть лучшим решением для использования в производстве. Особенно, если из-за большого количества запросов брокер сообщений должен быть кластеризован. (Простой брокер сообщений Spring не подходит для кластеризации.) Затем вместо включения простого брокера в WebSocketConfig требуется включить ретранслятор брокера Stomp, который перенаправляет сообщения к внешнему брокеру сообщений и от него. Подводя итог, можно сказать, что внешний брокер сообщений может помочь вам создать более масштабируемое и надежное решение.

Если вы готовы продолжить свое путешествие по Java-разработчику, изучая Spring Boot, попробуйте прочитать Использование Spring Boot для защиты OAuth2 и JWT REST .