The WebSocket protocol provides a powerful full-duplex communication channel between a client and a server. However, it operates at a low level, dealing only with frames and messages without any inherent structure or semantics. This is where STOMP (Simple Text Oriented Messaging Protocol) comes in. STOMP is a simple, text-based protocol that defines a set of commands and semantics for client-server messaging, providing a higher-level, frame-based protocol that both parties can understand.
This article explores how STOMP works over WebSocket in the Java ecosystem, particularly with the Spring Framework, to build robust, message-driven web applications.
Why STOMP? The Problem with Raw WebSockets
While raw WebSockets are powerful, they leave several challenges:
- No Built-in Messaging Patterns: You have to invent your own conventions for pub/sub, point-to-point, request-reply, etc.
- No Native Destination Semantics: There's no concept of "sending to a topic" or "queue." You must build this logic yourself.
- No Protocol-Level Acknowledgment: You must implement your own acknowledgment framework if needed.
- Content Negotiation: You have to decide on a message format (e.g., JSON, XML) and build parsers on both ends.
STOMP solves these problems by providing a well-defined protocol on top of the WebSocket transport, much like HTTP is a protocol on top of TCP.
Core STOMP Concepts
STOMP is frame-based. A frame consists of a command, a set of headers, and an optional body.
Key STOMP Commands:
CONNECT/CONNECTED: Establishes and confirms the connection.SEND: Sends a message to a destination (e.g., a topic or queue).SUBSCRIBE: Registers to listen to a destination.UNSUBSCRIBE: Stops listening to a destination.MESSAGE: Delivers a message from the server to a client.ACK/NACK: Acknowledges or negatively acknowledges message consumption.DISCONNECT: Closes the connection.
Key Headers:
destination: The target of aSENDor the topic of aSUBSCRIBE.content-type: The type of the body (e.g.,application/json).subscription: The ID of a subscription.id: A unique identifier for a message or subscription.receipt: Requests a receipt from the server for a client frame.
Architecture: STOMP over WebSocket
The architecture layers look like this:
[STOMP Client] <--(STOMP Frames)--> [STOMP Broker] <--(STOMP Frames)--> [STOMP Server/Spring App] | | | [WebSocket] [WebSocket] [WebSocket] | | | [TCP/IP] [TCP/IP] [TCP/IP]
- The client establishes a WebSocket connection to the server.
- The client sends a STOMP
CONNECTframe over the WebSocket channel. - The server responds with a STOMP
CONNECTEDframe. - The client can now
SUBSCRIBEto destinations andSENDmessages. - The server can
SENDmessages to any destination the client is subscribed to.
In a Spring application, the framework acts as the STOMP Broker, handling connection management, subscription routing, and message delivery.
Implementing STOMP over WebSocket with Spring
Spring Framework provides first-class support for STOMP over WebSocket. Here's how to build a simple chat application.
1. Server-Side Configuration (Java Config)
@Configuration @EnableWebSocketMessageBroker public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { @Override public void configureMessageBroker(MessageBrokerRegistry config) { // Enable a simple memory-based message broker to carry messages // back to clients on destinations prefixed with "/topic" config.enableSimpleBroker("/topic"); // Designate the "/app" prefix for messages that are bound for // @MessageMapping-annotated methods (application endpoints). config.setApplicationDestinationPrefixes("/app"); } @Override public void registerStompEndpoints(StompEndpointRegistry registry) { // Register the "/ws" endpoint, enabling SockJS fallback options. // Clients will use this to connect to the WebSocket server. registry.addEndpoint("/ws").withSockJS(); } } 2. The Message Model (POJO)
public class ChatMessage { private String from; private String text; private String timestamp; // Default constructor, getters, and setters are required for JSON serialization public ChatMessage() {} public ChatMessage(String from, String text, String timestamp) { this.from = from; this.text = text; this.timestamp = timestamp; } // Getters and Setters... } 3. The Controller (Message Handling)
@Controller public class ChatController { // The SimpMessagingTemplate is used to send messages to the broker @Autowired private SimpMessagingTemplate messagingTemplate; // Handles messages sent to /app/chat.send // The @MessageMapping annotation ensures that if a message is sent to // destination "/app/chat.send", the sendMessage() method is called. @MessageMapping("/chat.send") @SendTo("/topic/public") // Alternative: return the message to send public void sendMessage(@Payload ChatMessage chatMessage) { // Process the message (e.g., save to database, add timestamp) chatMessage.setTimestamp(LocalDateTime.now().toString()); // Send the processed message to all subscribers of "/topic/public" messagingTemplate.convertAndSend("/topic/public", chatMessage); } // Handles new user arrivals @MessageMapping("/chat.addUser") public void addUser(@Payload ChatMessage chatMessage) { // Notify everyone that a new user has joined chatMessage.setType(ChatMessage.MessageType.JOIN); messagingTemplate.convertAndSend("/topic/public", chatMessage); } } 4. Client-Side JavaScript Example
This example uses the sockjs-client and stompjs libraries.
<!DOCTYPE html> <html> <head> <title>STOMP Chat</title> <script src="https://cdn.jsdelivr.net/npm/sockjs-client@1/dist/sockjs.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/@stomp/stompjs@7/bundles/stomp.umd.min.js"></script> </head> <body> <div id="messageArea"></div> <input type="text" id="messageInput" placeholder="Type a message..."/> <button onclick="sendMessage()">Send</button> <script> const stompClient = new StompJs.Client({ brokerURL: 'ws://localhost:8080/ws', debug: function (str) { console.log(str); }, reconnectDelay: 5000, heartbeatIncoming: 4000, heartbeatOutgoing: 4000 }); stompClient.onConnect = (frame) => { console.log('Connected: ' + frame); // Subscribe to the Public Topic stompClient.subscribe('/topic/public', (message) => { const chatMessage = JSON.parse(message.body); showMessage(chatMessage.from + ': ' + chatMessage.text); }); }; stompClient.onWebSocketError = (error) => { console.error('Error with websocket', error); }; stompClient.activate(); function sendMessage() { const messageInput = document.getElementById('messageInput'); const message = messageInput.value.trim(); if (message) { const chatMessage = { from: 'User123', // In real app, get from authentication text: message }; // Send message to the server application endpoint stompClient.publish({ destination: '/app/chat.send', body: JSON.stringify(chatMessage) }); messageInput.value = ''; } } function showMessage(message) { const messageArea = document.getElementById('messageArea'); const p = document.createElement('p'); p.textContent = message; messageArea.appendChild(p); } </script> </body> </html> Message Flow in Detail
- Connection:
- Client connects to
ws://localhost:8080/ws. - Client sends STOMP
CONNECTframe. - Server responds with STOMP
CONNECTEDframe.
- Client connects to
- Subscription:
- Client sends
SUBSCRIBEframe withid: sub-1anddestination: /topic/public. - Server records this subscription.
- Client sends
- Sending a Message:
- Client types "Hello" and clicks Send.
- Client sends a
SENDframe:SEND destination:/app/chat.send content-type:application/json {"from":"User123","text":"Hello"} - Spring routes this to
ChatController.sendMessage()based on the/app/chat.senddestination. - The controller method uses
messagingTemplateto send a new message to/topic/public. - The Spring broker sends a
MESSAGEframe to all subscribers of/topic/public:MESSAGE subscription:sub-1 destination:/topic/public content-type:application/json {"from":"User123","text":"Hello","timestamp":"2023-10-26T12:00:00"} - All subscribed clients receive and display the message.
Advanced Features and Best Practices
- Authentication: Integrate with Spring Security to secure STOMP endpoints and messages.
- Full-Featured Broker: For production, replace the simple broker with a dedicated one like RabbitMQ or ActiveMQ.
- User Destinations: Send messages to specific users using
@SendToUserand user-specific destinations. - Error Handling: Implement
@MessageExceptionHandlermethods to handle errors gracefully. - Message Acknowledgment: Use client or server acknowledgment modes for reliable messaging.
Conclusion
STOMP over WebSocket provides a powerful, protocol-based alternative to raw WebSocket communication. By leveraging the Spring Framework's comprehensive STOMP support, Java developers can quickly build sophisticated, message-driven applications with clear communication patterns, destination-based routing, and built-in broker capabilities. This combination allows you to focus on application logic rather than building low-level messaging infrastructure, making it an ideal choice for real-time features like chat, notifications, live updates, and collaborative applications.