The WebSocket protocol defines two types of messages (text and binary), but their content is not defined. The protocol defines a mechanism for the client and server to agree on a subprotocol (that is, a higher-level messaging protocol) to be used on top of WebSocket to determine what messages each can send, what their format is, the content of each message, and so on. The use of a subprotocol is optional, but in any case some protocol must be agreed upon between the client and server that defines the content of the message.

Brief description

Protocol STOMP (Simple Text Oriented Messaging Protocol) was originally created for scripting languages (such as Ruby, Python and Perl) to connect to corporate message brokers. It is designed to work with a simple subset of commonly used messaging patterns. STOMP can be used over any reliable bidirectional streaming network protocol such as TCP and WebSocket. Although STOMP is a text-oriented protocol, the message payload can be in either text or binary form.

STOMP is a frame-based protocol whose frames are modeled after HTTP. The following listing shows the structure of the STOMP frame:

COMMAND
header1:value1
header2:value2
Body^@

Clients can use the SEND or SUBSCRIBE commands to send or subscribe to messages along with a destination, which describes what the message is about and who should receive it. This allows you to create a simple publish-subscribe mechanism that can be used to send messages through the broker to other connected clients, or to send messages to the server requesting that specific work be done.

If you are using Spring's STOMP protocol support, A WebSocket application in Spring acts as a STOMP broker for clients. Messages are routed to message processing methods marked with the @Controller annotation, or to a simple in-memory broker that tracks subscriptions and distributes messages to subscribed users. You can also configure Spring to work with a special STOM broker (such as RabbitMQ, ActiveMQ, and others) to actually broadcast messages. In this case, Spring supports establishing TCP connections with the broker, relays messages to it, and forwards messages from it to connected WebSocket clients. In this way, Spring web applications can use unified HTTP-based security, common validation, and a familiar programming model for message processing.

The following example shows a client subscribing to receive stock quotes that the server can generate periodically (for example, through a scheduled task that sends messages via SimpMessagingTemplate to the broker):

SUBSCRIBE
id:sub-1
destination:/topic/price.stock.*
^@

The following example shows a client sending a trade request that the server can process using a method with the annotation @MessageMapping:

SEND
destination:/queue/trade
content-type:application/json
content-length:44
{"action":"BUY","ticker":"MMM","shares",44}^@

Once executed, the server can send a transaction confirmation message and detailed information to the client.

The meaning of the destination address is intentionally left opaque in the STOMP specification. This can be any string, and STOMP servers fully define the semantics and syntax of the destination addresses they support. However, very often destinations are strings like paths, where /topic/.. implies publish-subscribe (one-to-many) and /queue/ implies messaging" point-to-point" (one to one).

STOMP servers can use the MESSAGE command to send messages to all subscribers. The following example shows the server sending a stock quote to a subscribed client:

MESSAGE
message-id:nxahklf6-1
subscription:sub-1
destination:/topic/price.stock.MMM
{"ticker":"MMM","price":129.45}^@

The server cannot send unsolicited messages. All messages from the server must be in response to a specific client subscription, and the subscription-id header of the server message must match the id header of the client's subscription.

Previous short This description is intended to provide a very basic understanding of the STOMP protocol. We recommend that you read the entire protocol specification.

Advantages

Using STOMP as a subprotocol allows the Spring Framework and Spring Security to provide a more rich programming model than using raw WebSockets of the WebSocket protocol. The same can be said about comparing HTTP to raw TCP and how this allows Spring MVC and other web frameworks to provide rich functionality. Below is a list of advantages:

  • No need to invent a custom messaging protocol and message format.

  • STOMP clients, including a Java client , are available in the Spring Framework.

  • You can (optionally) use message brokers (such as RabbitMQ, ActiveMQ, and others) to manage subscriptions and broadcast messages.

  • Application logic can be organized into any number of @Controller instances, and messages can be routed to them based on the STOMP destination address header, as opposed to processing raw WebSocket messages using a single WebSocketHandler for a given connection.

  • You can use Spring Security to protect messages based on STOMP destinations and message types.

STOMP activation

STOMP over WebSocket support is available in the spring-messaging and spring-websocket modules. Once you have these dependencies in place, you can expose STOMP endpoints over WebSocket using a fallback via the SockJS protocol, as shown in the following example:


import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/portfolio").withSockJS(); 
    }
    @Override
    public void configureMessageBroker(MessageBrokerRegistry config) {
        config.setApplicationDestinationPrefixes("/app"); 
        config.enableSimpleBroker("/topic", "/queue"); 
    }
}
  1. /portfolio is the HTTP URL for the endpoint that the WebSocket (or SockJS) client must connect to to confirm the WebSocket handshake.
  2. Messages STOMPs whose destination header begins with /app are routed to methods annotated with @MessageMapping in classes with the annotation @Controller.
  3. We use the built-in message broker for subscription and broadcast and send messages whose destination header begins with /topic ` or `/queue to the broker.

The following example shows the XML equivalent of the configuration from the previous example:


<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:websocket="http://www.springframework.org/schema/websocket"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/websocket
        https://www.springframework.org/schema/websocket/spring-websocket.xsd">
    <websocket:message-broker application-destination-prefix="/app">
        <websocket:stomp-endpoint path="/portfolio">
            <websocket:sockjs/>
        </websocket:stomp-endpoint>
        <websocket:simple-broker prefix="/topic, /queue"/>
    </websocket:message-broker>
</beans>
In the case of a built-in simple broker, the prefixes are /topic and /queue have no special meaning. They are merely a convention to differentiate between publisher-subscriber and peer-to-peer messaging (that is, between many subscribers and a single consumer). If you are using an external broker, refer to the broker's STOMP page to understand what destinations and STOMP prefixes it supports.

To connect from the browser, when using SockJS, you can use sockjs-client. For STOMP, many applications used the jmesnil/stomp-websocket library (also known as stomp.js), which is fully functional and has been used in production for many years, but is no longer supported. Currently JSteunou/webstomp-client is the most actively maintained and developed successor to this library. The following code example is based on it:


