Computer scienceBackendSpring BootWebWebSockets

STOMP over web sockets

8 minutes read

We've learned in previous topics what WebSockets are and how they can create a two-way communication channel between a client and a server. While this is a robust feature, it's not always easy to use. WebSockets are low-level, and using them directly can be quite complex. This is where STOMP comes in.

In this topic, we will learn what STOMP is and how it simplifies the use of WebSockets. We will also explore how to use STOMP with Spring Boot. Let's get started!

What is STOMP?

The WebSocket specification allows the use of subprotocols that operate at a higher, application level. One supported by the Spring Framework is STOMP.

STOMP stands for Simple Text Oriented Messaging Protocol. It is a straightforward, text-based protocol that streamlines the use of WebSockets. It acts as a higher-level protocol layered on top of WebSockets, offering an easy way to send and receive messages over WebSocket connections. Originally designed for scripting languages like Ruby, Python, and Perl to interact with enterprise message brokers, STOMP makes it simple to exchange messages between clients and brokers in different languages.

The protocol focuses on a fundamental subset of commonly used messaging patterns and can operate over any reliable two-way streaming network protocol, including TCP and WebSocket. While STOMP mainly handles text-based communication, message payloads can include both text and binary data.

STOMP is a frame-based protocol, with frames similar to HTTP. The following listing shows a STOMP frame's structure:

COMMAND
header1:value1
header2:value2

Body^@

Clients can use commands like SEND or SUBSCRIBE to send messages or subscribe to them, with a destination header specifying where to send the message or the subscription destination. This enables a simple publish-subscribe mechanism for sending messages through the broker to other connected clients, or for sending requests to the server for action.

Note that the frame's body ends with a null character (^@), control-@ in ASCII, and the frame can have zero or more headers. The headers and the body are separated by a blank line.

If you want to learn more about STOMP, you can check out the official STOMP specification.

Now that we understand STOMP, let's look at how it's used in Spring Boot. We'll create a simple WebSocket endpoint where clients can connect, send a name as a message, and receive a greeting in response. For the sake of simplicity, we will use the same Spring Boot project to build both the server and the client.

The server

Spring Boot includes the spring-boot-starter-websocket dependency, which provides support for STOMP over WebSocket. This allows us to create a WebSocket endpoint that can be used to send and receive messages using STOMP.

To use STOMP with Spring Boot, we need to follow these steps:

  • Add the spring-boot-starter-websocket dependency.

  • Create a configuration class that implements WebSocketMessageBrokerConfigurer and overrides the registerStompEndpoints and configureMessageBroker methods.

  • Create a controller class that handles the messages.

While the first step is straightforward, the second and third steps require a bit more explanation. Let's take a closer look at each of them.

Creating the configuration class

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

    @Override
    public void configureMessageBroker(MessageBrokerRegistry config) {
        config.enableSimpleBroker("/topic");
        config.setApplicationDestinationPrefixes("/app");
    }

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/mywebsocket");
    }
}

The annotation @EnableWebSocketMessageBroker is used to enable WebSocket message handling, backed by a message broker.

The configureMessageBroker method is used to configure the message broker. Here, we are using the enableSimpleBroker method to enable a simple in-memory message broker that can send messages to clients on destinations prefixed with /topic. Instead of using an in-memory message broker, we could use one like RabbitMQ or ActiveMQ. That would require additional configuration, but we will stick with the in-memory message broker to keep things simple.

We are also using the setApplicationDestinationPrefixes method to set the application destination prefixes to /app. This means messages sent to destinations prefixed with /app will route to @MessageMapping-annotated methods in the controller class. We should note that when messages are processed, the destination prefix is stripped, and the remaining part is used to determine which method to invoke in the controller class. So, if we have a destination /app/hello, and one of the prefix is /app, the @MessageMapping annotation should be @MessageMapping("/hello").

The registerStompEndpoints method is used to register a STOMP over WebSocket endpoint. In this example, we use the addEndpoint method to register the /mywebsocket endpoint.

Creating the controller class

@Controller
public class GreetingController {

    @MessageMapping("/hello")
    @SendTo("/topic/greetings")
    public String greeting(String personName) throws Exception {
        Thread.sleep(1000); // simulated delay
        return "Hello, " + HtmlUtils.htmlEscape(personName) + "!";
    }
}

