Chatting with Spring Boot and WebSockets

Hello. On the eve of the start of the course “Spring Framework Developer” we have prepared another useful translation for you. But, before moving on to the article, we want to share with you a free lesson recording from our teachers on the topic: “Refactoring Application Code in Spring”and also offer watch the webinar recording from which you you can learn in detail about the course program and training format

Now let’s move on to the article


The article Building Scalable Facebook-like Notification using Server-Sent Event and Redis we used Server-sent Events to send messages from the server to the client. It also mentioned WebSocket, a bi-directional communication technology between a server and a client.

In this article, we’ll take a look at one of the common use cases for WebSocket. We will write a private messaging application.

The video below demonstrates what we are going to do.

Introduction to WebSockets and STOMP

WebSocket Is a protocol for two-way communication between a server and a client.
WebSocket, unlike HTTP, the application layer protocol, is a transport layer protocol (TCP). Although HTTP is used for the initial connection, the connection is then “upgraded” to the TCP connection used in WebSocket.

WebSocket is a low-level protocol that does not define message formats. Therefore, the WebSocket RFC defines subprotocols that describe the structure and standards of messages. We will be using STOMP over WebSockets (STOMP over WebSockets).

Protocol STOMP (Simple / Streaming Text Oriented Message Protocol) defines the rules for exchanging messages between server and client.

STOMP is similar to HTTP and runs on top of TCP using the following commands:

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

The specification and full list of STOMP commands can be found here

Architecture

  • Authentication service (Auth Service) is responsible for user authentication and management. Here we will not reinvent the wheel and will use the authentication service from the article JWT and Social Authentication using Spring Boot
  • Chat service (Chat Service) is responsible for configuring WebSocket, handling STOMP messages, and storing and processing user messages.
  • Customer (Chat Client) Is a ReactJS application that uses a STOMP client to connect and subscribe to a chat. Also here is the user interface.

Message model

The first thing to think about is the message model. ChatMessage as follows:

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;
}

Class ChatMessage pretty simple, with fields needed to identify the sender and recipient.

It also has a status field indicating whether the message has been delivered to the client.

public enum MessageStatus {
    RECEIVED, DELIVERED
}

When the server receives a message from the chat, it does not send the message directly to the recipient, but sends a notification (ChatNotification) to notify the client that a new message has been received. After that, the client himself can receive a new message. As soon as the client receives the message, it is marked as DELIVERED.

The notification looks like this:

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

The notification contains the ID of the new message and information about the sender so that the client can show information about the new message or the number of new messages, as shown below.

Configuring WebSocket and STOMP in Spring

The first step is to configure the STOMP endpoint and message broker.

To do this, we create a class WebSocketConfig with annotations @Configuration and @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 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;
    }
}

The first method configures a simple in-memory message broker with one prefixed address /user to send and receive messages. Prefixed addresses /app intended for messages processed by annotated methods @MessageMappingwhich we will discuss in the next section.

Second method registers STOMP endpoint /ws… This endpoint will be used by clients to connect to the STOMP server. This also includes a fallback SockJS that will be used if the WebSocket is not available.

The last method configures the JSON converter that Spring uses to convert messages to / from JSON.

Controller for handling messages

In this section, we will create controllerthat will handle requests. It will receive a message from the user and send it to the recipient.

@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()));
    }
}

Using annotation @MessageMapping we configure that when sending a message to /app/chat method is called processMessage… Please note that the previously configured application prefix will be added to the mapping. /app

This method saves the message to MongoDB and then calls the method convertAndSendToUser to send a notification to the addressee.

Method convertAndSendToUser adds a prefix /user and recipientId to the address /queue/messages… The final address will look like this:

/user/{recipientId}/queue/messages

All subscribers to this address (in our case, one) will receive the message.

Generating chatId

For each conversation between two users, we create a chat room and generate a unique chatId

Class ChatRoom as follows:

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

Value chatId equal to concatenation senderId_recipientId… For each conversation, we keep two entities with the same chatId: one between the sender and the recipient, and the other between the recipient and the sender, so that both users receive the same chatId

JavaScript client

In this section, we will create a JavaScript client that will send messages to and receive WebSocket / STOMP server from there.

We will be using SockJS and Stomp.js to communicate with the server using 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);
  };

Method connect() establishes a connection with /wswhere our server is waiting for connections, and also defines a callback function onConnectedwhich will be called upon successful connection, and onErrorcalled if an error occurs while connecting to the server.

const onConnected = () => {
    console.log("connected");

    stompClient.subscribe(
      "/user/" + currentUser.id + "/queue/messages",
      onMessageReceived
    );
  };

Method onConnected() subscribes to a specific address and receives all messages sent there.

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));
    }
  };

At the end of the method sendMessage() the message is sent to the address /app/chatwhich is listed in our controller.

Conclusion

In this article, we have covered all the important points of creating a chat using Spring Boot and STOMP over WebSocket.

We also created a JavaScript client using the SockJs libraries and Stomp.js

Sample source code can be found here


Learn more about the course.


Read more

  • Integration testing in SpringBoot with TestContainers starter
  • What’s New in Spring Data (Klara Dan von) Neumann

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *