Chatting with Spring Boot and WebSockets





. « Spring Framework» . , , : « Spring», .









In 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 technology for bidirectional communication 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 server and 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 (the Simple / the Streaming the Text Oriented the Message Protocol) defines the rules for the exchange of messages between the server and the 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







  • The Auth Service is responsible for authenticating and managing users. We will not reinvent the wheel here and will use the authentication service from JWT and Social Authentication using Spring Boot .
  • The Chat Service is responsible for configuring WebSocket, handling STOMP messages, and storing and processing user messages.
  • The 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 looks like this:



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


The class is ChatMessagepretty 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 a 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 the 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 WebSocketConfig class with annotations @Configurationand @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;
    }
}


The first method configures a simple in-memory message broker with a single prefixed address /userto send and receive messages. Prefixed addresses /appare for messages processed by annotated methods @MessageMapping, which we'll discuss in the next section.



The second method registers the 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 a controller that 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()));
    }
}


We use annotation @MessageMappingto configure that when the message is sent to /app/chat, the method is called processMessage. Please note that the previously configured application prefix will be added to the mapping /app.



This method stores the message in MongoDB and then calls the method convertAndSendToUserto send the notification to the target.



The method of convertAndSendToUseradding a prefix /userand recipientIdto 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 one to identify it chatId.



The ChatRoom class looks like this:



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


The value chatIdis 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 get the same chatId.



JavaScript client



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



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


The method connect()establishes a connection to /ws, where our server is waiting for connections, and also defines a callback function onConnectedthat will be called upon successful connection, and onErrorcalled if an error occurred while connecting to the server.



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

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


The 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, a sendMessage()message is sent to the address /app/chatthat is specified 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 built a JavaScript client using the SockJs and Stomp.js libraries .



Sample source code can be found here .






Learn more about the course.











All Articles