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.