The @MessageMapping annotation maps the /hello destination to the greeting method. The @SendTo annotation specifies that the return value of the greeting method should go to the /topic/greetings destination, i.e. to all subscribers of the /topic/greetings destination.

With these components in place, we can achieve the following flow:

  • A client connects to the /mywebsocket endpoint.

  • The client subscribes to the /topic/greetings destination.

  • The client sends a message to the /app/hello destination. This message routes to the greeting method in the controller class.

  • The return value from the greeting method is the payload of the message for the /topic/greetings destination. The message broker handles this message and sends it to all /topic/greetings subscribers.

From the flow above, we can identify the frames exchanged between the client and the server:

  • The client sends a CONNECT frame to the server to establish a connection.

  • The server sends a CONNECTED frame to the client to acknowledge the connection.

  • The client sends a SUBSCRIBE frame to the server to subscribe to the /topic/greetings destination.

  • The client sends a SEND frame to the server to send a message to the /app/hello destination.

  • The server sends a MESSAGE frame to the client to deliver the message to the /topic/greetings destination.

While we have only discussed the server side of WebSocket communication so far, to complete the example, we need to create a client that connects to the WebSocket endpoint and sends a message. It's important to note that the client can use any technology that supports STOMP over WebSocket, but for this example, we'll use Spring Boot, as it offers a convenient STOMP client.

The client

A simple Java class with a main method can play the role of the client. Additionally, we will need a StompSessionHandler implementation to handle the STOMP session.

public class StompClient {
    public static void main(String[] args) {
        WebSocketClient client = new StandardWebSocketClient();
        WebSocketStompClient stompClient = new WebSocketStompClient(client);

        // Set the message converter for the payload.
        stompClient.setMessageConverter(new StringMessageConverter());

        StompSessionHandler sessionHandler = new MyStompSessionHandler();
        stompClient.connectAsync("ws://localhost:8080/mywebsocket", sessionHandler);

        new Scanner(System.in).nextLine(); // Don't close immediately
    }
}

For the session handler, we can create a class that extends StompSessionHandlerAdapter and overrides the following methods: afterConnected, handleFrame, and handleException.

public class MyStompSessionHandler extends StompSessionHandlerAdapter {

    @Override
    public void afterConnected(StompSession session, StompHeaders connectedHeaders) {
        session.subscribe("/topic/greetings", this);
        LocalDateTime now = LocalDateTime.now();
        session.send("/app/hello", "Jane @ " + now);
    }

    @Override
    public void handleException(StompSession session,
                                StompCommand command,
                                StompHeaders headers,
                                byte[] payload,
                                Throwable exception) {
        System.out.println("Got an error. Handle it here.");
    }

    @Override
    public Type getPayloadType(StompHeaders headers) {
        return String.class;
    }

    @Override
    public void handleFrame(StompHeaders headers, Object payload) {
         System.out.println("Received: " + payload);
    }
}

The implementation is straightforward. Once connected to the WebSocket endpoint, the client subscribes to the /topic/greetings destination and sends a message to the /app/hello destination. The handleFrame method handles messages received from the server, and the handleException method deals with any errors encountered.

Note that when the client subscribes to /topic/greetings, it uses this as the second argument. This is achievable because the MyStompSessionHandler class implements the StompFrameHandler interface, enabling it to manage messages received from the server. This is accomplished by overriding the handleFrame method, which accepts the message payload as an argument. In our scenario, the payload type is specifically a String, as indicated by the getPayloadType method.

And there you have it!

Starting the server and two clients will result in the following output for the clients:

Client 1:
Received: Hello, Jane @ 2024-02-27T11:51:02.104292!
Received: Hello, Jane @ 2024-02-27T11:51:11.677341!
Client 2:
Received: Hello, Jane @ 2024-02-27T11:51:11.677341!

From the output, we see that when the second client connects and sends a message, the first client also receives it. That's because both clients are subscribed to the same destination and the message broker relays the message to all subscribed clients.

Conclusion

We've covered what STOMP is and its role in streamlining WebSocket usage. We've also delved into integrating STOMP with Spring Boot, establishing a WebSocket endpoint for messaging via STOMP, and crafting a client that connects to this endpoint, subscribes, and sends messages to the server.

12 learners liked this piece of theory. 0 didn't like it. What about you?
Report a typo