Spring Framework
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

379 lines
17 KiB

[[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 xref:web/webmvc/mvc-servlet.adoc[`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 xref:web/websocket/server.adoc#websocket-server-allowed-origins[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`).