Пишем чат с использованием Spring Boot и WebSockets

от автора

Всем привет. В преддверии старта курса «Разработчик на Spring Framework» мы подготовили для вас еще один полезный перевод. Но, прежде чем перейти к статье, хотим поделиться с вами бесплатной записью урока от наших преподавателей по теме: «Рефакторинг кода приложений на Spring», а также предлагаем посмотреть запись вебинара из которого вы сможете подробно узнать о программе курса и формате обучения.

А теперь перейдем к статье


В статье Building Scalable Facebook-like Notification using Server-Sent Event and Redis для отправки сообщений от сервера клиенту мы использовали Server-sent Events. Также там было упомянуто о WebSocket — технологии двунаправленной связи между сервером и клиентом.

В этой статье мы посмотрим на один из распространенных примеров использования WebSocket. Мы напишем приложение для обмена приватными сообщениями.

Ниже на видео продемонстрировано то, что мы собираемся сделать.

Введение в WebSockets и STOMP

WebSocket — это протокол для двусторонней связи между сервером и клиентом.
WebSocket, в отличие от HTTP, протокола прикладного уровня, является протоколом транспортного уровня (TCP). Хотя для первоначальной установки соединения используется HTTP, но потом соединение «обновляется» до TCP-соединения, используемого в WebSocket.

WebSocket — протокол низкого уровня, который не определяет форматы сообщений. Поэтому WebSocket RFC определяет подпротоколы, описывающие структуру и стандарты сообщений. Мы будем использовать STOMP поверх WebSockets (STOMP over WebSockets).

Протокол STOMP (Simple / Streaming Text Oriented Message Protocol) определяет правила обмена сообщениями между сервером и клиентом.

STOMP похож на HTTP и работает поверх TCP, используя следующие команды:

  • CONNECT
  • SUBSCRIBE
  • UNSUBSCRIBE
  • SEND
  • BEGIN
  • COMMIT
  • ACK

Спецификацию и полный список команд STOMP можно найти здесь.

Архитектура

  • Сервис аутентификации (Auth Service) ответственен за аутентификацию и управление пользователями. Здесь мы не будем изобретать колесо и воспользуемся сервисом аутентификации из статьи JWT and Social Authentication using Spring Boot.
  • Сервис чата (Chat Service) ответственен за настройку WebSocket, обработку STOMP-сообщений, а также за сохранение и обработку сообщений пользователей.
  • Клиент (Chat Client) — это приложение на ReactJS, использующее STOMP-клиента для подключения и подписки на чат. Также здесь находится пользовательский интерфейс.

Модель сообщения

Первое, о чем нужно подумать — это модель сообщения. ChatMessage выглядит следующим образом:

public class ChatMessage {    @Id    private String id;    private String chatId;    private String senderId;    private String recipientId;    private String senderName;    private String recipientName;    private String content;    private Date timestamp;    private MessageStatus status; }

Класс ChatMessage довольно простой, с полями, необходимыми для идентификации отправителя и получателя.

В нем также есть поле статуса, указывающее доставлено ли сообщение клиенту.

public enum MessageStatus {     RECEIVED, DELIVERED }

Когда сервер получает сообщение из чата, он не отправляет сообщение адресату напрямую, а отправляет уведомление (ChatNotification), чтобы оповестить клиента о получении нового сообщения. После этого клиент сам может получить новое сообщение. Как только клиент получит сообщение, оно помечается как доставленное (DELIVERED).

Уведомление выглядит следующим образом:

public class ChatNotification {     private String id;     private String senderId;     private String senderName; }

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

Настройка WebSocket и STOMP в Spring

Первым делом настраиваем конечную точку STOMP и брокер сообщений.

Для этого создаем класс WebSocketConfig с аннотациями @Configuration и @EnableWebSocketMessageBroker.

@Configuration @EnableWebSocketMessageBroker public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {      @Override     public void configureMessageBroker(MessageBrokerRegistry config) {         config.enableSimpleBroker( "/user");         config.setApplicationDestinationPrefixes("/app");         config.setUserDestinationPrefix("/user");     }      @Override     public void registerStompEndpoints(StompEndpointRegistry registry) {         registry                 .addEndpoint("/ws")                 .setAllowedOrigins("*")                 .withSockJS();     }      @Override     public boolean configureMessageConverters(List<MessageConverter> messageConverters) {         DefaultContentTypeResolver resolver = new DefaultContentTypeResolver();         resolver.setDefaultMimeType(MimeTypeUtils.APPLICATION_JSON);         MappingJackson2MessageConverter converter = new MappingJackson2MessageConverter();         converter.setObjectMapper(new ObjectMapper());         converter.setContentTypeResolver(resolver);         messageConverters.add(converter);         return false;     } }