var socket = new SockJS("/spring-websocket-portfolio/portfolio");
var stompClient = webstomp.over(socket);
stompClient.connect({}, function(frame) {
}

In addition, if the connection is made via WebSocket (without SockJS), then you can use the following code :


var socket = new WebSocket("/spring-websocket-portfolio/portfolio");
var stompClient = Stomp.over(socket);
stompClient.connect({}, function(frame) {
}

Note that in the previous stompClient example, you do not need to specify the login and passcode headers. Even if you did, they would be ignored (or rather overridden) on the server side.

For more code examples, see:

WebSocket Server

To configure a basic WebSocket server, information from the section "Server" is applicable configuration". In the case of Jetty, however, you need to set HandshakeHandler and WebSocketPolicy via StompEndpointRegistry:


@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/portfolio").setHandshakeHandler(handshakeHandler());
    }
    @Bean
    public DefaultHandshakeHandler handshakeHandler() {
        WebSocketPolicy policy = new WebSocketPolicy(WebSocketBehavior.SERVER);
        policy.setInputBufferSize(8192);
        policy.setIdleTimeout(600000);
        return new DefaultHandshakeHandler(
                new JettyRequestUpgradeStrategy(new WebSocketServerFactory(policy)));
    }
}

Message flow

If the STOMP endpoint is open, the Spring application becomes a STOMP broker for connected clients. This section describes server-side message flow.

The spring-messaging module contains fundamental support for messaging applications that originated in the Spring Integration and were then extracted and incorporated into the Spring Framework for wider use in many Spring projects and application scenarios. The following list briefly describes some of the available messaging abstractions:

Both the Java configuration (that is, the @EnableWebSocketMessageBroker annotation) and the XML namespace configuration (that is is <websocket:message-broker>) use the previous components to assemble the message passing workflow. The following diagram shows the components used when activating a simple built-in message broker:

The previous diagram shows three message channels:

  • clientInboundChannel: For transmitting messages received from WebSocket clients.

  • clientOutboundChannel: For sending server-side messages to clients WebSocket.

  • brokerChannel: To send messages to the message broker from server-side application code.

The following diagram shows the components used if an external broker (such as RabbitMQ) is configured to manage subscriptions and message broadcasts:

The main difference between the two previous schemes is to use a "broker relay" to pass messages to an external STOMP broker over TCP and to downstream messages from the broker to subscribing clients.

If messages are received from a WebSocket connection, they are decoded into STOMP frames, turned into a Message representation from Spring, and sent along the clientInboundChannel for further processing. For example, STOMP messages whose destination headers begin with /app can be routed to methods with the @MessageMapping annotation in annotated controllers, while messages /topic and /queue can be sent directly to the message broker.

An annotated @Controller that handles the STOMP message from the client, can send a message to the message broker via brokerChannel, and the broker broadcasts the message to the appropriate subscribers via clientOutboundChannel. The same controller can do the same in response to HTTP requests, so the client can perform an HTTP POST method, and then the method with the @PostMapping annotation can send a message to the message broker to distribute to subscribers.

We can follow this process with a simple example. Consider the following example in which the server is configured:

 
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/portfolio");
    }
    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
        registry.setApplicationDestinationPrefixes("/app");
        registry.enableSimpleBroker("/topic");
    }
}
@Controller
public class GreetingController {
    @MessageMapping("/greeting")
    public String handle(String greeting) {
        return "[" + getTimestamp() + ": " + greeting;
    }
}

The previous example supports the following flow:

  1. The client connects to http://localhost:8080/portfolio and as soon as the WebSocket connection is established, STOMP frames begin to be transmitted over it.

  2. The client sends a SUBSCRIBE frame with a destination header /topic/greeting. Once received and decoded, the message is sent to the clientInboundChannel and then routed to the message broker, which stores the client's subscription.

  3. The client sends a SEND frame to /app/greeting. The /app prefix helps route it to annotated controllers. After removing the /app prefix, the remainder of the /greeting assignment is mapped to a method annotated with @MessageMapping in the GreetingController.

  4. The value returned by the GreetingController is turned into a Message from Spring with a payload based on the return value and a destination header by default /topic/greeting (derived from the input destination, replacing /app with /topic). The received message is sent to the brokerChannel and processed by the message broker.

  5. The message broker finds all eligible subscribers and sends each of them a MESSAGE frame on the clientOutboundChannel, from where messages are encoded as STOMP frames and sent over a WebSocket connection.

The next section contains more details about annotated methods, including supported argument types and return values.

Annotated controllers

Applications can use classes marked with the @Controller annotation to process messages from clients. Such classes can declare methods with the @MessageMapping, @SubscribeMapping and @ExceptionHandler annotations, as described in the following topics:

  • @MessageMapping

  • @SubscribeMapping

  • @MessageExceptionHandler

@MessageMapping

You can use the @MessageMapping annotation to annotate methods that route messages based on their destination. It is supported at both the method level and the type level. At the type level, the @MessageMapping annotation is used to express common mappings for all controller methods.

