这部分的参考文档涵盖了Spring框架的支持 websocket式消息传递在web应用程序中包括利用STOMP作为 应用程序级别的WebSocket子协议.
??? establishes a frame of mind in which to think about WebSocket, covering adoption challenges, design considerations, and thoughts on when it is a good fit.
??? 回顾在服务器端的Spring WebSocket API,当???解释sockJS协议和演示如何设置和使用它 reviews the Spring WebSocket API on the server-side while Section 20.3, “SockJS Fallback Options” explains the SockJS protocol and shows how to configure and use it.
Section 20.4.1, “Overview of STOMP” 介绍 STOMP 消息 protocol.
Section 20.4.2, “Enable STOMP over WebSocket” 示范如果在spring中配置STOMP
Section 20.4.4, “Annotation Message Handling” 接下来的章节来阐述如何给消息处理函数,发送消息函数,选择消息服务实例选项,以及使用特殊的“用户”的目的地来添加注解
Section 20.4.16, “测试带注解的控制器方法 Testing Annotated Controller Methods” 展示三种测试STOMP/WebSocket应用的处理方式
WebSocket协议 RFC 6455 为web应用定义了 一项重要的能力: 全双工, 双向沟通 在客户端和服务器端. 这是一个令人兴奋的历史悠久的新功能技术使网络更加互动包括了Java applet,XMLHttpRequest, Adobe Flash,ActiveXObject,各种Comet技术,服务器发送的事件,等等
一个完整的WebSocket 协议的介绍超出了这个文档的范围.不管怎样 A proper introduction of the WebSocket protocol is beyond the scope of this document. At a minimum however it’s important to understand that HTTP is used only for the initial handshake, which relies on a mechanism built into HTTP to request a protocol upgrade (or in this case a protocol switch) to which the server can respond with HTTP status 101 (switching protocols) if it agrees. Assuming the handshake succeeds the TCP socket underlying the HTTP upgrade request remains open and both client and server can use it to send messages to each other.
Spring Framework 4 包含了一个新的对WebSocket提供全面支持的 spring-websocket 模块 兼容了标准的Java WebSocket API(JSR-356) 而且也提供了额外增值的一些支持比如说其他的一些介绍
一个严重的挑战是在一些浏览器对WebSocket支持的不足.尤其是第一个支持websocket的IE是版本10 (see http://caniuse.com/websockets 查找浏览器版本对websocket支持的帮助). 此外,一些使用严格限制的代理的情况也存在这样的问题,比如用来阻止HTTP upgrade的尝试或者因为保持连接太长而被断开连接 一个良好的关于这个主题的综述来自Peter Lubbers你可以在InFoQ 查看文章 "How HTML5 Web Sockets Interact With Proxy Servers".
Therefore to build a WebSocket application today, fallback options are required to simulate the WebSocket API where necessary. Spring Framework provides such transparent fallback options based on the SockJS protocol. These options can be enabled through configuration and do not require modifying the application otherwise.
Aside from short-to-midterm adoption challenges, using WebSocket brings up important design considerations that are important to recognize early on, especially in contrast to what we know about building web applications today.
Today REST is a widely accepted, understood, and supported architecture for building web applications. It is an architecture that relies on having many URLs (nouns), a handful of HTTP methods (verbs), and other principles such as using hypermedia (links), remaining stateless, etc.
By contrast a WebSocket application may use a single URL only for the initial HTTP handshake. All messages thereafter share and flow on the same TCP connection. This points to an entirely different, asynchronous, event-driven, messaging architecture. One that is much closer to traditional messaging applications (e.g. JMS, AMQP).
Spring Framework 4 includes a new spring-messaging
module with key
abstractions from the
Spring Integration project
such as Message
, MessageChannel
, MessageHandler
and others that can serve as
a foundation for such a messaging architecture. The module also includes a
set of annotations for mapping messages to methods, similar to the Spring MVC
annotation based programming model.
WebSocket does imply a messaging architecture but does not mandate the use of any specific messaging protocol. It is a very thin layer over TCP that transforms a stream of bytes into a stream of messages (either text or binary) and not much more. It is up to applications to interpret the meaning of a message.
Unlike HTTP, which is an application-level protocol, in the WebSocket protocol there is simply not enough information in an incoming message for a framework or container to know how to route it or process it. Therefore WebSocket is arguably too low level for anything but a very trivial application. It can be done, but it will likely lead to creating a framework on top. This is comparable to how most web applications today are written using a web framework rather than the Servlet API alone.
For this reason the WebSocket RFC defines the use of
sub-protocols.
During the handshake, client and server can use the header
Sec-WebSocket-Protocol
to agree on a sub-protocol, i.e. a higher, application-level
protocol to use. The use of a sub-protocol is not required, but
even if not used, applications will still need to choose a message
format that both client and server can understand. That format can be custom,
framework-specific, or a standard messaging protocol.
Spring Framework provides support for using STOMP — a simple, messaging protocol originally created for use in scripting languages with frames inspired by HTTP. STOMP is widely support and well suited for use over WebSocket and over the web.
它适合使用吗? 应该是围绕着WebSocket使用的设计要素
The best fit for WebSocket is in web applications where client and server need to exchange events at high frequency and at low latency. Prime candidates include but are not limited to applications in finance, games, collaboration, and others. Such applications are both very sensitive to time delays and also need to exchange a wide variety of messages at high frequency.
For other application types, however, this may not be the case. For example, a news or social feed that shows breaking news as they become available may be perfectly okay with simple polling once every few minutes. Here latency is important, but it is acceptable if the news takes a few minutes to appear.
Even in cases where latency is crucial, if the volume of messages is relatively low (e.g. monitoring network failures) the use of long polling should be considered as a relatively simple alternative that works reliably and is comparable by efficiency (again assuming the volume of messages is relatively low).
It is the combination of both low latency and high frequency of messages that can make the use of the WebSocket protocol critical. Even in such applications, the choice remains whether all client-server communication should be done through WebSocket messages as opposed to using HTTP and REST? The answer is going to vary by application, however, it is likely that some functionality may be exposed over both WebSocket and as a REST API in order to provide clients with alternatives. Furthermore, a REST API call may need to broadcast a message to interested clients connected via WebSocket.
Spring Framework allows @Controller
and @RestController
classes to have both
HTTP request handling and WebSocket message handling methods.
Furthermore, a Spring MVC request handling method, or any application
method for that matter, can easily broadcast a message to all interested
WebSocket clients or to a specific user.
Spring Framework提供用来适应多种WebSocket引擎的WebSocket API
Currently the list includes WebSocket runtimes such as Tomcat 7.0.47+, Jetty 9.1+, GlassFish 4.1+, WebLogic 12.1.3+, and Undertow 1.0+ (and WildFly 8.0+). Additional support may be added as more WebSocket runtimes become available.
Note | |
---|---|
As explained in the introduction, direct use of a WebSocket API is too low level for applications — until assumptions are made about the format of a message there is little a framework can do to interpret messages or route them via annotations. This is why applications should consider using a sub-protocol and Spring’s STOMP over WebSocket support. When using a higher level protocol, the details of the WebSocket API become less relevant, much like the details of TCP communication are not exposed to applications when using HTTP. Nevertheless this section covers the details of using WebSocket directly. |
创建一个WebSocket 服务器就像实现一个 WebsocketHandler一样简单, 或者更像继承TextWebSocketHandler或BinaryWebSocketHandler:
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-config and XML namespace support for mapping the above WebSocket handler at a specific URL:
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(); } }
XML configuration equivalent:
<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 http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/websocket http://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 above is for use in Spring MVC applications and should be included in the
configuration of a 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
WebSocketHttpRequestHandler.
The easiest way to customize the initial HTTP WebSocket handshake request is through
a HandshakeInterceptor
, which exposes "before" and "after" the handshake methods.
Such an interceptor can be used to preclude the handshake or to make any attributes
available to the WebSocketSession
. For example, there is a built-in interceptor
for passing HTTP session attributes to the WebSocket session:
@Configuration @EnableWebSocket public class WebSocketConfig implements WebSocketConfigurer { @Override public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { registry.addHandler(new MyHandler(), "/myHandler") .addInterceptors(new HttpSessionHandshakeInterceptor()); } }
And the XML configuration equivalent:
<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 http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/websocket http://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 others. 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
(also see Section 20.2.4, “部署注意事项” for more on this subject).
Both the Java-config and XML namespace make it possible to configure a custom
HandshakeHandler
.
Spring provides a WebSocketHandlerDecorator
base class that can be used to decorate
a WebSocketHandler
with additional behavior. Logging and exception handling
implementations are provided and added by default when using the WebSocket Java-config
or XML namespace. The ExceptionWebSocketHandlerDecorator
catches all uncaught
exceptions arising from any WebSocketHandler method and closes the WebSocket
session with status 1011
that indicates a server error.
Spring WebSocket API 可以很方便的继承到那些使用DispatcherServlet服务于WebSocket 握手或者其他http请求的springMVC的应用
The Spring WebSocket API is easy to integrate into a Spring MVC application where
the DispatcherServlet
serves both HTTP WebSocket handshake as well as other
HTTP requests.
它同样可以很方便的集成到其他的通过调用WebSocketHttpRequestHandler来处理Http的情况
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 Java WebSocket API (JSR-356) provides two deployment mechanisms. The first
involves a Servlet container classpath scan (Servlet 3 feature) at startup; and
the other is a registration API to use at Servlet container initialization.
Neither of these mechanism make 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 by providing a server-specific RequestUpgradeStrategy
even when
running in a JSR-356 runtime.
Note | |
---|---|
A request to overcome the above limitation in the Java WebSocket API has been created and can be followed at WEBSOCKET_SPEC-211. Also note that Tomcat and Jetty already provide native API alternatives that makes it easy to overcome the limitation. We are hopeful that more servers will follow their example regardless of when it is addressed in the Java WebSocket API. |
A secondary consideration is that Servlet containers with JSR-356 support
are expected to perform an 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 an <absolute-ordering />
element in web.xml
:
<web-app xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation=" http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" version="3.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, if required:
<web-app xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation=" http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" version="3.0"> <absolute-ordering> <name>spring_web</name> </absolute-ordering> </web-app>
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 add a ServletServerContainerFactoryBean
to your
WebSocket Java config:
@Configuration @EnableWebSocket public class WebSocketConfig implements WebSocketConfigurer { @Bean public ServletServerContainerFactoryBean createWebSocketContainer() { ServletServerContainerFactoryBean container = new ServletServerContainerFactoryBean(); container.setMaxTextMessageBufferSize(8192); container.setMaxBinaryMessageBufferSize(8192); return container; } }
or WebSocket XML namespace:
<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 http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/websocket http://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 | |
---|---|
对于客户端Websocket的配置来说, 你应该使用 |
对于Jetty来说,你将需要提供一个预设置的 Jetty WebSocketServerFactory 和
For Jetty, you’ll need to supply a pre-configured Jetty WebSocketServerFactory
and plug
that into Spring’s DefaultHandshakeHandler
through your WebSocket Java config:
@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))); } }
or WebSocket XML namespace:
<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 http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/websocket http://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>
As explained in the introduction, WebSocket is not supported in all browsers yet and may be precluded by restrictive network proxies. This is why Spring provides fallback options that emulate the WebSocket API as close as possible based on the SockJS protocol.
The goal of SockJS is to let applications use a WebSocket API but fall back to non-WebSocket alternatives when necessary at runtime, i.e. without the need to change application code.
SockJS consists of:
spring-websocket
module.
spring-websocket
also provides a SockJS Java client.
SockJS is designed for use in browsers. It goes to great lengths to support a wide range of browser versions using a variety of techniques. For the full list of SockJS transport types and browsers see the SockJS client page. Transports fall in 3 general categories: WebSocket, HTTP Streaming, and HTTP Long Polling. For an overview of these categories see 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 and if not then HTTP (long)
polling is used.
All transport requests have the following URL structure:
http://host:port/myApp/myEndpoint/{server-id}/{session-id}/{transport}
{server-id}
- useful for routing requests in a cluster but not used otherwise.
{session-id}
- correlates HTTP requests belonging to a SockJS session.
{transport}
- indicates the transport type, e.g. "websocket", "xhr-streaming", etc.
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 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 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 enable
TRACE logging for org.springframework.web.socket
.
For even more detail refer to the SockJS protocol
narrated test.
SockJS is easy to enable through a configuration:
@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(); } }
and the XML configuration equivalent:
<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 http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/websocket http://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 above is for use in Spring MVC applications and should be included in the configuration of a 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 SockJsHttpRequestHandler.
On the browser side, applications can use the sockjs-client that emulates the W3C WebSocket API and communicates with the server to select the best transport option depending on the browser it’s running in. Review the 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.
Internet Explorer 8 and 9 are and will remain common for some time. They are a key reason for having SockJS. This section covers important considerations about running in those browsers.
SockJS client supports Ajax/XHR streaming in IE 8, 9 via Microsoft’s XDomainRequest. That works across domains but does not support sending cookies. Cookies are very 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 do matter. If so the SockJS client prefers Ajax/XHR for streaming or otherwise it relies on a iframe-based technique.
The very 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,
e.g. 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 the SockJS client should choose xdr-streaming
in IE 8 and 9.
If you do use an iframe-based transport, and in any case, it is good to know
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
clickjacking.
Note | |
---|---|
Spring Security 3.2+ provides support for setting See Section 7.1. "Default Security Headers"
of the Spring Security documentation for details no how to configure the
setting of the |
If your application adds the X-Frame-Options
response header (as it should!)
and relies on an iframe-based transport, you will need to set the header value to
SAMEORIGIN
or ALLOW-FROM <origin>
. Along with that 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
a URL from the same origin as the application.
In Java config this can be done as shown below. The XML namespace provides a
similar option on the <websocket:sockjs>
element:
@Configuration @EnableWebSocket public class WebSocketConfig implements WebSocketConfigurer { @Override public void registerStompEndpoints(StompEndpointRegistry registry) { registry.addEndpoint("/portfolio").withSockJS() .setClientLibraryUrl("http://localhost:8080/myapp/js/sockjs-client.js"); } // ... }
Note | |
---|---|
During initial development, do enable the SockJS client |
The SockJS protocol requires servers to send heartbeat messages to preclude proxies
from concluding a connection is hung. The Spring SockJS configuiration has a property
called heartbeatTime
that can be used to customize the frequency. By default a
heartbeat is sent after 25 seconds assuming no other messages were sent on that
connection. This 25 seconds value is in line with the following
IETF recommendation for public Internet applications.
Note | |
---|---|
When using STOMP over WebSocket/SockJS, if the STOMP client and server negotiate heartbeats to be exchanged, the SockJS heartbeats are disabled. |
The Spring SockJS support also allows configuring the TaskScheduler
to use
for scheduling heartbeats tasks. The task scheduler is backed by a thread pool
with default settings based on the number of available processors. Applications
should consider customizing the settings according to their specific needs.
HTTP streaming and HTTP long polling SockJS transports require a connection to remain open longer than usual. For an overview of these techniques see this blog post.
In Servlet containers this is done through Servlet 3 async 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 the Servlet API does not provide notifications for a client that has gone away, see SERVLET_SPEC-44. However, Servlet containers raise an exception on subseqeunt attempts to write to the response. Since Spring’s SockJS Service support sever-sent heartbeats (every 25 seconds by default), that means a client disconnect is usually detected within that time period or earlier if a message are sent more frequently.
Note | |
---|---|
As a result network IO failures may occur simply 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 more minimal message using the dedicated log category |
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, e.g. through a Servlet Filter, Spring’s SockJsService will skip this part.
It is also possible to disable the addition of these CORS headers thanks to the
suppressCors
property in Spring’s SockJsService.
The following is the list of headers and values expected by SockJS:
"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
as well
as 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.
A SockJS Java client is provided in order to connect to remote SockJS endpoints without using a browser. This can be especially useful when there is a need of bidirectional communication between 2 servers over a public network, i.e. where network proxies may 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.
The WebSocketTransport
can be configured with:
StandardWebSocketClient
in a JSR-356 runtime
JettyWebSocketClient
using the Jetty 9+ native WebSocket API
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 the RestTemplate for HTTP requests.
JettyXhrTransport
uses Jetty’s HttpClient for HTTP requests.
The example below shows how to create a SockJS client and connect to a SockJS endpoint:
List<Transport> transports = new ArrayList<>(2); transports.add(new WebSocketTransport(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
|
To use the SockJsClient for simulating a large number of concurrent users you will need to configure the underlying HTTP client (for XHR transports) to allow a sufficient number of connections and threads. For example with Jetty:
HttpClient jettyHttpClient = new HttpClient(); jettyHttpClient.setMaxConnectionsPerDestination(1000); jettyHttpClient.setExecutor(new QueuedThreadPool(1000));
Consider also customizing these server-side SockJS related properties (see Javadoc for details):
@Configuration public class WebSocketConfig extends WebSocketMessageBrokerConfigurationSupport { @Override public void registerStompEndpoints(StompEndpointRegistry registry) { registry.addEndpoint("/sockjs").withSockJS() .setStreamBytesLimit(512 * 1024) .setHttpMessageCacheSize(1000) .setDisconnectDelay(30 * 1000); } // ... }
The WebSocket protocol defines two main types of messages — text and binary — but leaves their content undefined. Instead it’s expected that client and server may agree on using a sub-protocol, i.e. a higher-level protocol that defines the message content. Using a sub-protocol is optional but either way client and server both need to understand how to interpret messages.
STOMP is a simple messaging protocol originally created for scripting languages (such as Ruby, Python and Perl) to connect to enterprise message brokers. It is designed to address a subset of commonly used patterns in messaging protocols. STOMP can be used over any reliable 2-way streaming network protocol such as TCP and WebSocket.
STOMP is a frame based protocol with frames modelled on HTTP. This is the structure of a frame:
COMMAND header1:value1 header2:value2 Body^@
For example, a client can use the SEND
command to send a message or the
SUBSCRIBE
command to express interest in receiving messages. Both of these commands
require a "destination"
header that indicates where to send a message to, or likewise
what to subscribe to.
Here is an example of a client sending a request to buy stock shares:
SEND destination:/queue/trade content-type:application/json content-length:44 {"action":"BUY","ticker":"MMM","shares",44}^@
Here is an example of a client subscribing to receive stock quotes:
SUBSCRIBE id:sub-1 destination:/topic/price.stock.* ^@
Note | |
---|---|
The meaning of a destination is intentionally left opaque in the STOMP spec. It can
be any string and it’s 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 |
STOMP servers can use the MESSAGE
command to broadcast messages to all subscribers.
Here is an example of 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}^@
Note | |
---|---|
It’s important to know that a server cannot send unsolicited messages.
All messages from a server must be in response to a specific client subscription
and the |
The above overview is intended to provide the most basic understanding of the STOMP protocol. It is recommended to review the protocol specification, which is easy to follow and manageable in terms of size.
The following summarizes the benefits for an application from using STOMP over WebSocket:
Most importantly the use of STOMP (vs plain WebSocket) enables the Spring Framework to provide a programming model for application-level use in the same way that Spring MVC provides a programming model based on HTTP.
The Spring Framework provides support for using STOMP over WebSocket through
the spring-messaging
and spring-websocket
modules. It’s easy to enable it.
Here is an example of configuring a STOMP WebSocket endpoint with SockJS fallback
options. The endpoint is available for clients to connect to at URL path /app/portfolio
:
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 configureMessageBroker(MessageBrokerRegistry config) { config.setApplicationDestinationPrefixes("/app"); config.enableSimpleBroker("/queue", "/topic"); } @Override public void registerStompEndpoints(StompEndpointRegistry registry) { registry.addEndpoint("/portfolio").withSockJS(); } // ... }
XML configuration equivalent:
<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 http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/websocket http://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="/queue, /topic"/> ... </websocket:message-broker> </beans>
On the browser side, a client might connect as follows using stomp.js and the sockjs-client:
var socket = new SockJS("/spring-websocket-portfolio/portfolio"); var stompClient = Stomp.over(socket); stompClient.connect({}, function(frame) { }
Or if connecting via WebSocket (without SockJS):
var socket = new WebSocket("/spring-websocket-portfolio/portfolio"); var stompClient = Stomp.over(socket); stompClient.connect({}, function(frame) { }
Note that the stompClient above does not need to specify a login
and passcode
headers.
Even if it did, they would be ignored, or rather overridden, on the server side. See the
sections Section 20.4.8, “Connections To Full-Featured Broker” and
Section 20.4.10, “Authentication” for more information on authentication.
When a STOMP endpoint is configured, the Spring application acts as the STOMP broker to connected clients. It handles incoming messages and sends messages back. This section provides a big picture overview of how messages flow inside the application.
The spring-messaging
module contains a number of abstractions that originated in the
Spring Integration project and are intended
for use as building blocks in messaging applications:
MessageChannel
and sends messages to registered MessageHandler
subscribers.
SubscribableChannel
that can deliver messages
asynchronously through a thread pool.
The provided STOMP over WebSocket config, both Java and XML, uses the above to assemble a concrete message flow including the following 3 channels:
"clientInboundChannel"
— for messages from WebSocket clients. Every incoming
WebSocket message carrying a STOMP frame is sent through this channel.
"clientOutboundChannel"
— for messages to WebSocket clients. Every outgoing
STOMP message from the broker is sent through this channel before getting sent
to a client’s WebSocket session.
"brokerChannel"
— for messages to the broker from within the application.
Every message sent from the application to the broker passes through this channel.
Messages on the "clientInboundChannel"
can flow to annotated
methods for application handling (e.g. a stock trade execution request) or can
be forwarded to the broker (e.g. client subscribing for stock quotes).
The STOMP destination is used for simple prefix-based routing. For example
the "/app" prefix could route messages to annotated methods while the "/topic"
and "/queue" prefixes could route messages to the broker.
When a message-handling annotated method has a return type, its return
value is sent as the payload of a Spring Message to the "brokerChannel"
.
The broker in turn broadcasts the message to clients. Sending a message
to a destination can also be done from anywhere in the application with
the help of a messaging template. For example a an HTTP POST handling method
can broadcast a message to connected clients or a service component may
periodically broadcast stock quotes.
Below is a simple example to illustrate the flow of messages:
@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 following explains the message flow for the above exmaple:
GreetingController
. The controller adds the current
time and the return value is passed through the "brokerChannel" as message
to "/topic/greeting" (destination is selected based on a convention but can be
overridden via @SendTo
).
"clientOutboundChannel"
.
The next section provides more details on annotated methods including the kinds of arguments and return values supported.
The @MessageMapping
annotation is supported on methods of @Controller
classes.
It can be used for mapping methods to message destinations and can also be combined
with the type-level @MessageMapping
for expressing shared mappings across all
annotated methods within a controller.
By default destination mappings are treated as Ant-style, slash-separated, path
patterns, e.g. "/foo*", "/foo/**". etc. They can also contain template variables,
e.g. "/foo/{id}" that can then be referenced via @DestinationVariable
-annotated
method arguments.
Note | |
---|---|
Applications can also use dot-separated destinations (vs slash).
See Section 20.4.9, “Using Dot as Separator in |
The following method arguments are supported for @MessageMapping
methods:
Message
method argument to get access to the complete message being processed.
@Payload
-annotated argument for access to the payload of a message, converted with
a org.springframework.messaging.converter.MessageConverter
.
The presence of the annotation is not required since it is assumed by default.
Payload method arguments annotated with Validation annotations (like @Validated
) will
be subject to JSR-303 validation.
@Header
-annotated arguments for access to a specific header value along with
type conversion using an org.springframework.core.convert.converter.Converter
if necessary.
@Headers
-annotated method argument that must also be assignable to java.util.Map
for access to all headers in the message.
MessageHeaders
method argument for getting access to a map of all headers.
MessageHeaderAccessor
, SimpMessageHeaderAccessor
, or StompHeaderAccessor
for access to headers via typed accessor methods.
@DestinationVariable
-annotated arguments for access to template
variables extracted from the message destination. Values will be converted to
the declared method argument type as necessary.
java.security.Principal
method arguments reflecting the user logged in at
the time of the WebSocket HTTP handshake.
The return value from an @MessageMapping
method is converted with a
org.springframework.messaging.converter.MessageConverter
and used as the body
of a new message that is then sent, by default, to the "brokerChannel"
with
the same destination as the client message but using the prefix "/topic" by
default. An @SendTo
message level annotation can be used to specify any
other destination instead.
An @SubscribeMapping
annotation can also be used to map subscription requests
to @Controller
methods. It is supported on the method level, but can also be
combined with a type level @MessageMapping
annotation that expresses shared
mappings across all message handling methods within the same controller.
By default the return value from an @SubscribeMapping
method is sent as a
message directly back to the connected client and does not pass through the
broker. This is useful for implementing request-reply message interactions; for
example, to fetch application data when the application UI is being initialized.
Or alternatively an @SubscribeMapping
method can be annotated with @SendTo
in which case the resulting message is sent to the "brokerChannel"
using
the specified target destination.
What if you wanted 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 that is to have a SimpMessagingTemplate
injected, and
use it to send messages. Typically it should be easy to have it injected by
type, for example:
@Controller public class GreetingController { private SimpMessagingTemplate template; @Autowired public GreetingController(SimpMessagingTemplate template) { this.template = template; } @RequestMapping(value="/greetings", method=POST) public void greet(String greeting) { String text = "[" + getTimestamp() + "]:" + greeting; this.template.convertAndSend("/topic/greetings", text); } }
But it can also be qualified by its name "brokerMessagingTemplate" if another bean of the same type exists.
The built-in, simple, message broker handles subscription requests from clients, stores them in memory, and broadcasts messages to connected clients with matching destinations. The broker supports path-like destinations, including subscriptions to Ant-style destination patterns.
Note | |
---|---|
Applications can also use dot-separated destinations (vs slash).
See Section 20.4.9, “Using Dot as Separator in |
The simple broker is great for getting started but supports only a subset of STOMP commands (e.g. no acks, receipts, etc), relies on a simple message sending loop, and is not suitable for clustering. Instead, applications can upgrade to using a full-featured message broker.
Check the STOMP documentation for your message broker of choice (e.g. RabbitMQ, ActiveMQ, or other), install and run the broker with STOMP support enabled. Then enable the STOMP broker relay in the Spring configuration instead of the simple broker.
Below is example configuration that enables a full-featured broker:
@Configuration @EnableWebSocketMessageBroker public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { @Override public void registerStompEndpoints(StompEndpointRegistry registry) { registry.addEndpoint("/portfolio").withSockJS(); } @Override public void configureMessageBroker(MessageBrokerRegistry registry) { registry.enableStompBrokerRelay("/topic", "/queue"); registry.setApplicationDestinationPrefixes("/app"); } }
XML configuration equivalent:
<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 http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/websocket http://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 above configuration is a Spring 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 reversely forwards all messages received from the broker to clients through their WebSocket sessions. Essentially it acts as a "relay" forwarding messages in both directions.
Note | |
---|---|
Please add a dependency on |
Furthermore, application components (e.g. HTTP request handling methods, business services, etc) can also send messages to the broker relay, as described in Section 20.4.5, “Sending Messages”, in order to broadcast messages to subscribed WebSocket clients.
In effect, the broker relay enables robust and scalable message broadcasting.
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
for this connection, i.e. the STOMP frame login
and passcode
headers. This
is exposed in both the XML namespace and the Java config as the
systemLogin
/systemPasscode
properties with default values guest
/guest
.
The STOMP broker relay also creates a separate TCP connection for every connected
WebSocket client. You can configure the STOMP credentials to use for all TCP
connections created on behalf of clients. This is exposed in both the XML namespace
and the Java config as the clientLogin
/clientPasscode
properties with default
values guest
/guest
.
Note | |
---|---|
The STOMP broker relay always sets the |
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 will continue to try to reconnect, every 5 seconds, until it succeeds.
Note | |
---|---|
A Spring bean can implement |
The STOMP broker relay can also be configured with a virtualHost
property.
The value of this property will be set as the host
header of every CONNECT frame
and may be useful for example in a cloud environment where the actual host to which
the TCP connection is established is different from the host providing the
cloud-based STOMP service.
Although slash-separated path patterns are familiar to web developers, in messaging
it is common to use "." as separator for example in the names of topics, queues,
exchanges, etc. Applications can also switch to using "." (dot) instead of "/" (slash)
as the separator in @MessageMapping
mappings by configuring a custom AntPathMatcher
.
In Java config:
@Configuration @EnableWebSocketMessageBroker public class WebsocketConfig extends AbstractWebSocketMessageBrokerConfigurer { // ... @Override public void configureMessageBroker(MessageBrokerRegistry registry) { registry.enableStompBrokerRelay("/queue/", "/topic/"); registry.setApplicationDestinationPrefixes("/app"); registry.setPathMatcher(new AntPathMatcher(".")); } }
In XML config:
<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 http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/websocket http://www.springframework.org/schema/websocket/spring-websocket.xsd"> <websocket:message-broker application-destination-prefix="/app" path-matcher="pathMatcher"> <websocket:stomp-endpoint path="/stomp" /> <websocket:simple-broker prefix="/topic, /queue"/> </websocket:message-broker> <bean id="pathMatcher" class="org.springframework.util.AntPathMatcher"> <constructor-arg index="0" value="." /> </bean> </beans>
And below is a simple example to illustrate a controller with "." separator:
@Controller @MessageMapping("foo") public class FooController { @MessageMapping("bar.{baz}") public void handleBaz(@DestinationVariable String baz) { } }
If the application prefix is set to "/app" then the foo method is effectively mapped to "/app/foo.bar.{baz}".
In a WebSocket-style application it is often useful to know who sent a message. Therefore some form of authentication is needed to establish the user identity and associate it with the current session.
Existing Web applications already use HTTP based authentication. For example Spring Security can secure the HTTP URLs of the application as usual. Since a WebSocket session begins with an HTTP handshake, that means URLs mapped to STOMP/WebSocket are already automatically protected and require authentication. Moreover the page that opens the WebSocket connection is itself likely protected and so by the time of the actual handshake, the user should have been authenticated.
When a WebSocket handshake is made and a new WebSocket session created,
Spring’s WebSocket support automatically transfers the java.security.Principal
from the HTTP request to the WebSocket session. After that every message flowing
through the application on that WebSocket session is enriched with
the user information. It’s present in the message as a header.
Controller methods can access the current user by adding a method argument of
type javax.security.Principal
.
Note that even though the STOMP CONNECT
frame has "login" and "passcode" headers
that can be used for authentication, Spring’s STOMP WebSocket support ignores them
and currently expects users to have been authenticated already via HTTP.
In some cases it may be useful to assign an identity to WebSocket session even
when the user has not formally authenticated. For example a mobile app might
assign some identity to anonymous users, perhaps based on geographical location.
The do that currently, an application can sub-class DefaultHandshakeHandler
and override the determineUser
method. The custom handshake handler can then
be plugged in (see examples in Section 20.2.4, “部署注意事项”).
An application can send messages targeting a specific user.
Spring’s STOMP support recognizes destinations prefixed with "/user/"
.
For example, a client might subscribe to the destination "/user/queue/position-updates"
.
This destination will be handled by the UserDestinationMessageHandler
and
transformed into a destination unique to the user session,
e.g. "/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 subscribing to the same destination so that each user can receive
unique stock position updates.
On the sending side messages can be sent to a destination such as
"/user/{username}/queue/position-updates"
, which in turn will be translated
by the UserDestinationMessageHandler
into one or more destinations, one for each
session associated with the user. This allows any component within the application to
send messages targeting a specific user without necessarily knowing anything more
than their name and the generic destination. This is also supported through an
annotation as well as a messaging template.
For example message-handling method can send messages to the user associated with
the message being handled through the @SendToUser
annotation:
@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 sessions, 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. This can be done by
setting the broadcast
attribute to false, for example:
@Controller public class MyController { @MessageMapping("/action") public void handleAction() throws Exception{ // raise MyBusinessException here } @MessageExceptionHandler @SendToUser(value="/queue/errors", broadcast=false) public ApplicationError handleException(MyBusinessException exception) { // ... return appError; } }
Note | |
---|---|
While user destinations generally imply an authenticated user, it isn’t required
strictly. A WebSocket session that is not associated with an authenticated user
can subscribe to a user destination. In such cases the |
It is also possible to send a message to user destinations from any application
component by injecting the SimpMessageTemplate
created by the Java config or
XML namespace, for example (the bean name is "brokerMessagingTemplate` if required
for qualification with @Qualifier
):
@Service public class TradeServiceImpl implements TradeService { private final SimpMessageTemplate messagingTemplate; @Autowired public TradeServiceImpl(SimpMessageTemplate messagingTemplate) { this.messagingTemplate = messagingTemplate; } // ... public void afterTradeExecuted(Trade trade) { this.messagingTemplate.convertAndSendToUser( trade.getUserName(), "/queue/position-updates", trade.getResult()); } }
Note | |
---|---|
When using user destinations with an external message broker, 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 destinations like |
Several ApplicationContext
events (listed below) are published and can be
received by implementing Spring’s ApplicationListener
interface.
BrokerAvailabilityEvent
— indicates when the broker becomes available/unavailable.
While the "simple" broker becomes available immediately on startup and remains so while
the application is running, the STOMP "broker relay" may lose its connection
to the full featured broker for example if the broker is restarted. The broker relay
has reconnect logic and will re-establish the "system" connection to the broker
when it comes back, hence this event is published whenever the state changes from connected
to disconnected and vice versa. Components using 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
indicating the start of a new client session. The event contains the message representing the
connect including the session id, user information (if any), and any custom headers the client
may have sent. This is useful for tracking client sessions. Components subscribed
to this event can wrap the contained message using 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 also be automatically generated when the
WebSocket session is closed. In some cases this event may be published more than once
per session. Components should be idempotent to multiple disconnect events.
Note | |
---|---|
When using a full-featured broker, the STOMP "broker relay" automatically reconnects the "system" connection should the broker become temporarily unavailable. Client connections however are not automatically reconnected. Assuming heartbeats are enabled, the client will typically notice the broker is not responding within 10 seconds. Clients need to implement their own reconnect logic. |
Furthermore, an application can directly intercept every incoming and outgoing message by
registering a ChannelInterceptor
on the respective message channel. For example
to intercept inbound messages:
@Configuration @EnableWebSocketMessageBroker public class WebsocketConfig extends AbstractWebSocketMessageBrokerConfigurer { @Override public void configureClientInboundChannel(ChannelRegistration registration) { registration.setInterceptors(new MyChannelInterceptor()); } }
A custom ChannelInterceptor
can extend the empty method base class
ChannelInterceptorAdapter
and use StompHeaderAccessor
or SimpMessageHeaderAccessor
to access information about the message.
public class MyChannelInterceptor extends ChannelInterceptorAdapter { @Override public Message<?> preSend(Message<?> message, MessageChannel channel) { StompHeaderAccessor accessor = StompHeaderAccessor.wrap(message); StompCommand command = accessor.getStompCommand(); // ... return message; } }
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, for example:
@Controller public class MyController { @MessageMapping("/action") public void handle(SimpMessageHeaderAccessor headerAccessor) { Map<String, Object> attrs = headerAccessor.getSessionAttributes(); // ... } }
It is also possible to declare a Spring-managed bean in the "websocket"
scope.
WebSocket-scoped beans can be injected into controllers and any channel interceptors
registered on the "clientInboundChannel". Those are typically singletons and live
longer than any individual WebSocket session. Therefore you will need to use a
scope proxy mode for WebSocket-scoped beans:
@Component @Scope(value="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 returned subsequently until the session ends. WebSocket-scoped beans will have all Spring lifecycle methods invoked as shown in the examples above.
There is no silver bullet when it comes to performance. Many factors may affect it including the size of messages, the volume, whether application methods perform work that requires blocking, as well as external factors such as network speed and others. 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 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 Section 20.4.3, “Flow of Messages”.
The obvious place to start is to configure the thread pools backing 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 then 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 then the thread pool size
will need to be increased.
Note | |
---|---|
A common point of confusion is that configuring the core pool size (e.g. 10) and max pool size (e.g. 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 then the thread pool will never increase beyond the core pool size since all additional tasks will be queued. Please review the Javadoc of |
On the "clientOutboundChannel"
side it is all about sending messages to WebSocket
clients. If clients are on a fast network then the number of threads should
remain close to the number of available processors. If they are slow or on
low bandwidth they will take longer to consume messages and put a burden on the
thread pool. Therefore increasing the thread pool size will be 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 there are two additional
properties related to the sending of messages. Those are the "sendTimeLimit"
and the "sendBufferSizeLimit"
. Those are used 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 may 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 mean time. Please review the Javadoc of XML schema for this configuration for important additional details.
Here is example configuration:
@Configuration @EnableWebSocketMessageBroker public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { @Override public void configureWebSocketTransport(WebSocketTransportRegistration registration) { registration.setSendTimeLimit(15 * 1000).setSendBufferSizeLimit(512 * 1024); } // ... }
<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 http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/websocket http://www.springframework.org/schema/websocket/spring-websocket.xsd"> <websocket:message-broker> <websocket:transport send-timeout="15000" send-buffer-size="524288" /> <!-- ... --> </websocket:message-broker> </beans>
The WebSocket transport configuration shown above can also be used to configure the maximum allowed size for incoming STOMP messages. Although in theory a WebSocket message can be almost unlimited in size, in pracitce WebSocket servers impose limits. For example 8K on Tomcat and 64K on Jetty. For this reason STOMP clients such as stomp.js split larger STOMP messages at 16K boundaries and send them as multiple WebSocket messages thus requiring 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. Do keep in mind that the WebSocket message size will be automatically adjusted if necessary to ensure they can carry 16K WebSocket messages at a minimum.
Here is example configuration:
@Configuration @EnableWebSocketMessageBroker public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { @Override public void configureWebSocketTransport(WebSocketTransportRegistration registration) { registration.setMessageSizeLimit(128 * 1024); } // ... }
<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 http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/websocket http://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 is using multiple application instances. Currently it is not possible to do that with the simple broker. However when using 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.
When using @EnableWebSocketMessageBroker
or <websocket:message-broker>
key
infrastructure components automatically gather stats 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 INFO 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.
Below is a summary of the available information.
有两个主要的方法来测试使用Spring STOMP来提供WebSocket支持的应用. 第一个就是写一个服务器端的测试来验证控制器的函数和那些注解的消息处理方法. 第二个是写一个完整的端到端的测试包含了运行的客户端和服务器端
There are two main approaches to testing applications using Spring’s STOMP over WebSocket support. The first is to write server-side tests verifying 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’re 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 can’t test that.
Ideally controllers under test should be invoked as they are at runtime, much like the approach to testing controllers handling HTTP requests using the Spring MVC Test framework. i.e. without running a Servlet container but relying on the Spring Framework to invoke the annotated controllers. Just like with Spring MVC Test here there are two two possible alternatives, either using a "context-based" or "standalone" setup:
1.使用spring testContext framework 来帮助加载现行的spring配置文件, 注入"clientInboundChannel"作为一个试验田,使用它来发送被控制器方法处理的消息
SimpAnnotationMethodMessageHandler
) and pass messages for
controllers directly to it.
Both of these setup scenarios are demonstrated in the tests for the stock portfolio sample application.
The second approach is to create end-to-end integration tests. For that you will need to run a WebSocket server in embedded mode and connect to it as a WebSocket client sending WebSocket messages containing STOMP frames. The tests for the stock portfolio sample application also demonstrate this approach using Tomcat as the embedded WebSocket server and a simple STOMP client for test purposes.