Первый метод конфигурирует простой брокер сообщений в памяти с одним адресом с префиксом /user для отправки и получения сообщений. Адреса с префиксом /app предназначены для сообщений, обрабатываемых методами с аннотацией @MessageMapping, которые мы обсудим в следующем разделе.

Второй метод регистрирует конечную точку STOMP /ws. Эта конечная точка будет использоваться клиентами для подключения к STOMP-серверу. Здесь также включается резервный SockJS, который будет использоваться, если WebSocket будет недоступен.

Последний метод настраивает конвертер JSON, который используется Spring’ом для преобразования сообщений из/в JSON.

Контроллер для обработки сообщений

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

@Controller public class ChatController {      @Autowired private SimpMessagingTemplate messagingTemplate;     @Autowired private ChatMessageService chatMessageService;     @Autowired private ChatRoomService chatRoomService;      @MessageMapping("/chat")     public void processMessage(@Payload ChatMessage chatMessage) {         var chatId = chatRoomService                 .getChatId(chatMessage.getSenderId(), chatMessage.getRecipientId(), true);         chatMessage.setChatId(chatId.get());          ChatMessage saved = chatMessageService.save(chatMessage);                  messagingTemplate.convertAndSendToUser(                 chatMessage.getRecipientId(),"/queue/messages",                 new ChatNotification(                         saved.getId(),                         saved.getSenderId(),                         saved.getSenderName()));     } }

С помощью аннотации @MessageMapping мы настраиваем, что при отправке сообщения в /app/chat вызывается метод processMessage. Обратите внимание, что к маппингу добавится сконфигурированный ранее application-префикс /app.

Этот метод сохраняет сообщение в MongoDB, а затем вызывает метод convertAndSendToUser для отправки уведомления адресату.

Метод convertAndSendToUser добавляет префикс /user и recipientId к адресу /queue/messages. Конечный адрес будет выглядеть так:

/user/{recipientId}/queue/messages

Все подписчики данного адреса (в нашем случае один) получат сообщение.

Генерация chatId

Для каждой беседы между двумя пользователями мы создаем чат-комнату и для ее идентификации генерируем уникальный chatId.

Класс ChatRoom выглядит следующим образом:

public class ChatRoom {     private String id;     private String chatId;     private String senderId;     private String recipientId; }

Значение chatId равно конкатенации senderId_recipientId. Для каждой беседы мы сохраняем две сущности с одинаковыми chatId: одну между отправителем и получателем, а другую между получателем и отправителем, чтобы оба пользователя получали одинаковый chatId.

JavaScript-клиент

В этом разделе мы создадим JavaScript-клиента, который будет отправлять сообщения на WebSocket/STOMP-сервер и получать их оттуда.

Мы будем использовать SockJS и Stomp.js для общения с сервером с использованием STOMP over WebSocket.

const connect = () => {     const Stomp = require("stompjs");     var SockJS = require("sockjs-client");     SockJS = new SockJS("http://localhost:8080/ws");     stompClient = Stomp.over(SockJS);     stompClient.connect({}, onConnected, onError);   };

Метод connect() устанавливает соединение с /ws, где ожидает подключений наш сервер, и также определяет callback-функцию onConnected, которая будет вызвана при успешном подключении, и onError, вызываемую, если при подключении к серверу произошла ошибка.

const onConnected = () => {     console.log("connected");      stompClient.subscribe(       "/user/" + currentUser.id + "/queue/messages",       onMessageReceived     );   };

Метод onConnected() подписывается на определенный адрес и получает все отправляемые туда сообщения.

const sendMessage = (msg) => {     if (msg.trim() !== "") {       const message = {         senderId: currentUser.id,         recipientId: activeContact.id,         senderName: currentUser.name,         recipientName: activeContact.name,         content: msg,         timestamp: new Date(),       };                stompClient.send("/app/chat", {}, JSON.stringify(message));     }   };

В конце метода sendMessage() сообщение отправляется по адресу /app/chat, который указан в нашем контроллере.

Заключение

В этой статье мы рассмотрели все важные моменты создания чата с использованием Spring Boot и STOMP over WebSocket.

Мы также создали JavaScript-клиент с применением библиотек SockJs и Stomp.js.

Пример исходного кода можно найти здесь.


Узнать о курсе подробнее.


Читать ещё

ссылка на оригинал статьи https://habr.com/ru/company/otus/blog/516702/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *