spring-framework/framework-docs/modules/ROOT/pages/web/websocket.adoc

2514 lines
104 KiB
Plaintext

[[websocket]]
= WebSockets
[.small]#<<web-reactive.adoc#webflux-websocket, See equivalent in the Reactive stack>>#
This part of the reference documentation covers support for Servlet stack, WebSocket
messaging that includes raw WebSocket interactions, WebSocket emulation through SockJS, and
publish-subscribe messaging through STOMP as a sub-protocol over WebSocket.
include::websocket-intro.adoc[leveloffset=+1]
[[websocket-server]]
== WebSocket API
[.small]#<<web-reactive.adoc#webflux-websocket-server, See equivalent in the Reactive stack>>#
The Spring Framework provides a WebSocket API that you can use to write client- and
server-side applications that handle WebSocket messages.
[[websocket-server-handler]]
=== `WebSocketHandler`
[.small]#<<web-reactive.adoc#webflux-websocket-server-handler, See equivalent in the Reactive stack>>#
Creating a WebSocket server is as simple as implementing `WebSocketHandler` or, more
likely, extending either `TextWebSocketHandler` or `BinaryWebSocketHandler`. The following
example uses `TextWebSocketHandler`:
[source,java,indent=0,subs="verbatim,quotes"]
----
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.TextMessage;
public class MyHandler extends TextWebSocketHandler {
@Override
public void handleTextMessage(WebSocketSession session, TextMessage message) {
// ...
}
}
----
There is dedicated WebSocket Java configuration and XML namespace support for mapping the preceding
WebSocket handler to a specific URL, as the following example shows:
[source,java,indent=0,subs="verbatim,quotes"]
----
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(myHandler(), "/myHandler");
}
@Bean
public WebSocketHandler myHandler() {
return new MyHandler();
}
}
----
The following example shows the XML configuration equivalent of the preceding example:
[source,xml,indent=0,subs="verbatim,quotes,attributes"]
----
<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:handlers>
<websocket:mapping path="/myHandler" handler="myHandler"/>
</websocket:handlers>
<bean id="myHandler" class="org.springframework.samples.MyHandler"/>
</beans>
----
The preceding example is for use in Spring MVC applications and should be included
in the configuration of a <<mvc-servlet, `DispatcherServlet`>>. However, Spring's
WebSocket support does not depend on Spring MVC. It is relatively simple to
integrate a `WebSocketHandler` into other HTTP-serving environments with the help of
{api-spring-framework}/web/socket/server/support/WebSocketHttpRequestHandler.html[`WebSocketHttpRequestHandler`].
When using the `WebSocketHandler` API directly vs indirectly, e.g. through the
<<websocket-stomp>> messaging, the application must synchronize the sending of messages
since the underlying standard WebSocket session (JSR-356) does not allow concurrent
sending. One option is to wrap the `WebSocketSession` with
{api-spring-framework}/web/socket/handler/ConcurrentWebSocketSessionDecorator.html[`ConcurrentWebSocketSessionDecorator`].
[[websocket-server-handshake]]
=== WebSocket Handshake
[.small]#<<web-reactive.adoc#webflux-websocket-server-handshake, See equivalent in the Reactive stack>>#
The easiest way to customize the initial HTTP WebSocket handshake request is through
a `HandshakeInterceptor`, which exposes methods for "`before`" and "`after`" the handshake.
You can use such an interceptor to preclude the handshake or to make any attributes
available to the `WebSocketSession`. The following example uses a built-in interceptor
to pass HTTP session attributes to the WebSocket session:
[source,java,indent=0,subs="verbatim,quotes"]
----
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(new MyHandler(), "/myHandler")
.addInterceptors(new HttpSessionHandshakeInterceptor());
}
}
----
The following example shows the XML configuration equivalent of the preceding example:
[source,xml,indent=0,subs="verbatim,quotes,attributes"]
----
<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:handlers>
<websocket:mapping path="/myHandler" handler="myHandler"/>
<websocket:handshake-interceptors>
<bean class="org.springframework.web.socket.server.support.HttpSessionHandshakeInterceptor"/>
</websocket:handshake-interceptors>
</websocket:handlers>
<bean id="myHandler" class="org.springframework.samples.MyHandler"/>
</beans>
----
A more advanced option is to extend the `DefaultHandshakeHandler` that performs
the steps of the WebSocket handshake, including validating the client origin,
negotiating a sub-protocol, and other details. An application may also need to use this
option if it needs to configure a custom `RequestUpgradeStrategy` in order to
adapt to a WebSocket server engine and version that is not yet supported
(see <<websocket-server-deployment>> for more on this subject).
Both the Java configuration and XML namespace make it possible to configure a custom
`HandshakeHandler`.
TIP: Spring provides a `WebSocketHandlerDecorator` base class that you can use to decorate
a `WebSocketHandler` with additional behavior. Logging and exception handling
implementations are provided and added by default when using the WebSocket Java configuration
or XML namespace. The `ExceptionWebSocketHandlerDecorator` catches all uncaught
exceptions that arise from any `WebSocketHandler` method and closes the WebSocket
session with status `1011`, which indicates a server error.
[[websocket-server-deployment]]
=== Deployment
The Spring WebSocket API is easy to integrate into a Spring MVC application where
the `DispatcherServlet` serves both HTTP WebSocket handshake and other
HTTP requests. It is also easy to integrate into other HTTP processing scenarios
by invoking `WebSocketHttpRequestHandler`. This is convenient and easy to
understand. However, special considerations apply with regards to JSR-356 runtimes.
The Jakarta WebSocket API (JSR-356) provides two deployment mechanisms. The first
involves a Servlet container classpath scan (a Servlet 3 feature) at startup.
The other is a registration API to use at Servlet container initialization.
Neither of these mechanism makes it possible to use a single "`front controller`"
for all HTTP processing -- including WebSocket handshake and all other HTTP
requests -- such as Spring MVC's `DispatcherServlet`.
This is a significant limitation of JSR-356 that Spring's WebSocket support addresses with
server-specific `RequestUpgradeStrategy` implementations even when running in a JSR-356 runtime.
Such strategies currently exist for Tomcat, Jetty, GlassFish, WebLogic, WebSphere, and Undertow
(and WildFly). As of Jakarta WebSocket 2.1, a standard request upgrade strategy is available
which Spring chooses on Jakarta EE 10 based web containers such as Tomcat 10.1 and Jetty 12.
A secondary consideration is that Servlet containers with JSR-356 support are expected
to perform a `ServletContainerInitializer` (SCI) scan that can slow down application
startup -- in some cases, dramatically. If a significant impact is observed after an
upgrade to a Servlet container version with JSR-356 support, it should
be possible to selectively enable or disable web fragments (and SCI scanning)
through the use of the `<absolute-ordering />` element in `web.xml`, as the following example shows:
[source,xml,indent=0,subs="verbatim,quotes,attributes"]
----
<web-app xmlns="https://jakarta.ee/xml/ns/jakartaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
https://jakarta.ee/xml/ns/jakartaee
https://jakarta.ee/xml/ns/jakartaee/web-app_5_0.xsd"
version="5.0">
<absolute-ordering/>
</web-app>
----
You can then selectively enable web fragments by name, such as Spring's own
`SpringServletContainerInitializer` that provides support for the Servlet 3
Java initialization API. The following example shows how to do so:
[source,xml,indent=0,subs="verbatim,quotes,attributes"]
----
<web-app xmlns="https://jakarta.ee/xml/ns/jakartaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
https://jakarta.ee/xml/ns/jakartaee
https://jakarta.ee/xml/ns/jakartaee/web-app_5_0.xsd"
version="5.0">
<absolute-ordering>
<name>spring_web</name>
</absolute-ordering>
</web-app>
----
[[websocket-server-runtime-configuration]]
=== Server Configuration
[.small]#<<web-reactive.adoc#webflux-websocket-server-config, See equivalent in the Reactive stack>>#
Each underlying WebSocket engine exposes configuration properties that control
runtime characteristics, such as the size of message buffer sizes, idle timeout,
and others.
For Tomcat, WildFly, and GlassFish, you can add a `ServletServerContainerFactoryBean` to your
WebSocket Java config, as the following example shows:
[source,java,indent=0,subs="verbatim,quotes"]
----
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
@Bean
public ServletServerContainerFactoryBean createWebSocketContainer() {
ServletServerContainerFactoryBean container = new ServletServerContainerFactoryBean();
container.setMaxTextMessageBufferSize(8192);
container.setMaxBinaryMessageBufferSize(8192);
return container;
}
}
----
The following example shows the XML configuration equivalent of the preceding example:
[source,xml,indent=0,subs="verbatim,quotes,attributes"]
----
<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">
<bean class="org.springframework...ServletServerContainerFactoryBean">
<property name="maxTextMessageBufferSize" value="8192"/>
<property name="maxBinaryMessageBufferSize" value="8192"/>
</bean>
</beans>
----
NOTE: For client-side WebSocket configuration, you should use `WebSocketContainerFactoryBean`
(XML) or `ContainerProvider.getWebSocketContainer()` (Java configuration).
For Jetty, you need to supply a pre-configured Jetty `WebSocketServerFactory` and plug
that into Spring's `DefaultHandshakeHandler` through your WebSocket Java config.
The following example shows how to do so:
[source,java,indent=0,subs="verbatim,quotes"]
----
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(echoWebSocketHandler(),
"/echo").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)));
}
}
----
The following example shows the XML configuration equivalent of the preceding example:
[source,xml,indent=0,subs="verbatim,quotes,attributes"]
----
<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:handlers>
<websocket:mapping path="/echo" handler="echoHandler"/>
<websocket:handshake-handler ref="handshakeHandler"/>
</websocket:handlers>
<bean id="handshakeHandler" class="org.springframework...DefaultHandshakeHandler">
<constructor-arg ref="upgradeStrategy"/>
</bean>
<bean id="upgradeStrategy" class="org.springframework...JettyRequestUpgradeStrategy">
<constructor-arg ref="serverFactory"/>
</bean>
<bean id="serverFactory" class="org.eclipse.jetty...WebSocketServerFactory">
<constructor-arg>
<bean class="org.eclipse.jetty...WebSocketPolicy">
<constructor-arg value="SERVER"/>
<property name="inputBufferSize" value="8092"/>
<property name="idleTimeout" value="600000"/>
</bean>
</constructor-arg>
</bean>
</beans>
----
[[websocket-server-allowed-origins]]
=== Allowed Origins
[.small]#<<web-reactive.adoc#webflux-websocket-server-cors, See equivalent in the Reactive stack>>#
As of Spring Framework 4.1.5, the default behavior for WebSocket and SockJS is to accept
only same-origin requests. It is also possible to allow all or a specified list of origins.
This check is mostly designed for browser clients. Nothing prevents other types
of clients from modifying the `Origin` header value (see
https://tools.ietf.org/html/rfc6454[RFC 6454: The Web Origin Concept] for more details).
The three possible behaviors are:
* Allow only same-origin requests (default): In this mode, when SockJS is enabled, the
Iframe HTTP response header `X-Frame-Options` is set to `SAMEORIGIN`, and JSONP
transport is disabled, since it does not allow checking the origin of a request.
As a consequence, IE6 and IE7 are not supported when this mode is enabled.
* Allow a specified list of origins: Each allowed origin must start with `http://`
or `https://`. In this mode, when SockJS is enabled, IFrame transport is disabled.
As a consequence, IE6 through IE9 are not supported when this
mode is enabled.
* Allow all origins: To enable this mode, you should provide `{asterisk}` as the allowed origin
value. In this mode, all transports are available.
You can configure WebSocket and SockJS allowed origins, as the following example shows:
[source,java,indent=0,subs="verbatim,quotes"]
----
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(myHandler(), "/myHandler").setAllowedOrigins("https://mydomain.com");
}
@Bean
public WebSocketHandler myHandler() {
return new MyHandler();
}
}
----
The following example shows the XML configuration equivalent of the preceding example:
[source,xml,indent=0,subs="verbatim,quotes,attributes"]
----
<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:handlers allowed-origins="https://mydomain.com">
<websocket:mapping path="/myHandler" handler="myHandler" />
</websocket:handlers>
<bean id="myHandler" class="org.springframework.samples.MyHandler"/>
</beans>
----
[[websocket-fallback]]
== SockJS Fallback
Over the public Internet, restrictive proxies outside your control may preclude WebSocket
interactions, either because they are not configured to pass on the `Upgrade` header or
because they close long-lived connections that appear to be idle.
The solution to this problem is WebSocket emulation -- that is, attempting to use WebSocket
first and then falling back on HTTP-based techniques that emulate a WebSocket
interaction and expose the same application-level API.
On the Servlet stack, the Spring Framework provides both server (and also client) support
for the SockJS protocol.
[[websocket-fallback-sockjs-overview]]
=== Overview
The goal of SockJS is to let applications use a WebSocket API but fall back to
non-WebSocket alternatives when necessary at runtime, without the need to
change application code.
SockJS consists of:
* The https://github.com/sockjs/sockjs-protocol[SockJS protocol]
defined in the form of executable
https://sockjs.github.io/sockjs-protocol/sockjs-protocol-0.3.3.html[narrated tests].
* The https://github.com/sockjs/sockjs-client/[SockJS JavaScript client] -- a client library for use in browsers.
* SockJS server implementations, including one in the Spring Framework `spring-websocket` module.
* A SockJS Java client in the `spring-websocket` module (since version 4.1).
SockJS is designed for use in browsers. It uses a variety of techniques
to support a wide range of browser versions.
For the full list of SockJS transport types and browsers, see the
https://github.com/sockjs/sockjs-client/[SockJS client] page. Transports
fall in three general categories: WebSocket, HTTP Streaming, and HTTP Long Polling.
For an overview of these categories, see
https://spring.io/blog/2012/05/08/spring-mvc-3-2-preview-techniques-for-real-time-updates/[this blog post].
The SockJS client begins by sending `GET /info` to
obtain basic information from the server. After that, it must decide what transport
to use. If possible, WebSocket is used. If not, in most browsers,
there is at least one HTTP streaming option. If not, then HTTP (long)
polling is used.
All transport requests have the following URL structure:
----
https://host:port/myApp/myEndpoint/{server-id}/{session-id}/{transport}
----
where:
* pass:q[`{server-id}`] is useful for routing requests in a cluster but is not used otherwise.
* pass:q[`{session-id}`] correlates HTTP requests belonging to a SockJS session.
* pass:q[`{transport}`] indicates the transport type (for example, `websocket`, `xhr-streaming`, and others).
The WebSocket transport needs only a single HTTP request to do the WebSocket handshake.
All messages thereafter are exchanged on that socket.
HTTP transports require more requests. Ajax/XHR streaming, for example, relies on
one long-running request for server-to-client messages and additional HTTP POST
requests for client-to-server messages. Long polling is similar, except that it
ends the current request after each server-to-client send.
SockJS adds minimal message framing. For example, the server sends the letter `o`
("`open`" frame) initially, messages are sent as `a["message1","message2"]`
(JSON-encoded array), the letter `h` ("`heartbeat`" frame) if no messages flow
for 25 seconds (by default), and the letter `c` ("`close`" frame) to close the session.
To learn more, run an example in a browser and watch the HTTP requests.
The SockJS client allows fixing the list of transports, so it is possible to
see each transport one at a time. The SockJS client also provides a debug flag,
which enables helpful messages in the browser console. On the server side, you can enable
`TRACE` logging for `org.springframework.web.socket`.
For even more detail, see the SockJS protocol
https://sockjs.github.io/sockjs-protocol/sockjs-protocol-0.3.3.html[narrated test].
[[websocket-fallback-sockjs-enable]]
=== Enabling SockJS
You can enable SockJS through Java configuration, as the following example shows:
[source,java,indent=0,subs="verbatim,quotes"]
----
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(myHandler(), "/myHandler").withSockJS();
}
@Bean
public WebSocketHandler myHandler() {
return new MyHandler();
}
}
----
The following example shows the XML configuration equivalent of the preceding example:
[source,xml,indent=0,subs="verbatim,quotes,attributes"]
----
<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:handlers>
<websocket:mapping path="/myHandler" handler="myHandler"/>
<websocket:sockjs/>
</websocket:handlers>
<bean id="myHandler" class="org.springframework.samples.MyHandler"/>
</beans>
----
The preceding example is for use in Spring MVC applications and should be included in the
configuration of a <<mvc-servlet, `DispatcherServlet`>>. However, Spring's WebSocket
and SockJS support does not depend on Spring MVC. It is relatively simple to
integrate into other HTTP serving environments with the help of
{api-spring-framework}/web/socket/sockjs/support/SockJsHttpRequestHandler.html[`SockJsHttpRequestHandler`].
On the browser side, applications can use the
https://github.com/sockjs/sockjs-client/[`sockjs-client`] (version 1.0.x). It
emulates the W3C WebSocket API and communicates with the server to select the best
transport option, depending on the browser in which it runs. See the
https://github.com/sockjs/sockjs-client/[sockjs-client] page and the list of
transport types supported by browser. The client also provides several
configuration options -- for example, to specify which transports to include.
[[websocket-fallback-xhr-vs-iframe]]
=== IE 8 and 9
Internet Explorer 8 and 9 remain in use. They are
a key reason for having SockJS. This section covers important
considerations about running in those browsers.
The SockJS client supports Ajax/XHR streaming in IE 8 and 9 by using Microsoft's
https://web.archive.org/web/20160219230343/https://blogs.msdn.com/b/ieinternals/archive/2010/05/13/xdomainrequest-restrictions-limitations-and-workarounds.aspx[`XDomainRequest`].
That works across domains but does not support sending cookies.
Cookies are often essential for Java applications.
However, since the SockJS client can be used with many server
types (not just Java ones), it needs to know whether cookies matter.
If so, the SockJS client prefers Ajax/XHR for streaming. Otherwise, it
relies on an iframe-based technique.
The first `/info` request from the SockJS client is a request for
information that can influence the client's choice of transports.
One of those details is whether the server application relies on cookies
(for example, for authentication purposes or clustering with sticky sessions).
Spring's SockJS support includes a property called `sessionCookieNeeded`.
It is enabled by default, since most Java applications rely on the `JSESSIONID`
cookie. If your application does not need it, you can turn off this option,
and SockJS client should then choose `xdr-streaming` in IE 8 and 9.
If you do use an iframe-based transport, keep in mind
that browsers can be instructed to block the use of IFrames on a given page by
setting the HTTP response header `X-Frame-Options` to `DENY`,
`SAMEORIGIN`, or `ALLOW-FROM <origin>`. This is used to prevent
https://www.owasp.org/index.php/Clickjacking[clickjacking].
[NOTE]
====
Spring Security 3.2+ provides support for setting `X-Frame-Options` on every
response. By default, the Spring Security Java configuration sets it to `DENY`.
In 3.2, the Spring Security XML namespace does not set that header by default
but can be configured to do so. In the future, it may set it by default.
See {docs-spring-security}/features/exploits/headers.html#headers-default[Default Security Headers]
of the Spring Security documentation for details on how to configure the
setting of the `X-Frame-Options` header. You can also see
https://github.com/spring-projects/spring-security/issues/2718[gh-2718]
for additional background.
====
If your application adds the `X-Frame-Options` response header (as it should!)
and relies on an iframe-based transport, you need to set the header value to
`SAMEORIGIN` or `ALLOW-FROM <origin>`. The Spring SockJS
support also needs to know the location of the SockJS client, because it is loaded
from the iframe. By default, the iframe is set to download the SockJS client
from a CDN location. It is a good idea to configure this option to use
a URL from the same origin as the application.
The following example shows how to do so in Java configuration:
[source,java,indent=0,subs="verbatim,quotes"]
----
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/portfolio").withSockJS()
.setClientLibraryUrl("http://localhost:8080/myapp/js/sockjs-client.js");
}
// ...
}
----
The XML namespace provides a similar option through the `<websocket:sockjs>` element.
NOTE: During initial development, do enable the SockJS client `devel` mode that prevents
the browser from caching SockJS requests (like the iframe) that would otherwise
be cached. For details on how to enable it see the
https://github.com/sockjs/sockjs-client/[SockJS client] page.
[[websocket-fallback-sockjs-heartbeat]]
=== Heartbeats
The SockJS protocol requires servers to send heartbeat messages to preclude proxies
from concluding that a connection is hung. The Spring SockJS configuration has a property
called `heartbeatTime` that you can use to customize the frequency. By default, a
heartbeat is sent after 25 seconds, assuming no other messages were sent on that
connection. This 25-second value is in line with the following
https://tools.ietf.org/html/rfc6202[IETF recommendation] for public Internet applications.
NOTE: When using STOMP over WebSocket and SockJS, if the STOMP client and server negotiate
heartbeats to be exchanged, the SockJS heartbeats are disabled.
The Spring SockJS support also lets you configure the `TaskScheduler` to
schedule heartbeats tasks. The task scheduler is backed by a thread pool,
with default settings based on the number of available processors. Your
should consider customizing the settings according to your specific needs.
[[websocket-fallback-sockjs-servlet3-async]]
=== Client Disconnects
HTTP streaming and HTTP long polling SockJS transports require a connection to remain
open longer than usual. For an overview of these techniques, see
https://spring.io/blog/2012/05/08/spring-mvc-3-2-preview-techniques-for-real-time-updates/[this blog post].
In Servlet containers, this is done through Servlet 3 asynchronous support that
allows exiting the Servlet container thread, processing a request, and continuing
to write to the response from another thread.
A specific issue is that the Servlet API does not provide notifications for a client
that has gone away. See https://github.com/eclipse-ee4j/servlet-api/issues/44[eclipse-ee4j/servlet-api#44].
However, Servlet containers raise an exception on subsequent attempts to write
to the response. Since Spring's SockJS Service supports server-sent heartbeats (every
25 seconds by default), that means a client disconnect is usually detected within that
time period (or earlier, if messages are sent more frequently).
NOTE: As a result, network I/O failures can occur because a client has disconnected, which
can fill the log with unnecessary stack traces. Spring makes a best effort to identify
such network failures that represent client disconnects (specific to each server) and log
a minimal message by using the dedicated log category, `DISCONNECTED_CLIENT_LOG_CATEGORY`
(defined in `AbstractSockJsSession`). If you need to see the stack traces, you can set that
log category to TRACE.
[[websocket-fallback-cors]]
=== SockJS and CORS
If you allow cross-origin requests (see <<websocket-server-allowed-origins>>), the SockJS protocol
uses CORS for cross-domain support in the XHR streaming and polling transports. Therefore,
CORS headers are added automatically, unless the presence of CORS headers in the response
is detected. So, if an application is already configured to provide CORS support (for example,
through a Servlet Filter), Spring's `SockJsService` skips this part.
It is also possible to disable the addition of these CORS headers by setting the
`suppressCors` property in Spring's SockJsService.
SockJS expects the following headers and values:
* `Access-Control-Allow-Origin`: Initialized from the value of the `Origin` request header.
* `Access-Control-Allow-Credentials`: Always set to `true`.
* `Access-Control-Request-Headers`: Initialized from values from the equivalent request header.
* `Access-Control-Allow-Methods`: The HTTP methods a transport supports (see `TransportType` enum).
* `Access-Control-Max-Age`: Set to 31536000 (1 year).
For the exact implementation, see `addCorsHeaders` in `AbstractSockJsService` and
the `TransportType` enum in the source code.
Alternatively, if the CORS configuration allows it, consider excluding URLs with the
SockJS endpoint prefix, thus letting Spring's `SockJsService` handle it.
[[websocket-fallback-sockjs-client]]
=== `SockJsClient`
Spring provides a SockJS Java client to connect to remote SockJS endpoints without
using a browser. This can be especially useful when there is a need for bidirectional
communication between two servers over a public network (that is, where network proxies can
preclude the use of the WebSocket protocol). A SockJS Java client is also very useful
for testing purposes (for example, to simulate a large number of concurrent users).
The SockJS Java client supports the `websocket`, `xhr-streaming`, and `xhr-polling`
transports. The remaining ones only make sense for use in a browser.
You can configure the `WebSocketTransport` with:
* `StandardWebSocketClient` in a JSR-356 runtime.
* `JettyWebSocketClient` by using the Jetty 9+ native WebSocket API.
* Any implementation of Spring's `WebSocketClient`.
An `XhrTransport`, by definition, supports both `xhr-streaming` and `xhr-polling`, since,
from a client perspective, there is no difference other than in the URL used to connect
to the server. At present there are two implementations:
* `RestTemplateXhrTransport` uses Spring's `RestTemplate` for HTTP requests.
* `JettyXhrTransport` uses Jetty's `HttpClient` for HTTP requests.
The following example shows how to create a SockJS client and connect to a SockJS endpoint:
[source,java,indent=0,subs="verbatim,quotes"]
----
List<Transport> transports = new ArrayList<>(2);
transports.add(new WebSocketTransport(new StandardWebSocketClient()));
transports.add(new RestTemplateXhrTransport());
SockJsClient sockJsClient = new SockJsClient(transports);
sockJsClient.doHandshake(new MyWebSocketHandler(), "ws://example.com:8080/sockjs");
----
NOTE: SockJS uses JSON formatted arrays for messages. By default, Jackson 2 is used and needs
to be on the classpath. Alternatively, you can configure a custom implementation of
`SockJsMessageCodec` and configure it on the `SockJsClient`.
To use `SockJsClient` to simulate a large number of concurrent users, you
need to configure the underlying HTTP client (for XHR transports) to allow a sufficient
number of connections and threads. The following example shows how to do so with Jetty:
[source,java,indent=0,subs="verbatim,quotes"]
----
HttpClient jettyHttpClient = new HttpClient();
jettyHttpClient.setMaxConnectionsPerDestination(1000);
jettyHttpClient.setExecutor(new QueuedThreadPool(1000));
----
The following example shows the server-side SockJS-related properties (see javadoc for details)
that you should also consider customizing:
[source,java,indent=0,subs="verbatim,quotes"]
----
@Configuration
public class WebSocketConfig extends WebSocketMessageBrokerConfigurationSupport {
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/sockjs").withSockJS()
.setStreamBytesLimit(512 * 1024) <1>
.setHttpMessageCacheSize(1000) <2>
.setDisconnectDelay(30 * 1000); <3>
}
// ...
}
----
<1> Set the `streamBytesLimit` property to 512KB (the default is 128KB -- `128 * 1024`).
<2> Set the `httpMessageCacheSize` property to 1,000 (the default is `100`).
<3> Set the `disconnectDelay` property to 30 property seconds (the default is five seconds
-- `5 * 1000`).
[[websocket-stomp]]
== STOMP
The WebSocket protocol defines two types of messages (text and binary), but their
content is undefined. The protocol defines a mechanism for client and server to negotiate a
sub-protocol (that is, a higher-level messaging protocol) to use on top of WebSocket to
define what kind of messages each can send, what the format is, the content of each
message, and so on. The use of a sub-protocol is optional but, either way, the client and
the server need to agree on some protocol that defines message content.
[[websocket-stomp-overview]]
=== Overview
https://stomp.github.io/stomp-specification-1.2.html#Abstract[STOMP] (Simple
Text Oriented Messaging Protocol) was originally created for scripting languages
(such as Ruby, Python, and Perl) to connect to enterprise message brokers. It is
designed to address a minimal subset of commonly used messaging patterns. STOMP can be
used over any reliable two-way streaming network protocol, such as TCP and WebSocket.
Although STOMP is a text-oriented protocol, message payloads can be
either text or binary.
STOMP is a frame-based protocol whose frames are modeled on HTTP. The following listing shows the structure
of a STOMP frame:
----
COMMAND
header1:value1
header2:value2
Body^@
----
Clients can use the `SEND` or `SUBSCRIBE` commands to send or subscribe for
messages, along with a `destination` header that describes what the
message is about and who should receive it. This enables a simple
publish-subscribe mechanism that you can use to send messages through the broker
to other connected clients or to send messages to the server to request that
some work be performed.
When you use Spring's STOMP support, the Spring WebSocket application acts
as the STOMP broker to clients. Messages are routed to `@Controller` message-handling
methods or to a simple in-memory broker that keeps track of subscriptions and
broadcasts messages to subscribed users. You can also configure Spring to work
with a dedicated STOMP broker (such as RabbitMQ, ActiveMQ, and others) for the actual
broadcasting of messages. In that case, Spring maintains
TCP connections to the broker, relays messages to it, and passes messages
from it down to connected WebSocket clients. Thus, Spring web applications can
rely on unified HTTP-based security, common validation, and a familiar programming
model for message handling.
The following example shows a client subscribing to receive stock quotes, which
the server may emit periodically (for example, via a scheduled task that sends messages
through a `SimpMessagingTemplate` to the broker):
----
SUBSCRIBE
id:sub-1
destination:/topic/price.stock.*
^@
----
The following example shows a client that sends a trade request, which the server
can handle through an `@MessageMapping` method:
----
SEND
destination:/queue/trade
content-type:application/json
content-length:44
{"action":"BUY","ticker":"MMM","shares",44}^@
----
After the execution, the server can
broadcast a trade confirmation message and details down to the client.
The meaning of a destination is intentionally left opaque in the STOMP spec. It can
be any string, and it is entirely up to STOMP servers to define the semantics and
the syntax of the destinations that they support. It is very common, however, for
destinations to be path-like strings where `/topic/..` implies publish-subscribe
(one-to-many) and `/queue/` implies point-to-point (one-to-one) message
exchanges.
STOMP servers can use the `MESSAGE` command to broadcast messages to all subscribers.
The following example shows a 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}^@
----
A server cannot send unsolicited messages. All messages
from a server must be in response to a specific client subscription, and the
`subscription` header of the server message must match the `id` header of the
client subscription.
The preceding overview is intended to provide the most basic understanding of the
STOMP protocol. We recommended reviewing the protocol
https://stomp.github.io/stomp-specification-1.2.html[specification] in full.
[[websocket-stomp-benefits]]
=== Benefits
Using STOMP as a sub-protocol lets the Spring Framework and Spring Security
provide a richer programming model versus using raw WebSockets. The same point can be
made about HTTP versus raw TCP and how it lets Spring MVC and other web frameworks
provide rich functionality. The following is a list of benefits:
* No need to invent a custom messaging protocol and message format.
* STOMP clients, including a <<websocket-stomp-client, Java client>>
in the Spring Framework, are available.
* You can (optionally) use message brokers (such as RabbitMQ, ActiveMQ, and others) to
manage subscriptions and broadcast messages.
* Application logic can be organized in any number of `@Controller` instances and messages can be
routed to them based on the STOMP destination header versus handling raw WebSocket messages
with a single `WebSocketHandler` for a given connection.
* You can use Spring Security to secure messages based on STOMP destinations and message types.
[[websocket-stomp-enable]]
=== Enable STOMP
STOMP over WebSocket support is available in the `spring-messaging` and
`spring-websocket` modules. Once you have those dependencies, you can expose a STOMP
endpoints, over WebSocket with <<websocket-fallback>>, as the following example shows:
[source,java,indent=0,subs="verbatim,quotes"]
----
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(); // <1>
}
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.setApplicationDestinationPrefixes("/app"); // <2>
config.enableSimpleBroker("/topic", "/queue"); // <3>
}
}
----
<1> `/portfolio` is the HTTP URL for the endpoint to which a WebSocket (or SockJS)
client needs to connect for the WebSocket handshake.
<2> STOMP messages whose destination header begins with `/app` are routed to
`@MessageMapping` methods in `@Controller` classes.
<3> Use the built-in message broker for subscriptions and broadcasting and
route messages whose destination header begins with `/topic `or `/queue` to the broker.
The following example shows the XML configuration equivalent of the preceding example:
[source,xml,indent=0,subs="verbatim,quotes,attributes"]
----
<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>
----
NOTE: For the built-in simple broker, the `/topic` and `/queue` prefixes do not have any special
meaning. They are merely a convention to differentiate between pub-sub versus point-to-point
messaging (that is, many subscribers versus one consumer). When you use an external broker,
check the STOMP page of the broker to understand what kind of STOMP destinations and
prefixes it supports.
To connect from a browser, for SockJS, you can use the
https://github.com/sockjs/sockjs-client[`sockjs-client`]. For STOMP, many applications have
used the https://github.com/jmesnil/stomp-websocket[jmesnil/stomp-websocket] library
(also known as stomp.js), which is feature-complete and has been used in production for
years but is no longer maintained. At present the
https://github.com/JSteunou/webstomp-client[JSteunou/webstomp-client] is the most
actively maintained and evolving successor of that library. The following example code
is based on it:
[source,javascript,indent=0,subs="verbatim,quotes"]
----
var socket = new SockJS("/spring-websocket-portfolio/portfolio");
var stompClient = webstomp.over(socket);
stompClient.connect({}, function(frame) {
}
----
Alternatively, if you connect through WebSocket (without SockJS), you can use the following code:
[source,javascript,indent=0,subs="verbatim,quotes"]
----
var socket = new WebSocket("/spring-websocket-portfolio/portfolio");
var stompClient = Stomp.over(socket);
stompClient.connect({}, function(frame) {
}
----
Note that `stompClient` in the preceding example does not need to specify `login`
and `passcode` headers. Even if it did, they would be ignored (or, rather,
overridden) on the server side. See <<websocket-stomp-handle-broker-relay-configure>>
and <<websocket-stomp-authentication>> for more information on authentication.
For more example code see:
* https://spring.io/guides/gs/messaging-stomp-websocket/[Using WebSocket to build an
interactive web application] -- a getting started guide.
* https://github.com/rstoyanchev/spring-websocket-portfolio[Stock Portfolio] -- a sample
application.
[[websocket-stomp-server-config]]
=== WebSocket Server
To configure the underlying WebSocket server, the information in
<<websocket-server-runtime-configuration>> applies. For Jetty, however you need to set
the `HandshakeHandler` and `WebSocketPolicy` through the `StompEndpointRegistry`:
[source,java,indent=0,subs="verbatim,quotes"]
----
@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)));
}
}
----
[[websocket-stomp-message-flow]]
=== Flow of Messages
Once a STOMP endpoint is exposed, the Spring application becomes a STOMP broker for
connected clients. This section describes the flow of messages on the server side.
The `spring-messaging` module contains foundational support for messaging applications
that originated in https://spring.io/spring-integration[Spring Integration] and was
later extracted and incorporated into the Spring Framework for broader use across many
https://spring.io/projects[Spring projects] and application scenarios.
The following list briefly describes a few of the available messaging abstractions:
* {api-spring-framework}/messaging/Message.html[Message]:
Simple representation for a message, including headers and payload.
* {api-spring-framework}/messaging/MessageHandler.html[MessageHandler]:
Contract for handling a message.
* {api-spring-framework}/messaging/MessageChannel.html[MessageChannel]:
Contract for sending a message that enables loose coupling between producers and consumers.
* {api-spring-framework}/messaging/SubscribableChannel.html[SubscribableChannel]:
`MessageChannel` with `MessageHandler` subscribers.
* {api-spring-framework}/messaging/support/ExecutorSubscribableChannel.html[ExecutorSubscribableChannel]:
`SubscribableChannel` that uses an `Executor` for delivering messages.
Both the Java configuration (that is, `@EnableWebSocketMessageBroker`) and the XML namespace configuration
(that is, `<websocket:message-broker>`) use the preceding components to assemble a message
workflow. The following diagram shows the components used when the simple built-in message
broker is enabled:
image::images/message-flow-simple-broker.png[]
The preceding diagram shows three message channels:
* `clientInboundChannel`: For passing messages received from WebSocket clients.
* `clientOutboundChannel`: For sending server messages to WebSocket clients.
* `brokerChannel`: For sending messages to the message broker from within
server-side application code.
The next diagram shows the components used when an external broker (such as RabbitMQ)
is configured for managing subscriptions and broadcasting messages:
image::images/message-flow-broker-relay.png[]
The main difference between the two preceding diagrams is the use of the "`broker relay`" for passing
messages up to the external STOMP broker over TCP and for passing messages down from the
broker to subscribed clients.
When messages are received from a WebSocket connection, they are decoded to STOMP frames,
turned into a Spring `Message` representation, and sent to the
`clientInboundChannel` for further processing. For example, STOMP messages whose
destination headers start with `/app` may be routed to `@MessageMapping` methods in
annotated controllers, while `/topic` and `/queue` messages may be routed directly
to the message broker.
An annotated `@Controller` that handles a STOMP message from a client may send a message to
the message broker through the `brokerChannel`, and the broker broadcasts the
message to matching subscribers through the `clientOutboundChannel`. The same
controller can also do the same in response to HTTP requests, so a client can perform an
HTTP POST, and then a `@PostMapping` method can send a message to the message broker
to broadcast to subscribed clients.
We can trace the flow through a simple example. Consider the following example, which sets up a server:
[source,java,indent=0,subs="verbatim,quotes"]
----
@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 preceding example supports the following flow:
. The client connects to `http://localhost:8080/portfolio` and, once a WebSocket connection
is established, STOMP frames begin to flow on it.
. The client sends a SUBSCRIBE frame with a destination header of `/topic/greeting`. Once received
and decoded, the message is sent to the `clientInboundChannel` and is then routed to the
message broker, which stores the client subscription.
. The client sends a SEND frame to `/app/greeting`. The `/app` prefix helps to route it to
annotated controllers. After the `/app` prefix is stripped, the remaining `/greeting`
part of the destination is mapped to the `@MessageMapping` method in `GreetingController`.
. The value returned from `GreetingController` is turned into a Spring `Message` with
a payload based on the return value and a default destination header of
`/topic/greeting` (derived from the input destination with `/app` replaced by
`/topic`). The resulting message is sent to the `brokerChannel` and handled
by the message broker.
. The message broker finds all matching subscribers and sends a MESSAGE frame to each one
through the `clientOutboundChannel`, from where messages are encoded as STOMP frames
and sent on the WebSocket connection.
The next section provides more details on annotated methods, including the
kinds of arguments and return values that are supported.
[[websocket-stomp-handle-annotations]]
=== Annotated Controllers
Applications can use annotated `@Controller` classes to handle messages from clients.
Such classes can declare `@MessageMapping`, `@SubscribeMapping`, and `@ExceptionHandler`
methods, as described in the following topics:
* <<websocket-stomp-message-mapping>>
* <<websocket-stomp-subscribe-mapping>>
* <<websocket-stomp-exception-handler>>
[[websocket-stomp-message-mapping]]
==== `@MessageMapping`
You can use `@MessageMapping` to annotate methods that route messages based on their
destination. It is supported at the method level as well as at the type level. At the type
level, `@MessageMapping` is used to express shared mappings across all methods in a
controller.
By default, the mapping values are Ant-style path patterns (for example `/thing*`, `/thing/**`),
including support for template variables (for example, pass:q[`/thing/{id}`]). The values can be
referenced through `@DestinationVariable` method arguments. Applications can also switch to
a dot-separated destination convention for mappings, as explained in
<<websocket-stomp-destination-separator>>.
===== Supported Method Arguments
The following table describes the method arguments:
[cols="1,2", options="header"]
|===
| Method argument | Description
| `Message`
| For access to the complete message.
| `MessageHeaders`
| For access to the headers within the `Message`.
| `MessageHeaderAccessor`, `SimpMessageHeaderAccessor`, and `StompHeaderAccessor`
| For access to the headers through typed accessor methods.
| `@Payload`
| For access to the payload of the message, converted (for example, from JSON) by a configured
`MessageConverter`.
The presence of this annotation is not required since it is, by default, assumed if no
other argument is matched.
You can annotate payload arguments with `@jakarta.validation.Valid` or Spring's `@Validated`,
to have the payload arguments be automatically validated.
| `@Header`
| For access to a specific header value -- along with type conversion using an
`org.springframework.core.convert.converter.Converter`, if necessary.
| `@Headers`
| For access to all headers in the message. This argument must be assignable to
`java.util.Map`.
| `@DestinationVariable`
| For access to template variables extracted from the message destination.
Values are converted to the declared method argument type as necessary.
| `java.security.Principal`
| Reflects the user logged in at the time of the WebSocket HTTP handshake.
|===
===== Return Values
By default, the return value from a `@MessageMapping` method is serialized to a payload
through a matching `MessageConverter` and sent as a `Message` to the `brokerChannel`,
from where it is broadcast to subscribers. The destination of the outbound message is the
same as that of the inbound message but prefixed with `/topic`.
You can use the `@SendTo` and `@SendToUser` annotations to customize the destination of
the output message. `@SendTo` is used to customize the target destination or to
specify multiple destinations. `@SendToUser` is used to direct the output message
to only the user associated with the input message. See <<websocket-stomp-user-destination>>.
You can use both `@SendTo` and `@SendToUser` at the same time on the same method, and both
are supported at the class level, in which case they act as a default for methods in the
class. However, keep in mind that any method-level `@SendTo` or `@SendToUser` annotations
override any such annotations at the class level.
Messages can be handled asynchronously and a `@MessageMapping` method can return
`ListenableFuture`, `CompletableFuture`, or `CompletionStage`.
Note that `@SendTo` and `@SendToUser` are merely a convenience that amounts to using the
`SimpMessagingTemplate` to send messages. If necessary, for more advanced scenarios,
`@MessageMapping` methods can fall back on using the `SimpMessagingTemplate` directly.
This can be done instead of, or possibly in addition to, returning a value.
See <<websocket-stomp-handle-send>>.
[[websocket-stomp-subscribe-mapping]]
==== `@SubscribeMapping`
`@SubscribeMapping` is similar to `@MessageMapping` but narrows the mapping to
subscription messages only. It supports the same
<<websocket-stomp-message-mapping, method arguments>> as `@MessageMapping`. However
for the return value, by default, a message is sent directly to the client (through
`clientOutboundChannel`, in response to the subscription) and not to the broker (through
`brokerChannel`, as a broadcast to matching subscriptions). Adding `@SendTo` or
`@SendToUser` overrides this behavior and sends to the broker instead.
When is this useful? Assume that the broker is mapped to `/topic` and `/queue`, while
application controllers are mapped to `/app`. In this setup, the broker stores all
subscriptions to `/topic` and `/queue` that are intended for repeated broadcasts, and
there is no need for the application to get involved. A client could also subscribe to
some `/app` destination, and a controller could return a value in response to that
subscription without involving the broker without storing or using the subscription again
(effectively a one-time request-reply exchange). One use case for this is populating a UI
with initial data on startup.
When is this not useful? Do not try to map broker and controllers to the same destination
prefix unless you want both to independently process messages, including subscriptions,
for some reason. Inbound messages are handled in parallel. There are no guarantees whether
a broker or a controller processes a given message first. If the goal is to be notified
when a subscription is stored and ready for broadcasts, a client should ask for a
receipt if the server supports it (simple broker does not). For example, with the Java
<<websocket-stomp-client, STOMP client>>, you could do the following to add a receipt:
[source,java,indent=0,subs="verbatim,quotes"]
----
@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...
});
----
A server side option is <<websocket-stomp-interceptors, to register>> an
`ExecutorChannelInterceptor` on the `brokerChannel` and implement the `afterMessageHandled`
method that is invoked after messages, including subscriptions, have been handled.
[[websocket-stomp-exception-handler]]
==== `@MessageExceptionHandler`
An application can use `@MessageExceptionHandler` methods to handle exceptions from
`@MessageMapping` methods. You can declare exceptions in the annotation
itself or through a method argument if you want to get access to the exception instance.
The following example declares an exception through a method argument:
[source,java,indent=0,subs="verbatim,quotes"]
----
@Controller
public class MyController {
// ...
@MessageExceptionHandler
public ApplicationError handleException(MyException exception) {
// ...
return appError;
}
}
----
`@MessageExceptionHandler` methods support flexible method signatures and support
the same method argument types and return values as
<<websocket-stomp-message-mapping, `@MessageMapping`>> methods.
Typically, `@MessageExceptionHandler` methods apply within the `@Controller` class
(or class hierarchy) in which they are declared. If you want such methods to apply
more globally (across controllers), you can declare them in a class marked with
`@ControllerAdvice`. This is comparable to the
<<web.adoc#mvc-ann-controller-advice, similar support>> available in Spring MVC.
[[websocket-stomp-handle-send]]
=== Sending Messages
What if you want to send messages to connected clients from any part of the
application? Any application component can send messages to the `brokerChannel`.
The easiest way to do so is to inject a `SimpMessagingTemplate` and
use it to send messages. Typically, you would inject it by
type, as the following example shows:
[source,java,indent=0,subs="verbatim,quotes"]
----
@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 qualify it by its name (`brokerMessagingTemplate`), if another
bean of the same type exists.
[[websocket-stomp-handle-simple-broker]]
=== Simple Broker
The built-in simple message broker handles subscription requests from clients,
stores them in memory, and broadcasts messages to connected clients that have matching
destinations. The broker supports path-like destinations, including subscriptions
to Ant-style destination patterns.
NOTE: Applications can also use dot-separated (rather than slash-separated) destinations.
See <<websocket-stomp-destination-separator>>.
If configured with a task scheduler, the simple broker supports
https://stomp.github.io/stomp-specification-1.2.html#Heart-beating[STOMP heartbeats].
To configure a scheduler, you can declare your own `TaskScheduler` bean and set it through
the `MessageBrokerRegistry`. Alternatively, you can use the one that is automatically
declared in the built-in WebSocket configuration, however, you'll' need `@Lazy` to avoid
a cycle between the built-in WebSocket configuration and your
`WebSocketMessageBrokerConfigurer`. For example:
[source,java,indent=0,subs="verbatim,quotes"]
----
@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);
// ...
}
}
----
[[websocket-stomp-handle-broker-relay]]
=== External Broker
The simple broker is great for getting started but supports only a subset of
STOMP commands (it does not support acks, receipts, and some other features),
relies on a simple message-sending loop, and is not suitable for clustering.
As an alternative, you can upgrade your applications to use a full-featured
message broker.
See the STOMP documentation for your message broker of choice (such as
https://www.rabbitmq.com/stomp.html[RabbitMQ],
https://activemq.apache.org/stomp.html[ActiveMQ], and others), install the broker,
and run it with STOMP support enabled. Then you can enable the STOMP broker relay
(instead of the simple broker) in the Spring configuration.
The following example configuration enables a full-featured broker:
[source,java,indent=0,subs="verbatim,quotes"]
----
@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 configuration equivalent of the preceding example:
[source,xml,indent=0,subs="verbatim,quotes,attributes"]
----
<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 preceding configuration is a Spring
{api-spring-framework}/messaging/MessageHandler.html[`MessageHandler`]
that handles messages by forwarding them to an external message broker.
To do so, it establishes TCP connections to 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.
NOTE: Add `io.projectreactor.netty:reactor-netty` and `io.netty:netty-all`
dependencies to your project for TCP connection management.
Furthermore, application components (such as HTTP request handling methods,
business services, and others) can also send messages to the broker relay, as described
in <<websocket-stomp-handle-send>>, to broadcast messages to subscribed WebSocket clients.
In effect, the broker relay enables robust and scalable message broadcasting.
[[websocket-stomp-handle-broker-relay-configure]]
=== Connecting to a Broker
A STOMP broker relay maintains a single "`system`" TCP connection to the broker.
This connection is used for messages originating from the server-side application
only, not for receiving messages. You can configure the STOMP credentials (that is,
the STOMP frame `login` and `passcode` headers) for this connection. This is exposed
in both the XML namespace and Java configuration as the `systemLogin` and
`systemPasscode` properties with default values of `guest` and `guest`.
The STOMP broker relay also creates a separate TCP connection for every connected
WebSocket client. You can configure the STOMP credentials that are used for all TCP
connections created on behalf of clients. This is exposed in both the XML namespace
and Java configuration as the `clientLogin` and `clientPasscode` properties with default
values of `guest` and `guest`.
NOTE: The STOMP broker relay always sets the `login` and `passcode` headers on every `CONNECT`
frame that it forwards to the broker on behalf of clients. Therefore, WebSocket clients
need not set those headers. They are ignored. As the <<websocket-stomp-authentication>>
section explains, WebSocket clients should instead rely on HTTP authentication to protect
the WebSocket endpoint and establish the client identity.
The STOMP broker relay also sends and receives heartbeats to and from the message
broker over the "`system`" TCP connection. You can configure the intervals for sending
and receiving heartbeats (10 seconds each by default). If connectivity to the broker
is lost, the broker relay continues to try to reconnect, every 5 seconds,
until it succeeds.
Any Spring bean can implement `ApplicationListener<BrokerAvailabilityEvent>`
to receive notifications when the "`system`" connection to the broker is lost and
re-established. For example, a Stock Quote service that broadcasts stock quotes can
stop trying to send messages when there is no active "`system`" connection.
By default, the STOMP broker relay always connects, and reconnects as needed if
connectivity is lost, to the same host and port. If you wish to supply multiple addresses,
on each attempt to connect, you can configure a supplier of addresses, instead of a
fixed host and port. The following example shows how to do that:
[source,java,indent=0,subs="verbatim,quotes"]
----
@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 the STOMP broker relay with a `virtualHost` property.
The value of this property is set as the `host` header of every `CONNECT` frame
and can be useful (for example, in a cloud environment where the actual host to which
the TCP connection is established differs from the host that provides the
cloud-based STOMP service).
[[websocket-stomp-destination-separator]]
=== Dots as Separators
When messages are routed to `@MessageMapping` methods, they are matched with
`AntPathMatcher`. By default, patterns are expected to use slash (`/`) as the separator.
This is a good convention in web applications and similar to HTTP URLs. However, if
you are more used to messaging conventions, you can switch to using dot (`.`) as the separator.
The following example shows how to do so in Java configuration:
[source,java,indent=0,subs="verbatim,quotes"]
----
@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 configuration equivalent of the preceding example:
[source,xml,indent=0,subs="verbatim,quotes,attributes"]
----
<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>
----
After that, a controller can use a dot (`.`) as the separator in `@MessageMapping` methods,
as the following example shows:
[source,java,indent=0,subs="verbatim,quotes"]
----
@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 preceding example, we did not change the prefixes on the "`broker relay`", because those
depend entirely on the external message broker. See the STOMP documentation pages for
the broker you use to see what conventions it supports for the destination header.
The "`simple broker`", on the other hand, does rely on the configured `PathMatcher`, so, if
you switch the separator, that change also applies to the broker and the way the broker matches
destinations from a message to patterns in subscriptions.
[[websocket-stomp-authentication]]
=== Authentication
Every STOMP over WebSocket messaging session begins with an HTTP request.
That can be a request to upgrade to WebSockets (that is, a WebSocket handshake)
or, in the case of SockJS fallbacks, a series of SockJS HTTP transport requests.
Many web applications already have authentication and authorization in place to
secure HTTP requests. Typically, a user is authenticated through Spring Security
by using some mechanism such as a login page, HTTP basic authentication, or another way.
The security context for the authenticated user is saved in the HTTP session
and is associated with subsequent requests in the same cookie-based session.
Therefore, for a WebSocket handshake or for SockJS HTTP transport requests,
typically, there is already an authenticated user accessible through
`HttpServletRequest#getUserPrincipal()`. Spring automatically associates that user
with a WebSocket or SockJS session created for them and, subsequently, with all
STOMP messages transported over that session through a user header.
In short, a typical web application needs to do nothing
beyond what it already does for security. The user is authenticated at
the HTTP request level with a security context that is maintained through a cookie-based
HTTP session (which is then associated with WebSocket or SockJS sessions created
for that user) and results in a user header being stamped on every `Message` flowing
through the application.
The STOMP protocol does have `login` and `passcode` headers on the `CONNECT` frame.
Those were originally designed for and are needed for STOMP over TCP. However, for 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 level.
The expectation is that the WebSocket or SockJS session contain the authenticated user.
[[websocket-stomp-authentication-token-based]]
=== Token Authentication
https://github.com/spring-projects/spring-security-oauth[Spring Security OAuth]
provides support for token based security, including JSON Web Token (JWT).
You can use this as the authentication mechanism in Web applications,
including STOMP over WebSocket interactions, as described in the previous
section (that is, to maintain identity through a cookie-based session).
At the same time, cookie-based sessions are not always the best fit (for example,
in applications that do not maintain a server-side session or in
mobile applications where it is common to use headers for authentication).
The https://tools.ietf.org/html/rfc6455#section-10.5[WebSocket protocol, RFC 6455]
"doesn't prescribe any particular way that servers can authenticate clients during
the WebSocket handshake." In practice, however, browser clients can use only standard
authentication headers (that is, basic HTTP authentication) or cookies and cannot (for example)
provide custom headers. Likewise, the SockJS JavaScript client does not provide
a way to send HTTP headers with SockJS transport requests. See
https://github.com/sockjs/sockjs-client/issues/196[sockjs-client issue 196].
Instead, it does allow sending query parameters that you can use to send a token,
but that has its own drawbacks (for example, the token may be inadvertently
logged with the URL in server logs).
NOTE: The preceding limitations are for browser-based clients and do not apply to the
Spring Java-based STOMP client, which does support sending headers with both
WebSocket and SockJS requests.
Therefore, applications that wish to avoid the use of cookies may not have any good
alternatives for authentication at the HTTP protocol level. Instead of using cookies,
they may prefer to authenticate with headers at the STOMP messaging protocol level.
Doing so requires two simple steps:
. Use the STOMP client to pass authentication headers at connect time.
. Process the authentication headers with a `ChannelInterceptor`.
The next example uses server-side configuration to register a custom authentication
interceptor. Note that an interceptor needs only to authenticate and set
the user header on the CONNECT `Message`. Spring notes and saves the authenticated
user and associate it with subsequent STOMP messages on the same session. The following
example shows how register a custom authentication interceptor:
[source,java,indent=0,subs="verbatim,quotes"]
----
@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 you use Spring Security's authorization for messages, at present,
you need to ensure that the authentication `ChannelInterceptor` config is ordered
ahead of Spring Security's. This is best done by declaring the custom interceptor in
its own implementation of `WebSocketMessageBrokerConfigurer` that is marked with
`@Order(Ordered.HIGHEST_PRECEDENCE + 99)`.
[[websocket-stomp-authorization]]
=== Authorization
Spring Security provides
{docs-spring-security}/servlet/integrations/websocket.html#websocket-authorization[WebSocket sub-protocol authorization]
that uses a `ChannelInterceptor` to authorize messages based on the user header in them.
Also, Spring Session provides
https://docs.spring.io/spring-session/reference/web-socket.html[WebSocket integration]
that ensures the user's HTTP session does not expire while the WebSocket session is still active.
[[websocket-stomp-user-destination]]
=== User Destinations
An application can send messages that target a specific user, and Spring's STOMP support
recognizes destinations prefixed with `/user/` for this purpose.
For example, a client might subscribe to the `/user/queue/position-updates` destination.
`UserDestinationMessageHandler` handles this destination and transforms it into a
destination unique to the user session (such as `/queue/position-updates-user123`).
This provides the convenience of subscribing to a generically named destination while,
at the same time, ensuring no collisions with other users who subscribe to the same
destination so that each user can receive unique stock position updates.
TIP: When working with user destinations, it is important to configure broker and
application destination prefixes as shown in <<websocket-stomp-enable>>, or otherwise the
broker would handle "/user" prefixed messages that should only be handled by
`UserDestinationMessageHandler`.
On the sending side, messages can be sent to a destination such as
pass:q[`/user/{username}/queue/position-updates`], which in turn is translated
by the `UserDestinationMessageHandler` into one or more destinations, one for each
session associated with the user. This lets any component within the application
send messages that target a specific user without necessarily knowing anything more
than their name and the generic destination. This is also supported through an
annotation and a messaging template.
A message-handling method can send messages to the user associated with
the message being handled through the `@SendToUser` annotation (also supported on
the class-level to share a common destination), as the following example shows:
[source,java,indent=0,subs="verbatim,quotes"]
----
@Controller
public class PortfolioController {
@MessageMapping("/trade")
@SendToUser("/queue/position-updates")
public TradeResult executeTrade(Trade trade, Principal principal) {
// ...
return tradeResult;
}
}
----
If the user has more than one session, by default, all of the sessions subscribed
to the given destination are targeted. However, sometimes, it may be necessary to
target only the session that sent the message being handled. You can do so by
setting the `broadcast` attribute to false, as the following example shows:
[source,java,indent=0,subs="verbatim,quotes"]
----
@Controller
public class MyController {
@MessageMapping("/action")
public void handleAction() throws Exception{
// raise MyBusinessException here
}
@MessageExceptionHandler
@SendToUser(destinations="/queue/errors", broadcast=false)
public ApplicationError handleException(MyBusinessException exception) {
// ...
return appError;
}
}
----
NOTE: While user destinations generally imply an authenticated user, it is not strictly required.
A WebSocket session that is not associated with an authenticated user
can subscribe to a user destination. In such cases, the `@SendToUser` annotation
behaves exactly the same as with `broadcast=false` (that is, targeting only the
session that sent the message being handled).
You can send a message to user destinations from any application
component by, for example, injecting the `SimpMessagingTemplate` created by the Java configuration or
the XML namespace. (The bean name is `brokerMessagingTemplate` if required
for qualification with `@Qualifier`.) The following example shows how to do so:
[source,java,indent=0,subs="verbatim,quotes"]
----
@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());
}
}
----
NOTE: When you use user destinations with an external message broker, you should check the broker
documentation on how to manage inactive queues, so that, when the user session is
over, all unique user queues are removed. For example, RabbitMQ creates auto-delete
queues when you use destinations such as `/exchange/amq.direct/position-updates`.
So, in that case, the client could subscribe to `/user/exchange/amq.direct/position-updates`.
Similarly, ActiveMQ has
https://activemq.apache.org/delete-inactive-destinations.html[configuration options]
for purging inactive destinations.
In a multi-application server scenario, a user destination may remain unresolved because
the user is connected to a different server. In such cases, you can configure a
destination to broadcast unresolved messages so that other servers have a chance to try.
This can be done through the `userDestinationBroadcast` property of the
`MessageBrokerRegistry` in Java configuration and the `user-destination-broadcast` attribute
of the `message-broker` element in XML.
[[websocket-stomp-ordered-messages]]
=== Order of Messages
Messages from the broker are published to the `clientOutboundChannel`, from where they are
written to WebSocket sessions. As 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 an issue, enable the `setPreservePublishOrder` flag, as the following example shows:
[source,java,indent=0,subs="verbatim,quotes"]
----
@Configuration
@EnableWebSocketMessageBroker
public class MyConfig implements WebSocketMessageBrokerConfigurer {
@Override
protected void configureMessageBroker(MessageBrokerRegistry registry) {
// ...
registry.setPreservePublishOrder(true);
}
}
----
The following example shows the XML configuration equivalent of the preceding example:
[source,xml,indent=0,subs="verbatim,quotes,attributes"]
----
<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>
----
When the flag is set, messages within the same client session are published to the
`clientOutboundChannel` one at a time, so that the order of publication is guaranteed.
Note that this incurs a small performance overhead, so you should enable it only if it is required.
[[websocket-stomp-application-context-events]]
=== Events
Several `ApplicationContext` events are published and can be
received by implementing Spring's `ApplicationListener` interface:
* `BrokerAvailabilityEvent`: Indicates when the broker becomes available or unavailable.
While the "`simple`" broker becomes available immediately on startup and remains so while
the application is running, the STOMP "`broker relay`" can lose its connection
to the full featured broker (for example, if the broker is restarted). The broker relay
has reconnect logic and re-establishes the "`system`" connection to the broker
when it comes back. As a result, this event is published whenever the state changes from connected
to disconnected and vice-versa. Components that use the `SimpMessagingTemplate` should
subscribe to this event and avoid sending messages at times when the broker is not
available. In any case, they should be prepared to handle `MessageDeliveryException`
when sending a message.
* `SessionConnectEvent`: Published when a new STOMP CONNECT is received to
indicate the start of a new client session. The event contains the message that represents the
connect, including the session ID, user information (if any), and any custom headers the client
sent. This is useful for tracking client sessions. Components subscribed
to this event can wrap the contained message with `SimpMessageHeaderAccessor` or
`StompMessageHeaderAccessor`.
* `SessionConnectedEvent`: Published shortly after a `SessionConnectEvent` when the
broker has sent a STOMP CONNECTED frame in response to the CONNECT. At this point, the
STOMP session can be considered fully established.
* `SessionSubscribeEvent`: Published when a new STOMP SUBSCRIBE is received.
* `SessionUnsubscribeEvent`: Published when a new STOMP UNSUBSCRIBE is received.
* `SessionDisconnectEvent`: Published when a STOMP session ends. The DISCONNECT may
have been sent from the client or it may be automatically generated when the
WebSocket session is closed. In some cases, this event is published more than once
per session. Components should be idempotent with regard to multiple disconnect events.
NOTE: When you use a full-featured broker, the STOMP "`broker relay`" automatically reconnects the
"`system`" connection if broker becomes temporarily unavailable. Client connections,
however, are not automatically reconnected. Assuming heartbeats are enabled, the client
typically notices the broker is not responding within 10 seconds. Clients need to
implement their own reconnecting logic.
[[websocket-stomp-interceptors]]
=== Interception
<<websocket-stomp-application-context-events>> provide notifications for the lifecycle
of a STOMP connection but not for every client message. Applications can also register a
`ChannelInterceptor` to intercept any message and in any part of the processing chain.
The following example shows how to intercept inbound messages from clients:
[source,java,indent=0,subs="verbatim,quotes"]
----
@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 information about the message, as the following example shows:
[source,java,indent=0,subs="verbatim,quotes"]
----
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 sub-interface
of `ChannelInterceptor` with callbacks in the thread in which the messages are handled.
While a `ChannelInterceptor` is invoked once for each message sent to a channel, the
`ExecutorChannelInterceptor` provides hooks in the thread of each `MessageHandler`
subscribed to messages from the channel.
Note that, as with the `SessionDisconnectEvent` described earlier, a DISCONNECT message
can be from the client or it can also be automatically generated when
the WebSocket session is closed. In some cases, an interceptor may intercept this
message more than once for each session. Components should be idempotent with regard to
multiple disconnect events.
[[websocket-stomp-client]]
=== STOMP Client
Spring provides a STOMP over WebSocket client and a STOMP over TCP client.
To begin, you can create and configure `WebSocketStompClient`, as the following example shows:
[source,java,indent=0,subs="verbatim,quotes"]
----
WebSocketClient webSocketClient = new StandardWebSocketClient();
WebSocketStompClient stompClient = new WebSocketStompClient(webSocketClient);
stompClient.setMessageConverter(new StringMessageConverter());
stompClient.setTaskScheduler(taskScheduler); // for heartbeats
----
In the preceding example, you could replace `StandardWebSocketClient` with `SockJsClient`,
since that is also an implementation of `WebSocketClient`. The `SockJsClient` can
use WebSocket or HTTP-based transport as a fallback. For more details, see
<<websocket-fallback-sockjs-client>>.
Next, you can establish a connection and provide a handler for the STOMP session,
as the following example shows:
[source,java,indent=0,subs="verbatim,quotes"]
----
String url = "ws://127.0.0.1:8080/endpoint";
StompSessionHandler sessionHandler = new MyStompSessionHandler();
stompClient.connect(url, sessionHandler);
----
When the session is ready for use, the handler is notified, as the following example shows:
[source,java,indent=0,subs="verbatim,quotes"]
----
public class MyStompSessionHandler extends StompSessionHandlerAdapter {
@Override
public void afterConnected(StompSession session, StompHeaders connectedHeaders) {
// ...
}
}
----
Once the session is established, any payload can be sent and is
serialized with the configured `MessageConverter`, as the following example shows:
[source,java,indent=0,subs="verbatim,quotes"]
----
session.send("/topic/something", "payload");
----
You can also subscribe to destinations. The `subscribe` methods require a handler
for messages on the subscription and returns a `Subscription` handle that you can
use to unsubscribe. For each received message, the handler can specify the target
`Object` type to which the payload should be deserialized, as the following example shows:
[source,java,indent=0,subs="verbatim,quotes"]
----
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, you can configure `WebSocketStompClient` with a `TaskScheduler`
and optionally customize the heartbeat intervals (10 seconds for write inactivity,
which causes a heartbeat to be sent, and 10 seconds for read inactivity, which
closes the connection).
`WebSocketStompClient` sends a heartbeat only in case of inactivity, i.e. when no
other messages are sent. This can present a challenge when using an external broker
since messages with a non-broker destination represent activity but aren't actually
forwarded to the broker. In that case you can configure a `TaskScheduler`
when initializing the <<websocket-stomp-handle-broker-relay>> which ensures a
heartbeat is forwarded to the broker also when only messages with a non-broker
destination are sent.
NOTE: When you use `WebSocketStompClient` for performance tests to simulate thousands
of clients from the same machine, consider turning off heartbeats, since each
connection schedules its own heartbeat tasks and that is not optimized for
a large number of clients running on the same machine.
The STOMP protocol also supports receipts, where the client must add a `receipt`
header to which the server responds with a RECEIPT frame after the send or
subscribe are processed. To support this, the `StompSession` offers
`setAutoReceipt(boolean)` that causes a `receipt` header to be
added on every subsequent send or subscribe event.
Alternatively, you can also manually add a receipt header to the `StompHeaders`.
Both send and subscribe return an instance of `Receiptable`
that you can use to register for receipt success and failure callbacks.
For this feature, you must configure the client with a `TaskScheduler`
and the amount of time before a receipt expires (15 seconds by default).
Note that `StompSessionHandler` itself is a `StompFrameHandler`, which lets
it handle ERROR frames in addition to the `handleException` callback for
exceptions from the handling of messages and `handleTransportError` for
transport-level errors including `ConnectionLostException`.
[[websocket-stomp-websocket-scope]]
=== WebSocket Scope
Each WebSocket session has a map of attributes. The map is attached as a header to
inbound client messages and may be accessed from a controller method, as the following example shows:
[source,java,indent=0,subs="verbatim,quotes"]
----
@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` scope.
You can inject WebSocket-scoped beans into controllers and any channel interceptors
registered on the `clientInboundChannel`. Those are typically singletons and live
longer than any individual WebSocket session. Therefore, you need to use a
scope proxy mode for WebSocket-scoped beans, as the following example shows:
[source,java,indent=0,subs="verbatim,quotes"]
----
@Component
@Scope(scopeName = "websocket", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class MyBean {
@PostConstruct
public void init() {
// Invoked after dependencies injected
}
// ...
@PreDestroy
public void destroy() {
// Invoked when the WebSocket session ends
}
}
@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
}
}
----
As with any custom scope, Spring initializes a new `MyBean` instance the first
time it is accessed from the controller and stores the instance in the WebSocket
session attributes. The same instance is subsequently returned until the session
ends. WebSocket-scoped beans have all Spring lifecycle methods invoked, as
shown in the preceding examples.
[[websocket-stomp-configuration-performance]]
=== Performance
There is no silver bullet when it comes to performance. Many factors
affect it, including the size and volume of messages, whether application
methods perform work that requires blocking, and external factors
(such as network speed and other issues). The goal of this section is to provide
an overview of the available configuration options along with some thoughts
on how to reason about scaling.
In a messaging application, messages are passed through channels for asynchronous
executions that are backed by thread pools. Configuring such an application requires
good knowledge of the channels and the flow of messages. Therefore, it is
recommended to review <<websocket-stomp-message-flow>>.
The obvious place to start is to configure the thread pools that back the
`clientInboundChannel` and the `clientOutboundChannel`. By default, both
are configured at twice the number of available processors.
If the handling of messages in annotated methods is mainly CPU-bound, the
number of threads for the `clientInboundChannel` should remain close to the
number of processors. If the work they do is more IO-bound and requires blocking
or waiting on a database or other external system, the thread pool size
probably needs to be increased.
[NOTE]
====
`ThreadPoolExecutor` has three important properties: the core thread pool size,
the max thread pool size, and the capacity for the queue to store
tasks for which there are no available threads.
A common point of confusion is that configuring the core pool size (for example, 10)
and max pool size (for example, 20) results in a thread pool with 10 to 20 threads.
In fact, if the capacity is left at its default value of Integer.MAX_VALUE,
the thread pool never increases beyond the core pool size, since
all additional tasks are queued.
See the javadoc of `ThreadPoolExecutor` to learn how these properties work and
understand the various queuing strategies.
====
On the `clientOutboundChannel` side, it is all 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 on
low bandwidth, they take longer to consume messages and put a burden on the
thread pool. Therefore, increasing the thread pool size becomes necessary.
While the workload for the `clientInboundChannel` is possible to predict --
after all, it is based on what the application does -- how to configure the
"clientOutboundChannel" is harder, as it is based on factors beyond
the control of the application. For this reason, two additional
properties relate to the sending of messages: `sendTimeLimit`
and `sendBufferSizeLimit`. You can use those methods to configure how long a
send is allowed to take and how much data can be buffered when sending
messages to a client.
The general idea is that, at any given time, only a single thread can be used
to send to a client. All additional messages, meanwhile, get buffered, and you
can use these properties to decide how long sending a message is allowed to
take and how much data can be buffered in the meantime. See the javadoc and
documentation of the XML schema for important additional details.
The following example shows a possible configuration:
[source,java,indent=0,subs="verbatim,quotes"]
----
@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 configuration equivalent of the preceding example:
[source,xml,indent=0,subs="verbatim,quotes,attributes"]
----
<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 configuration shown earlier to configure the
maximum allowed size for incoming STOMP messages. In theory, a WebSocket
message can be almost unlimited in size. In practice, WebSocket servers impose
limits -- for example, 8K on Tomcat and 64K on Jetty. For this reason, STOMP clients
(such as the JavaScript https://github.com/JSteunou/webstomp-client[webstomp-client]
and others) split larger STOMP messages at 16K boundaries and send them as multiple
WebSocket messages, which requires the server to buffer and re-assemble.
Spring's STOMP-over-WebSocket support does this ,so applications can configure the
maximum size for STOMP messages irrespective of WebSocket server-specific message
sizes. Keep in mind that the WebSocket message size is automatically
adjusted, if necessary, to ensure they can carry 16K WebSocket messages at a
minimum.
The following example shows one possible configuration:
[source,java,indent=0,subs="verbatim,quotes"]
----
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void configureWebSocketTransport(WebSocketTransportRegistration registration) {
registration.setMessageSizeLimit(128 * 1024);
}
// ...
}
----
The following example shows the XML configuration equivalent of the preceding example:
[source,xml,indent=0,subs="verbatim,quotes,attributes"]
----
<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 about scaling involves using multiple application instances.
Currently, you cannot do that with the simple broker.
However, when you use a full-featured broker (such as RabbitMQ), each application
instance connects to the broker, and messages broadcast from one application
instance can be broadcast through the broker to WebSocket clients connected
through any other application instances.
[[websocket-stomp-stats]]
=== Monitoring
When you use `@EnableWebSocketMessageBroker` or `<websocket:message-broker>`, key
infrastructure components automatically gather statistics and counters that provide
important insight into the internal state of the application. The configuration
also declares a bean of type `WebSocketMessageBrokerStats` that gathers all
available information in one place and by default logs it at the `INFO` level once
every 30 minutes. This bean can be exported to JMX through Spring's
`MBeanExporter` for viewing at runtime (for example, through JDK's `jconsole`).
The following list summarizes the available information:
Client WebSocket Sessions::
Current::: Indicates how many client sessions there are
currently, with the count further broken down by WebSocket versus HTTP
streaming and polling SockJS sessions.
Total::: Indicates how many total sessions have been established.
Abnormally Closed:::
Connect Failures:::: Sessions that got established but were
closed after not having received any messages within 60 seconds. This is
usually an indication of proxy or network issues.
Send Limit Exceeded:::: Sessions closed after exceeding the configured send
timeout or the send buffer limits, which can occur with slow clients
(see previous section).
Transport Errors:::: Sessions closed after a transport error, such as
failure to read or write to a WebSocket connection or
HTTP request or response.
STOMP Frames::: The total number of CONNECT, CONNECTED, and DISCONNECT frames
processed, indicating how many clients connected on the STOMP level. Note that
the DISCONNECT count may be lower when sessions get closed abnormally or when
clients close without sending a DISCONNECT frame.
STOMP Broker Relay::
TCP Connections::: Indicates how many TCP connections on behalf of client
WebSocket sessions are established to the broker. This should be equal to the
number of client WebSocket sessions + 1 additional shared "`system`" connection
for sending messages from within the application.
STOMP Frames::: The total number of CONNECT, CONNECTED, and DISCONNECT frames
forwarded to or received from the broker on behalf of clients. Note that a
DISCONNECT frame is sent to the broker regardless of how the client WebSocket
session was closed. Therefore, a lower DISCONNECT frame count is an indication
that the broker is pro-actively closing connections (maybe because of a
heartbeat that did not arrive in time, an invalid input frame, or other issue).
Client Inbound Channel:: Statistics from the thread pool that backs the `clientInboundChannel`
that provide insight into the health of incoming message processing. Tasks queueing
up here is an indication that the application may be too slow to handle messages.
If there I/O bound tasks (for example, slow database queries, HTTP requests to third party
REST API, and so on), consider increasing the thread pool size.
Client Outbound Channel:: Statistics from the thread pool that backs the `clientOutboundChannel`
that provides insight into the health of broadcasting messages to clients. Tasks
queueing up here is an indication clients are too slow to consume messages.
One way to address this is to increase the thread pool size to accommodate the
expected number of concurrent slow clients. Another option is to reduce the
send timeout and send buffer size limits (see the previous section).
SockJS Task Scheduler:: Statistics from the thread pool of the SockJS task scheduler that
is used to send heartbeats. Note that, when heartbeats are negotiated on the
STOMP level, the SockJS heartbeats are disabled.
[[websocket-stomp-testing]]
=== Testing
There are two main approaches to testing applications when you use Spring's STOMP-over-WebSocket
support. The first is to write server-side tests to verify the functionality
of controllers and their annotated message-handling methods. The second is to write
full end-to-end tests that involve running a client and a server.
The two approaches are not mutually exclusive. On the contrary, each has a place
in an overall test strategy. Server-side tests are more focused and easier to write
and maintain. End-to-end integration tests, on the other hand, are more complete and
test much more, but they are also more involved to write and maintain.
The simplest form of server-side tests is to write controller unit tests. However,
this is not useful enough, since much of what a controller does depends on its
annotations. Pure unit tests simply cannot test that.
Ideally, controllers under test should be invoked as they are at runtime, much like
the approach to testing controllers that handle HTTP requests by using the Spring MVC Test
framework -- that is, without running a Servlet container but relying on the Spring Framework
to invoke the annotated controllers. As with Spring MVC Test, you have two
possible alternatives here, either use a "`context-based`" or use a "`standalone`" setup:
* Load the actual Spring configuration with the help of the
Spring TestContext framework, inject `clientInboundChannel` as a test field, and
use it to send messages to be handled by controller methods.
* Manually set up the minimum Spring framework infrastructure required to invoke
controllers (namely the `SimpAnnotationMethodMessageHandler`) and pass messages for
controllers directly to it.
Both of these setup scenarios are demonstrated in the
https://github.com/rstoyanchev/spring-websocket-portfolio/tree/master/src/test/java/org/springframework/samples/portfolio/web[tests for the stock portfolio]
sample application.
The second approach is to create end-to-end integration tests. For that, you need
to run a WebSocket server in embedded mode and connect to it as a WebSocket client
that sends WebSocket messages containing STOMP frames.
The https://github.com/rstoyanchev/spring-websocket-portfolio/tree/master/src/test/java/org/springframework/samples/portfolio/web[tests for the stock portfolio]
sample application also demonstrate this approach by using Tomcat as the embedded
WebSocket server and a simple STOMP client for test purposes.