By default, mapping values are Ant-style path patterns (for example, /thing* , /thing/**), including support for template variables (for example, /thing/{id}). Values can be referenced through the arguments of a method marked with the @DestinationVariable annotation. Applications can also switch to a convention of destination-separating the message for mappings.

Supported Method Arguments

The following table describes the method arguments:

Method argument Description

Message

Provides access to the complete message.

MessageHeaders

Provides access to headers within a Message.

MessageHeaderAccessor, SimpMessageHeaderAccessor, and StompHeaderAccessor

Provide access to headers through typed access methods.

@Payload

Provides access to the message payload converted (for example, from JSON) using the configured MessageConverter.

This annotation is optional, as it is provided by default if no other argument matches.

You can annotate payload arguments with @javax.validation.Valid or @Validated from Spring so that the payload arguments are validated automatically.

@Header

Provides access to a specific header value - along with a type conversion using org.springframework.core.convert.converter.Converter, if this necessary.

@Headers

Provides access to all headers in the message. This argument must be assignable to java.util.Map.

@DestinationVariable

Provides access to template variables extracted from the message destination. Values are converted if necessary according to the declared type of the method argument.

java.security.Principal

Reflects the user logged in during the WebSocket handshake over HTTP.

Return Values

By default, the return value of a method marked with the @MessageMapping annotation is serialized into a payload via the corresponding MessageConverter and sent as a Message via brokerChannel, from where it is sent to subscribers. The purpose of an outgoing message is the same as that of an incoming message, but with the prefix /topic.

You can use the @SendTo and @SendToUser to configure the destination of the output message. The @SendTo annotation is used to configure a destination or to specify multiple destinations. The @SendToUser annotation is used to direct the output message only to the user associated with the input message.

You can use @SendTo and @SendToUser at the same time code> in the same method, both methods being supported at the class level, in which case they will be the default for methods in the class. However, remember that any annotation with @SendTo or @SendToUser at the method level overrides any similar annotations at the class level.

Messages can be processed asynchronously, and a method with the @MessageMapping annotation can return a ListenableFuture, CompletableFuture, or CompletionStage.

Note Please note that the @SendTo and @SendToUser annotations are simply a convenience measure that is equivalent to using SimpMessagingTemplate to send messages. If necessary, in more complex scenarios, methods marked with the @MessageMapping annotation can fall back to using SimpMessagingTemplate directly. This may occur instead of, or perhaps in addition to, returning a value.

@SubscribeMapping

The @SubscribeMapping annotation is the same as @MessageMapping, but narrows the mapping to only subscription messages. It supports the same method arguments as the @MessageMapping annotation. However, in the case of a return value, by default, the message is sent directly to the client (via clientOutboundChannel, in response to a subscription) rather than to the broker (via brokerChannel, as a broadcast to the appropriate subscriptions). Adding a @SendTo annotation or a @SendToUser annotation overrides this logic and sends messages to the broker instead.

When is this practical? Let's assume that the broker is mapped to /topic and /queue, and the application controllers are mapped to /app. With this configuration, the broker stores all subscriptions to /topic and /queue for repeated mailings, and the application does not need to participate in this. The client can also subscribe to some /app destination address, and the controller can return a value in response to that subscription without broker involvement, without storing or reusing the subscription (effectively a one-time request-response exchange) ). One use case for this mechanism is to populate the UI with initial data at startup.

When is this not practical? Don't try to map broker and controllers to the same destination address prefix unless for some reason you need both to independently process messages, including subscriptions. Incoming messages are processed in parallel. There are no guarantees as to whether the broker or the controller will process this message first. If the goal is to receive notification that the subscription has been saved and is ready to be broadcast, the client must request an acknowledgment of receipt if the server supports it (a simple broker does not). For example, using the STOMP client in Java, you can do the following to add a receipt acknowledgment:


@Autowired
private TaskScheduler messageBrokerTaskScheduler;
// During initialization...
stompClient.setTaskScheduler(this.messageBrokerTaskScheduler);
// When subscribing...
StompHeaders headers = new StompHeaders();
headers.setDestination("/topic/...");
headers.setReceipt("r1");
FrameHandler handler = ...;
stompSession.subscribe(headers, handler).addReceiptTask(receiptHeaders -> {
     // Subscription ready...
});

On the server side you can register ExecutorChannelInterceptor for brokerChannel and implement the afterMessageHandled method, which is called after messages have been processed, including subscriptions.

@MessageExceptionHandler

An application can use methods annotated with @MessageExceptionHandler to handle exceptions from methods annotated with @MessageMapping. You can declare exceptions in the annotation itself or through a method argument if you need to access the exception instance. The following example declares an exception through a method argument:

 
@Controller
public class MyController {
    // ...
    @MessageExceptionHandler
    public ApplicationError handleException(MyException exception) {
        // ...
        return appError;
    }
}

Methods with the @MessageExceptionHandler annotation support flexible method signatures and the same method argument types and return values as methods with the @MessageMapping annotation.

Typically, methods marked with the @MessageExceptionHandler annotation are applied within a class with the @Controller annotation ( or class hierarchy) in which they are declared. If you want such methods to be applied more globally (across all controllers), you can declare them in a class marked with the @ControllerAdvice annotation. This is comparable to similar support available in Spring MVC.

Sending Messages

What if you need to send messages to connected clients from anywhere in the application? Any application component can send messages through brokerChannel. The easiest way to do this is to implement a SimpMessagingTemplate and use it to send messages. Typically, it is embedded by type, as shown in the following example:


@Controller
public class GreetingController {
    private SimpMessagingTemplate template;
    @Autowired
    public GreetingController(SimpMessagingTemplate template) {
        this.template = template;
    }
    @RequestMapping(path="/greetings", method=POST)
    public void greet(String greeting) {
        String text = "[" + getTimestamp() + "]:" + greeting;
        this.template.convertAndSend("/topic/greetings", text);
    }
}

However, you can also identify it by name(brokerMessagingTemplate) if another bean of the same type exists.

Simple Broker

The built-in simple message broker processes subscription requests from clients, stores them in memory, and sends messages to connected clients with suitable destination addresses. The broker supports destination address paths, including subscriptions to Ant-style destination address patterns.

Applications can also use destination addresses separated by dots (rather than slashes) ).

If the task scheduler is configured, the simple broker will support heartbeat - STOMPmessages. To configure the scheduler, you can declare your own TaskScheduler bean and install it via MessageBrokerRegistry. Alternatively, you can use one that is automatically declared in the built-in WebSocket configuration, however you will then need the @Lazy annotation to avoid looping between the built-in WebSocket configuration and your WebSocketMessageBrokerConfigurer. For example:


@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
    private TaskScheduler messageBrokerTaskScheduler;
    @Autowired
    public void setMessageBrokerTaskScheduler(@Lazy TaskScheduler taskScheduler) {
        this.messageBrokerTaskScheduler = taskScheduler;
    }
    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
        registry.enableSimpleBroker("/queue/", "/topic/")
                .setHeartbeatValue(new long[] {10000, 20000})
                .setTaskScheduler(this.messageBrokerTaskScheduler);
        // ...
    }
}

External broker

A simple broker is great for getting started, but only supports some of the STOMP commands ( it does not support ack characters, acknowledgments and some other features), uses a simple message loop and is not suitable for clustering. Alternatively, you can upgrade your applications to use a full-featured message broker.

Refer to the STOMP documentation for your message broker of choice (for example, RabbitMQ, ActiveMQ and others), install the broker and run it with STOMP support enabled. You can then enable the STOMP broker relay (instead of a simple broker) in the Spring configuration.

The following configuration example provides a fully functional broker:


@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/portfolio").withSockJS();
    }
    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
        registry.enableStompBrokerRelay("/topic", "/queue");
        registry.setApplicationDestinationPrefixes("/app");
    }
}

The following example shows the XML equivalent of the configuration from the previous example:


<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:websocket="http://www.springframework.org/schema/websocket"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/websocket
        https://www.springframework.org/schema/websocket/spring-websocket.xsd">
    <websocket:message-broker application-destination-prefix="/app">
        <websocket:stomp-endpoint path="/portfolio" />
            <websocket:sockjs/>
        </websocket:stomp-endpoint>
        <websocket:stomp-broker-relay prefix="/topic,/queue" />
    </websocket:message-broker>
</beans>

The STOMP broker relay in the previous configuration is MessageHandler from Spring, which processes messages by forwarding them to an external broker messages. To do this, it establishes a TCP connection with the broker, forwards all messages to it, and then forwards all messages received from the broker to clients through their WebSocket sessions. Essentially, it acts as a "relay" that forwards messages in both directions.

Add dependencies to your project io.projectreactor.netty:reactor-netty and io.netty:netty-all for managing TCP connections.

Moreover, application components (such as methods for processing HTTP requests, business services and others) can also send messages to the broker relay.

Essentially, the broker relay provides reliable and scalable message distribution.

Connecting to the broker

STOMP broker relay maintains a single "system" TCP connection to the broker. This connection is used solely for messages originating from the server-side application, and not for receiving messages. You can configure STOMP credentials (that is, the login and passcode headers in the STOMP frame) for this connection. This will be exposed in both the XML namespace and the Java configuration as the properties systemLogin and systemPasscode with default values of guest and guest.

The STOMP broker relay also creates a separate TCP connection for each connected WebSocket client. You can configure STOMP credentials that are used for all TCP connections made on behalf of clients. This will be exposed in both the XML namespace and the Java configuration as the properties clientLogin and clientPasscode with default values of guest and guest.

The STOMP broker relay always sets the login and passcode headers in each CONNECT frame that it forwards to the broker on behalf of clients. Therefore, WebSocket clients do not need to set these headers. They are ignored. WebSocket clients must use HTTP authentication to secure the WebSocket endpoint and establish the client's identity.

The STOMP broker relay also sends and receives heartbeat messages to and from the message broker over the "system" TCP connection. You can configure the intervals for sending and receiving heartbeat messages (default 10 seconds). If communication with the broker is lost, the broker relay will continue to try to reestablish the connection every 5 seconds until it fails.

Any Spring bean can implement ApplicationListener<BrokerAvailabilityEvent> to receive notifications if the "system" connection to the broker is lost and then restored. For example, the Stock Quote service that sends stock quotes may stop trying to send messages if there is no active "system" connection.

By default, the STOMP broker relay always connects - and optionally reconnects if the connection is lost - to the same host and port. If you need to provide multiple addresses on each connection attempt, you can configure an address provider instead of a fixed host and port. The following example shows how to do this:

 
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {
    // ...
    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
        registry.enableStompBrokerRelay("/queue/", "/topic/").setTcpClient(createTcpClient());
        registry.setApplicationDestinationPrefixes("/app");
    }
    private ReactorNettyTcpClient<byte[]> createTcpClient() {
        return new ReactorNettyTcpClient<>(
                client -> client.addressSupplier(() -> ... ),
                new StompReactorNettyCodec());
    }
}

You can also configure a STOMP broker relay using the virtualHost property. The value of this property is set as the host header of each CONNECT frame and can be useful (for example, in a cloud environment where the actual host to which the TCP connection is established is different from the host , which provides a cloud STOMP service).

Dots as delimiters

If messages are routed to methods annotated with @MessageMapping, they are matched with AntPathMatcher. By default, templates are expected to use forward slash (/) as the delimiter. This is a good convention for web applications, similar to HTTP URLs. However, if you are more used to messaging conventions, you can switch to using a period (.) as a separator.

The following example shows how to do this using Java. configurations:


@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
    // ...
    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
        registry.setPathMatcher(new AntPathMatcher("."));
        registry.enableStompBrokerRelay("/queue", "/topic");
        registry.setApplicationDestinationPrefixes("/app");
    }
}

The following example shows the XML equivalent of the configuration from the previous example:


<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:websocket="http://www.springframework.org/schema/websocket"
       xsi:schemaLocation="
                http://www.springframework.org/schema/beans
                https://www.springframework.org/schema/beans/spring-beans.xsd
                http://www.springframework.org/schema/websocket
                https://www.springframework.org/schema/websocket/spring-websocket.xsd">
    <websocket:message-broker application-destination-prefix="/app" path-matcher="pathMatcher">
        <websocket:stomp-endpoint path="/stomp"/>
        <websocket:stomp-broker-relay prefix="/topic,/queue" />
    </websocket:message-broker>
    <bean id="pathMatcher" class="org.springframework.util.AntPathMatcher">
        <constructor-arg index="0" value="."/>
    </bean>
</beans>

The controller will then be able to use the dot (.) as a separator in methods marked with the @MessageMapping, as shown in the following example:


@Controller
@MessageMapping("red")
public class RedController {
    @MessageMapping("blue.{green}")
    public void handleGreen(@DestinationVariable String green) {
        // ...
    }
}

The client can now send a message to /app/red.blue.green123.

In the previous example we did not change the prefixes for the "broker relay" because they are entirely dependent on the external message broker. Refer to the STOMP documentation pages for the broker you are using to see what conventions it supports for the destination address header.

A "simple broker", on the other hand, relies on a configured PathMatcher. so if you switch the separator, that change will also apply to the broker and how the broker matches destinations from the message to patterns in subscriptions.

Authentication

Each STOMP messaging session over A WebSocket starts with an HTTP request. This could be a request to pass to the WebSocket protocol (that is, a handshake over the WebSocket protocol) or, in the case of fallbacks via the SockJS protocol, a series of HTTP requests to the transfer mechanisms for SockJS.

Many web applications already have authentication and authorization tools to protect HTTP requests. Typically, the user is authenticated through Spring Security using some mechanism, such as a login page, HTTP Basic Authentication, or another method. The security context for the authenticated user is stored in the HTTP session and associated with subsequent requests in the same session based on the cookie.

So in the case of WebSocket handshake or HTTP requests, transfer mechanisms for SockJS like Typically, an authenticated user already exists, accessible via HttpServletRequest#getUserPrincipal(). Spring automatically associates this user with the WebSocket or SockJS session created for him and, subsequently, with all STOMP messages sent through this session via the user header.

In short, a typical web application does not need to do anything beyond this what it already does to ensure security. The user is authenticated at the HTTP request level using a security context that is maintained at the cookie-based HTTP session level (which is then associated with WebSocket or SockJS sessions created for that user), resulting in a user header being added to every Message passing through the application.

The STOMP protocol does have login and passcode headers in the CONNECT frame. They were originally designed for and are required for using STOMP over TCP. However, when using STOMP over WebSocket, by default, Spring ignores authentication headers at the STOMP protocol level, and assumes that the user is already authenticated at the HTTP transport mechanism level. The WebSocket or SockJS session is expected to contain an authenticated user.

Token Authentication

Project Spring Security OAuthprovides support for token-based security, including JSON Web Token (JWT). You can use it as an authentication mechanism in web applications, including STOMP over WebSocket, as described in the previous section (that is, to preserve identity through a cookie-based session).

At the same time, session-based cookies are not always better (for example, in applications that do not support a server-side session, or in mobile applications that typically use headers for authentication).

WebSocket Protocol, RFC 6455 "does not prescribe any specific way in which servers can authenticate clients during WebSocket handshake." In practice, however, browser clients can only use standard authentication headers (i.e. HTTP basic authentication) or cookies, but cannot (for example) provide custom headers. Likewise, the SockJS JavaScript client does not have the ability to send HTTP headers on SockJS transport requests. See sockjs-client issue 196. Instead, it allows you to send request parameters that you can use to send a token, but this has its drawbacks (for example, the token may accidentally be logged along with the URL in the server logs).

The previous restrictions apply to browser-based clients and do not apply to the Java-based STOMP client in Spring, which supports sending request headers over the WebSocket and SockJS protocols.

So for applications that need to avoid cookies, there may not be good alternatives to HTTP protocol level authentication. In this case, instead of using cookies, you can opt for authentication using headers at the STOMP messaging protocol level. This requires two simple steps:

  1. Use the STOMP client to pass authentication headers during connection.

  2. Process authentication headers with using ChannelInterceptor.

The following example uses server-side configuration to register a custom authentication interceptor. Note that the interceptor only needs to authenticate and set the user header to a Message of the form CONNECT. Spring marks and stores the authenticated user and associates it with subsequent STOMP messages in the same session. The following example shows how to register a custom authentication interceptor:


@Configuration
@EnableWebSocketMessageBroker
public class MyConfig implements WebSocketMessageBrokerConfigurer {
    @Override
    public void configureClientInboundChannel(ChannelRegistration registration) {
        registration.interceptors(new ChannelInterceptor() {
            @Override
            public Message preSend(Message message, MessageChannel channel) {
                StompHeaderAccessor accessor =
                        MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
                if (StompCommand.CONNECT.equals(accessor.getCommand())) {
                    Authentication user = ... ; // access authentication header(s)
                    accessor.setUser(user);
                }
                return message;
            }
        });
    }
}}

Also note that when using Spring Security message authorization, you currently need to ensure that the ChannelInterceptor configuration for authentication is in order before the Spring Security configuration. This is best done by declaring a custom interceptor in your own WebSocketMessageBrokerConfigurer implementation, which is marked with the @Order(Ordered.HIGHEST_PRECEDENCE + 99) annotation.

Authorization

Spring Security provides sub-protocol authorization WebSocket, which uses a ChannelInterceptor to authorize messages based on the user header they contain. In addition, Spring Session provides WebSocket protocol integration, which ensures that the user's HTTP session will not expire while the WebSocket session is still active.

Custom destination addresses

An application can send messages addressed to a specific user, and Spring's STOMP facilities support this recognize destination addresses with the prefix /user/. For example, a client might subscribe to the destination address /user/queue/position-updates. The UserDestinationMessageHandler processes this destination address and translates it into a destination address unique to the user's session (for example, /queue/position-updates-user123). This provides the convenience of subscribing to a common destination address while ensuring that there are no conflicts with other users subscribed to the same destination address, so that each user can receive unique stock position updates.

When working with custom destination addresses, it is important to configure the broker and application destination address prefixes, otherwise the broker will process messages with the "/user" prefix, which should only be processed by UserDestinationMessageHandler.

On the transmission side, messages can be sent to a destination address such as /user/{username}/queue/position-updates, which in turn is translated by UserDestinationMessageHandler to one or more destinations, one for each session associated with the user. This allows any application component to send messages targeted to a specific user without knowing anything more than the user's name and general destination address. This can also be done using the messaging annotation and template.

The message handling method can send messages to the user associated with the message being processed using the @SendToUser annotation (also supported at the class level for a general destination), as shown in the following example:


@Controller
public class PortfolioController {
    @MessageMapping("/trade")
    @SendToUser("/queue/position-updates")
    public TradeResult executeTrade(Trade trade, Principal principal) {
        // ...
        return tradeResult;
    }
}

If a user has more than one session, by default all sessions subscribed to the given destination address are targeted. However, sometimes you may want to target only the session that sent the message being processed. You can do this by setting the broadcast attribute to false, as shown in the following example:


@Controller
public class MyController {
    @MessageMapping("/action")
    public void handleAction() throws Exception{
        // MyBusinessException is thrown here
    }
    @MessageExceptionHandler
    @SendToUser(destinations="/queue/errors", broadcast=false)
    public ApplicationError handleException(MyBusinessException exception) {
        // ...
        return appError;
    }
}
Although user destinations typically assume an authenticated user, this is not strictly required. A WebSocket session not associated with an authenticated user can subscribe to the user's destination address. In such cases, the @SendToUser annotation functions exactly the same as with broadcast=false (that is, it targets only the session that sent the message being processed).

You can send a message to custom destinations from any application component, for example by implementing a SimpMessagingTemplate generated by a Java configuration or XML namespace. (The bean name is brokerMessagingTemplate if required to fully qualify the name using the @Qualifier annotation). The following example shows how to do this:

 
@Service
public class TradeServiceImpl implements TradeService {
    private final SimpMessagingTemplate messagingTemplate;
    @Autowired
    public TradeServiceImpl(SimpMessagingTemplate messagingTemplate) {
        this.messagingTemplate = messagingTemplate;
    }
    // ...
    public void afterTradeExecuted(Trade trade) {
        this.messagingTemplate.convertAndSendToUser(
                trade.getUserName(), "/queue/position-updates", trade.getResult());
    }
}
If you are using custom destinations with an external message broker, you should review the documentation for to the broker regarding how to manage inactive queues so that when the user session ends, all of the user's unique queues are deleted. For example, RabbitMQ creates auto-deleting queues when destinations such as /exchange/amq.direct/position-updates are used. In this case, the client can subscribe to /user/exchange/amq.direct/position-updates. Likewise, ActiveMQ has configuration options for cleaning up inactive destinations.

In a scenario with multiple application servers, the user's destination address may remain unresolved because the user is connected to another server. In such cases, you can configure the destination address to broadcast unresolved messages so that other servers can try to do so. This can be done using the userDestinationBroadcast property of the MessageBrokerRegistry registry in the Java configuration and the user-destination-broadcast attribute of the message-broker element in XML.

Message order

Messages from the broker are published through the clientOutboundChannel channel, from where they are written to WebSocket sessions. Because the channel is backed by a ThreadPoolExecutor, messages are processed in different threads, and the resulting sequence received by the client may not match the exact order of publication.

If this is a problem, then enable the setPreservePublishOrder, as shown in the following example:


@Configuration
@EnableWebSocketMessageBroker
public class MyConfig implements WebSocketMessageBrokerConfigurer {
    @Override
    protected void configureMessageBroker(MessageBrokerRegistry registry) {
        // ...
        registry.setPreservePublishOrder(true);
    }
}

The following example shows the XML equivalent of the configuration from the previous example:


<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:websocket="http://www.springframework.org/schema/websocket"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/websocket
        https://www.springframework.org/schema/websocket/spring-websocket.xsd">
    <websocket:message-broker preserve-publish-order="true">
            <!-- ... -->
    </websocket:message-broker>
</beans>

If the flag is set, messages within the same client session are published through the clientOutboundChannel one at a time, which ensures the required publication order. Note that this results in a slight performance hit, so you should only enable this flag if necessary.

Events

Multiple ApplicationContext events are published and can be obtained by implementing the ApplicationListener interface for Spring:

  • BrokerAvailabilityEvent: Indicates when the broker becomes available or unavailable. Although a "simple" broker becomes available immediately upon startup and remains so while the application is running, the STOMP "broker relay" may lose connection to a full-featured broker (for example, when the broker is restarted). The broker relay contains reconnection logic and re-establishes a "system" connection to the broker when it is reactivated. As a result, this event is published whenever the state changes from connected to disconnected and vice versa. Components using SimpMessagingTemplate need to subscribe to this event and avoid sending messages while the broker is unavailable. In any case, they must be prepared to handle MessageDeliveryException when a message is sent.

  • SessionConnectEvent: Published when a new frame is received CONNECT of the STOMP protocol to indicate the start of a new client session. The event contains a message representing the connection, including the session ID, user information (if any), and any custom headers sent by the client. This is useful for tracking client sessions. Components that subscribe to this event can wrap the contained message using SimpMessageHeaderAccessor or StompMessageHeaderAccessor.

  • SessionConnectedEvent: Published shortly after a SessionConnectEvent when the broker sent a STOMP protocol CONNECTED frame in response to a CONNECT frame. At this point, the STOMP session can be considered fully established.

  • SessionSubscribeEvent: Published when a new STOMP protocol SUBSCRIBE frame is received.

  • SessionUnsubscribeEvent: Published when a new STOMP protocol UNSUBSCRIBE frame is received.

  • SessionDisconnectEvent: Published when completion of the STOMP session. The DISCONNECT frame can be sent from the client or automatically generated when the WebSocket session is closed. In some cases, this event is published more than once per session. Components must be idempotent with respect to multiple connection loss events.

If you are using a full-featured broker, a "broker relay" STOMP automatically re-establishes a "system" connection if the broker becomes temporarily unavailable. However, client connections are not automatically re-established. If heartbeat messages are enabled, the client usually reacts when the broker does not respond within 10 seconds. Clients must implement their own connection re-establishment logic.

Interception

Events report notifications about the lifecycle of a STOMP connection, but not about every client message. Applications can also register a ChannelInterceptor to intercept any message anywhere in the processing chain. The following example shows how to intercept incoming messages from clients:


@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
    @Override
    public void configureClientInboundChannel(ChannelRegistration registration) {
        registration.interceptors(new MyChannelInterceptor());
    }
}

A custom ChannelInterceptor can use StompHeaderAccessor or SimpMessageHeaderAccessor to access message information, as shown in the following example:


public class MyChannelInterceptor implements ChannelInterceptor {
    @Override
    public Message preSend(Message message, MessageChannel channel) {
        StompHeaderAccessor accessor = StompHeaderAccessor.wrap(message);
        StompCommand command = accessor.getStompCommand();
        // ...
        return message;
    }
}

Applications can also implement ExecutorChannelInterceptor, which is a subinterface of ChannelInterceptor with in-thread callbacks , in which messages are processed. Although the ChannelInterceptor is called once for each message sent to the channel, the ExecutorChannelInterceptor provides interceptors on the thread of each MessageHandler that subscribes to messages from the channel.

Note that, as with the SessionDisconnectEvent described earlier, a DISCONNECT message may be received from the client and may also be automatically generated when the WebSocket session is closed. In some cases, an eavesdropper may intercept this message more than once for each session. Components must be idempotent with respect to multiple connection loss events.

STOMP Client

Spring provides a STOMP client over WebSocket and a STOMP client over TCP.

For starters, you can create and configure a WebSocketStompClient as shown in the following example:


WebSocketClient webSocketClient = new StandardWebSocketClient();
WebSocketStompClient stompClient = new WebSocketStompClient(webSocketClient);
stompClient.setMessageConverter(new StringMessageConverter());
stompClient.setTaskScheduler(taskScheduler); // for heartbeats

In the previous example, you can replace StandardWebSocketClient with SockJsClient, since it is also implementation of WebSocketClient. SockJsClient can use WebSocket or an HTTP-based transport mechanism as a fallback.

You can then establish a connection and provide a handler for the STOMP session, as shown in the following example:


String url = "ws://127.0. 0.1:8080/endpoint";
StompSessionHandler sessionHandler = new MyStompSessionHandler();
stompClient.connect(url, sessionHandler);

When the session is ready to be used, the handler will be notified, as shown in the following example:


public class MyStompSessionHandler extends StompSessionHandlerAdapter {
    @Override
    public void afterConnected(StompSession session, StompHeaders connectedHeaders) {
        // ...
    }
}

Once the session is established, you can send any payload that is serialized using configured MessageConverter, as shown in the following example:

session.send("/topic/something", "payload");

You can also subscribe to destination addresses. The subscribe methods require a message handler for the subscription and return a Subscription handle that you can use to unsubscribe. For each message received, the handler can specify a target type Object into which the payload should be deserialized, as shown in the following example:


session.subscribe("/topic/something", new StompFrameHandler() {
    @Override
    public Type getPayloadType(StompHeaders headers) {
        return String.class;
    }
    @Override
    public void handleFrame(StompHeaders headers, Object payload) {
        // ...
    }
});

To enable STOMP heartbeat messages, you can configure WebSocketStompClient using TaskScheduler and optionally configure heartbeat message transmission intervals (10 seconds for no write activity, which will cause a heartbeat message to be sent, and 10 seconds for no read activity, which will cause the connection to be closed).

WebSocketStompClient only sends a heartbeat message if there is no activity, i.e. when no other messages are being sent. This can be a problem when using an external broker because messages with a non-broker destination reflect activity but are not actually forwarded to the broker. In this case, you can configure TaskScheduler when initializing the external broker, which ensures that the heartbeat message is forwarded to the broker also if only messages with a non-broker destination address are sent.

If you are using WebSocketStompClient for performance tests to simulate thousands of clients from a single machine, consider disabling heartbeat messages since each connection schedules its own heartbeats -tasks, and there is no optimization for this process for a large number of clients running on the same machine.

The STOMP protocol also provides support for acknowledgment of receipt if the client must add a receipt header to which the server responds with a RECEIPT frame after processing the send or subscription. To provide this support, StompSession offers setAutoReceipt(boolean), which ensures that the receipt header is added on each subsequent send or subscribe event. You can also manually add a receipt acknowledgment header to StompHeaders. Both send and subscribe return a Receiptable instance that can be used to register receive success and failure callbacks. This feature requires that the client be configured to use TaskScheduler and set the amount of time before the receipt acknowledgment expires (default is 15 seconds).

Note that StompSessionHandler is itself a StompFrameHandler, which allows it to handle ERROR frames in addition to the handleException callback intended for exceptions thrown during message processing and handleTransportError targeting transmission-level errors, including ConnectionLostException.

WebSocket protocol reachability

Each WebSocket session has a Map of attributes. Map is bound as a header to incoming client messages and can be accessed from a controller method, as shown in the following example:


@Controller
public class MyController {
    @MessageMapping("/action")
    public void handle(SimpMessageHeaderAccessor headerAccessor) {
        Map<String, Object> attrs = headerAccessor.getSessionAttributes();
        // ...
    }
}

You can declare a Spring managed bean in the websocket availability scope. You can inject WebSocket-scoped beans into controllers and any channel hooks registered through clientInboundChannel. As a rule, they are singletons, and their life cycle is longer than each individual WebSocket session. Therefore, beans that are part of a WebSocket accessibility scope must use proxy mode for the accessibility scope, as shown in the following example:


@Component
@Scope(scopeName = "websocket", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class MyBean {
    @PostConstruct
    public void init() {
        // Called after dependency injection
    }
    // ...
    @PreDestroy
    public void destroy() {
        // Called after dependency injection
    }
}
@Controller
public class MyController {
    private final MyBean myBean;
    @Autowired
    public MyController(MyBean myBean) {
        this.myBean = myBean;
    }
    @MessageMapping("/action")
    public void handle() {
        // this.myBean from the current WebSocket session
    }
}

Like any custom scope availability, Spring initializes a new instance of MyBean the first time it is accessed from the controller and stores it in the WebSocket session attributes. The same copy is subsequently returned before the end of the session. For beans within the WebSocket reachability scope, all Spring lifecycle methods are called, as shown in the previous examples.

Performance

When it comes to performance, there is no one right solution. It is affected by many factors, including the size and volume of messages, whether the application is performing work that requires blocking, and external factors (such as network speed and other details). The purpose of this section is to briefly describe the available configuration options and to provide some ideas and discussion about scaling.

In a messaging application, messages are passed through channels for asynchronous execution, which are supported by thread pools. Configuring such an application requires a good understanding of how channels and message flow work.

The obvious place to start is by setting up thread pools that support clientInboundChannel and clientOutboundChannel. By default, both configurations are set to twice the number of available processors.

If message processing in annotated methods is primarily processor-dependent, the number of threads for clientInboundChannel should remain close to the number of processors. If the work they do is more I/O intensive and requires blocking or waiting in a database or other external system, the thread pool size probably needs to be increased.

ThreadPoolExecutor has three important properties: the size of the main thread pool, the maximum thread pool size, and the queue capacity to hold tasks for which there are no free threads.

Confusion often arises : Configuring the main pool size (for example, 10) and the maximum pool size (for example, 20) results in a thread pool with 10-20 threads. In fact, if you leave the default capacity value of Integer.MAX_VALUE, the thread pool will never exceed the size of the main pool, since all additional tasks are queued.

See the javadoc on ThreadPoolExecutor, to learn how these properties work and become familiar with different queuing strategies.

The clientOutboundChannel side is about sending messages to WebSocket clients. If clients are on a fast network, the number of threads should remain close to the number of available processors. If they are slow or have low throughput, they take longer to consume messages and put a strain on the thread pool. Therefore, the size of the thread pool needs to be increased.

While the workload for the clientInboundChannel channel can be predicted - after all, it depends on what the application is doing - configuring the clientOutboundChannel is more difficult , since its operation is based on factors independent of the application. For this reason, two additional properties are associated with sending messages: sendTimeLimit and sendBufferSizeLimit. You can use these methods to configure the send duration and amount of data to be buffered when sending messages to the client.

The general idea is that only one thread can be used to send to the client at any time . All additional messages are buffered in the meantime, and you can use these properties to decide how long a message should take to send and how much data can be buffered during that time. Please refer to the javadoc and XML schema documentation for important additional details.

The following example shows a possible configuration:


@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
    @Override
    public void configureWebSocketTransport(WebSocketTransportRegistration registration) {
        registration.setSendTimeLimit(15 * 1000).setSendBufferSizeLimit(512 * 1024);
    }
    // ...
}

The following example shows the XML equivalent of the configuration from the previous example:


<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:websocket="http://www.springframework.org/schema/websocket"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/websocket
        https://www.springframework.org/schema/websocket/spring-websocket.xsd">
    <websocket:message-broker>
        <websocket:transport send-timeout="15000" send-buffer-size="524288" />
        <!-- ... -->
    </websocket:message-broker>
</beans>

You can also use the WebSocket transport mechanism configuration shown earlier to configure the maximum allowed size of incoming STOMP messages. In theory, the size of a WebSocket message can be virtually unlimited. In practice, WebSocket servers set limits - for example, 8 KB for Tomcat and 64 KB for Jetty. For this reason, STOMP clients (such as webstomp-client from JavaScript and others) break large STOMP messages when reaches 16 KB in size and sends them as multiple WebSocket messages, which requires the server to buffer and reassemble.

Spring's STOMP over WebSocket support does this so applications can configure the maximum size of STOMP messages independently on message sizes specific to the WebSocket server. Remember that the WebSocket message size is automatically adjusted as necessary to ensure that WebSocket messages are sent at least 16 KB in size.

The following example shows one possible configuration:


@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
    @Override
    public void configureWebSocketTransport(WebSocketTransportRegistration registration) {
        registration.setMessageSizeLimit(128 * 1024);
    }
    // ...
}

The following example shows the XML equivalent of the configuration from the previous example:


<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:websocket="http://www.springframework.org/schema/websocket"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/websocket
        https://www.springframework.org/schema/websocket/spring-websocket.xsd">
    <websocket:message-broker>
        <websocket:transport message-size="131072" />
            <!-- ... -->
    </websocket:message-broker>
</beans>

An important point in scaling is the use of multiple application instances. Currently it is not possible to do this with a simple broker. However, when using a full-featured broker (such as RabbitMQ), each application instance is connected to the broker, and messages sent by one application instance can be sent through the broker to WebSocket clients connected through any other application instances.

Monitoring

If you use the @EnableWebSocketMessageBroker or <websocket:message-broker> annotation, key infrastructure components automatically collect statistics and counters that provide important insight into internal state applications. The configuration also declares a bean of type WebSocketMessageBrokerStats, which collects all available information in one place and by default registers it at the INFO level once every 30 minutes. This bean can be exported to JMX via MBeanExporter from Spring for runtime viewing (for example via jconsole from JDK). The following list provides a summary:

WebSocket Client Sessions
Current

Shows how many client sessions currently exist, with a further breakdown by WebSocket streaming versus HTTP and polling sessions via SockJS protocol.

Total

Indicates how many total sessions were established.

Emergency closed
Connect Failures

Sessions that were established but were closed after no messages were received for 60 seconds. This usually indicates problems with the proxy server or network.

Send Limit Exceeded

Sessions are closed after exceeding configured send timeout or send buffer limit, which can happen with slow clients (see previous section).

Transport Errors

Sessions are closed after a transmission error, such as the WebSocket connection or HTTP request or response being unable to read or write.

STOMP Frames

The total number of CONNECT, CONNECTED and DISCONNECT frames processed, indicating how many clients connected at the STOMP level. Note that the DISCONNECT frame counter may be lower if sessions are closed abnormally or if clients are closed without sending a DISCONNECT frame.

STOMP Broker Relay
TCP connections

Indicates how many TCP connections on behalf of client WebSocket sessions are established with the broker. The value should be equal to the number of client WebSocket sessions + 1 additional general "system" connection for sending messages from the application.

STOMP Frames

The total number of CONNECT, CONNECTED, and DISCONNECT frames sent to or received from the broker on behalf of clients. Note that the DISCONNECT frame is sent to the broker regardless of how the client WebSocket session was closed. Therefore, fewer DISCONNECT frames are a sign that the broker is actively closing connections (perhaps due to a mistimed heartbeat, invalid input frame, or other problem).

Client Inbound Channel

Statistics from the thread pool supporting clientInboundChannel that provide insight into the processing status of inbound messages. Queued tasks are a sign that the application may be processing messages too slowly. If you have I/O-intensive tasks (such as slow database queries, HTTP requests to a third-party REST API, and so on), consider increasing the thread pool size.

Client Outbound Channel

Statistics from the thread pool supporting clientOutboundChannel that provide insight into the health of message broadcasts to clients. Queued tasks are a sign that clients are too slow to accept messages. One way to solve this problem is to increase the size of the thread pool to accommodate the expected number of concurrent slow clients. Another option is to reduce the limits on send timeout and send buffer size (see previous section).

SockJS Task Scheduler

Statistics from the SockJS task scheduler thread pool, which is used to send heartbeat messages. Please note that when negotiating heartbeat messages at the STOMP level, sending heartbeat messages for SockJS is disabled.

Testing

There are two main approach to application testing when you use Spring's STOMP support over WebSocket. The first is to write server-side tests to test the functionality of the controllers and their annotated message handling methods. The second is to write complete end-to-end tests that include running both the client and the server.

The two approaches are not mutually exclusive. On the contrary, each of them has its own place in the overall testing strategy. Server-side tests are more focused and easier to write and maintain. On the other hand, end-to-end integration tests are more comprehensive and cover much more, but they are also more complex to write and maintain.

The simplest form of server-side testing is to write controller unit tests. However, this is not practical enough because much of what the controller does depends on its annotations. Pure unit tests simply won't test how it works.

Ideally, the controllers under test should be called as they are during program execution, similar to the approach to testing controllers that handle HTTP requests using Spring MVC Test framework - that is, without running a servlet container, but using the Spring Framework to call annotated controllers. As with the Spring MVC Test, here you have two possible alternatives: use a "context-specific" or "standalone" configuration:

  • Load the actual Spring configuration using the Spring framework TestContext, implement clientInboundChannel as a test field and use it to send messages that will be processed by the controller methods.

  • Manually install the minimum Spring framework infrastructure required to call controllers (namely SimpAnnotationMethodMessageHandler) and pass messages for controllers directly to it.

Both of these configuration scenarios are demonstrated in the sample application tests for stock portfolio.

The second approach is to create end-to-end integration tests. To do this, you need to run the WebSocket server in embedded mode and connect to it as a WebSocket client that sends WebSocket messages containing STOMP frames. Sample application tests Stock Portfolioalso takes this approach, using Tomcat as an embedded WebSocket server and a simple STOMP client for testing purposes.