On the public Internet, restrictive proxies outside of your control may prevent WebSocket communications, either
because they are not configured to send the Upgrade
header or because they close long-lived connections
, which appear to be greyed out.
The solution to this problem is WebSocket emulation - that is, trying to use WebSocket first and then fall back to HTTP-based technologies that emulate WebSocket communication and provide the same API at the application level .
In the servlet stack, the Spring Framework provides server-side (as well as client-side) support for the SockJS protocol.
Brief Description
The purpose of SockJS is to allow applications to use the WebSocket API, but Fall back to non-WebSocket alternatives at runtime if necessary, without having to change application code.
SockJS consists of:
SockJS protocol defined as executables tests with comments.
SockJS client from JavaScript - a client library for use in browsers.
SockJS server implementations, including one in
spring-websocket
module from the Spring Framework.SockJS Java client in the
spring-websocket
module (since version 4.1 ).
SockJS is designed for use in browsers. It uses various techniques to support a wide range of browser versions. For a complete list of SockJS delivery engine types and browsers, see the SockJS client page. Transfer mechanisms fall into three general categories: WebSocket, HTTP streaming, and HTTP long polling. For an overview of these categories, see this blog article.
The SockJS client starts by sending GET
/info
to get basic information from the server. After this, he must decide which transfer mechanism to use.
If possible, the WebSocket protocol is used. If not, most browsers have at least one HTTP streaming option. If there
is none, then HTTP polling (long) is used.
All transfer requests have the following URL structure:
https://host:port/myApp/myEndpoint/{server-id}/{session-id}/{transport}
where:
{server-id}
is used to route requests in the cluster, but is not used otherwise .{session-id}
maps HTTP requests belonging to a SockJS session.{transport}
indicates the type of transport mechanism (for example,websocket
,xhr-streaming
and others).
The WebSocket transfer mechanism requires only one HTTP request to confirm the establishment of the WebSocket protocol. All subsequent messages are sent over this socket.
HTTP transport mechanisms require more requests. Ajax/XHR streaming, for example, uses a single long-running request to pass messages from the server to the client and additional HTTP POST requests to pass messages from the client to the server. Long polling works in a similar way, except that it terminates the current request after each data is sent from the server to the client.
SockJS adds simple message framing. For example, the server initially sends the letter o
("open"
frame), messages are sent as a["message1", "message2"]
(JSON-encoded array), the letter
h
("heartbeat" frame) if no messages are received within 25 seconds (default), and the letter
c
("close" frame) to close the session.
To learn more, run the example in a browser and view the HTTP requests. The SockJS client allows you to capture a
list of transfer mechanisms, so you can view each mechanism in turn. The SockJS client also provides a debug
flag that allows you to output useful messages to the browser console. On the server side, you can enable TRACE
logging for org.springframework.web.socket
. Even more detailed information can be found in test of
the SockJS protocol with comments.
Activating SockJS
You can activate SockJS through the Java configuration, as shown in the following example:
@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 equivalent of the configuration from the previous example:
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:websocket="http://www.springframework.org/schema/websocket"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/websocket
https://www.springframework.org/schema/websocket/spring-websocket.xsd">
<websocket:handlers>
<websocket:mapping path="/myHandler" handler="myHandler"/>
<websocket:sockjs/>
</websocket:handlers>
<bean id="myHandler" class="org.springframework.samples.MyHandler"/>
</beans>
The previous example is intended for use in Spring MVC applications and should be included in the
DispatcherServlet
configuration. However, Spring's support for WebSocket and SockJS is independent of
Spring MVC. Using SockJsHttpRequestHandler
is relatively easy to integrate into other HTTP
frameworks.
On the browser side, applications can use sockjs-client
(version 1.0.x). It emulates the WebSocket API standardized by the
W3C consortium and interacts with the server to select the best transfer option depending on the browser it is
running in. See sockjs-client for a list of
browser-supported transfer mechanisms. The client also provides several configuration options - for example, to
specify which transport mechanisms to include.
IE 8 and 9
Internet Explorer 8 and 9 are still used. They are the key reason for having SockJS. This section covers important aspects of working in these browsers.
The SockJS client supports Ajax/XHR streaming in IE 8 and 9 using XDomainRequest
from Microsoft. This method works across domains, but does not
support sending cookies. Cookies are often necessary for Java applications to function. However, since the SockJS
client can be used with many types of servers (not just Java), it needs to understand whether cookies have any
significance. If yes, then the SockJS client will prefer Ajax/XHR for streaming. Otherwise, the iframe-based
technique is used.
The first /info
request from the SockJS client is a request for information
that may influence the client's choice of transfer mechanism. Part of this information is whether the server
application uses cookies (for example, for authentication or clustering in sticky sessions). Spring's SockJS support
includes a property called sessionCookieNeeded
. This feature is enabled by default because most Java
applications use the JSESSIONID
cookie. If your application does not require it, you can disable this
option and the SockJS client will then have to select xdr-streaming
in IE 8 and 9.
If you are
using a streaming mechanism based iframe, be aware that browsers can be instructed to block the use of an IFrame on
a given page by setting the X-Frame-Options
HTTP response header to DENY
,
SAMEORIGIN
or ALLOW-FROM <origin>
. This is used to prevent clickjacking.
Spring Security 3.2+ provides support for setting the X-Frame-Options
header for each response. By default, the Spring Security Java configuration is set to DENY
. In version
3.2, the Spring Security XML namespace does not set this header by default, but it can be configured to do so. In
the future, it will be able to set this title by default.
See: section "Default security headers" See the Spring Security documentation for details on how to
configure the X-Frame-Options
header options. You can also check out gh-2718 for
more
information.
If your application adds a X-Frame-Options
response header (as it
should!) and uses an iframe-based delivery mechanism, then you need to set the header value to
SAMEORIGIN
or ALLOW-FROM <origin>
. The Spring support for SockJS also needs to know
the location of the SockJS client since it is loaded from an iframe. By default, the iframe is configured to load
the SockJS client from the CDN. It's a good idea to configure this setting to use a URL from the same origin as the
application.
The following example shows how to do this using Java configuration:
@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 capability through the <websocket:sockjs>
element.
devel
mode of the SockJS
client, which will prevent the browser from caching SockJS requests (such as iframes) that would otherwise case
would be cached. For more information on how to activate it, see the SockJS client page.
Heartbeat messages
The SockJS protocol requires sending heartbeat messages from the server side so
that proxy servers cannot conclude that the connection is hung. The SockJS configuration for Spring has a heartbeatTime
property that can be used to set the frequency. By default, a heartbeat is sent after 25 seconds, provided that no
other messages have been sent for the connection. This 25-second value is consistent with the following IETF recommendation for public Internet
applications.
SockJS Support Tools Spring also allows you to configure TaskScheduler
to schedule heartbeat message
tasks. The task scheduler uses a thread pool whose default settings depend on the number of available processors.
You should consider customizing the settings to suit your specific needs.
Clearing the client connection
HTTP streaming and long HTTP polling require the SockJS transfer mechanisms to keep the connection open longer than usual. A brief description of these methods is provided in this blog article.
In servlet containers, this is accomplished through Servlet 3's asynchronous support, which allows you to exit the servlet container thread, process the request, and continue writing to the response from another thread.
The specific problem is that the Servlet API does not provide notifications for a client that has gone missing. See eclipse-ee4j/servlet-api#44. However, servlet containers throw an exception on subsequent attempts to write to the response. Since the Spring service for SockJS supports heartbeat messages sent by the server (every 25 seconds by default), this means that a client connection failure is usually detected within this period of time (or sooner if messages are sent more frequently).
DISCONNECTED_CLIENT_LOG_CATEGORY
(defined
in AbstractSockJsSession
). If you need to see the stack trace, you can set the log category to TRACE.
SockJS and CORS
If you allow requests between different origins, the SockJS protocol will use the
CORS mechanism for cross-domain support in streaming XHR transport and polling transport. Therefore, CORS headers
are added automatically if the presence of CORS headers is not detected in the response. Thus, if the application is
already configured to support CORS (for example, through a servlet filter), Spring's SockJsService
skips this part.
You can also disable the addition of these CORS headers by setting the property suppressCors
in SockJsService from Spring.
SockJS accepts the following headers and values:
Access-Control-Allow-Origin
: Initialized from the request header valueOrigin
.Access-Control-Allow-Credentials
: Always set totrue
.Access-Control-Request-Headers
: Initialized from the values of the equivalent request header.Access-Control-Allow-Methods
: HTTP methods that the transport mechanism supports (see enum typeTransportType
).Access-Control-Max-Age
: Sets the value to 31536000 (1 year).
For the exact implementation, see addCorsHeaders
in AbstractSockJsService
and the
TransportType
enum type in the source code.
Also, if CORS- configuration allows this, consider
excluding URLs prefixed with a SockJS endpoint, thereby allowing Spring's SockJsService
to handle them.
SockJsClient
Spring provides a SockJS Java client for connecting to remote SockJS endpoints without using a browser. This can be especially useful if you need to provide bidirectional communication between two servers over a public network (that is, where network proxies may prevent the use of the WebSocket protocol). The SockJS Java client is also very useful for testing (for example, to simulate a large number of concurrent users).
The SockJS Java client supports websocket
, xhr-streaming
and xhr-polling
.
The rest only make sense for use in a browser.
You can configure WebSocketTransport
with:
StandardWebSocketClient
in the JSR-356 runtime.JettyWebSocketClient,
using the WebSocket API built into Jetty 9+.Any implementation of
WebSocketClient
from Spring.
XhrTransport
, by definition, supports both xhr-streaming
and xhr-polling
because, from the client's point of view, there is no difference other than the URL used to connect to the
server. There are currently two implementations:
RestTemplateXhrTransport
usesRestTemplate
from Spring for HTTP requests.JettyXhrTransport
usesHttpClient
from Jetty for HTTP requests.
In the following example shows how to create a SockJS client and connect to the SockJS endpoint:
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");
SockJsMessageCodec
and configure it on SockJsClient
.
To use SockJsClient
to simulate a large number of simultaneous users, you need to configure the
underlying HTTP client (for the XHR transport mechanism) with a sufficient number of connections and threads.
The following example shows how to do this using Maven:
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 configure:
@Configuration
public class WebSocketConfig extends WebSocketMessageBrokerConfigurationSupport {
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/sockjs").withSockJS()
.setStreamBytesLimit(512 * 1024)
.setHttpMessageCacheSize(1000)
.setDisconnectDelay(30 * 1000);
}
// ...
}
- Set the
streamBytesLimit
property to 512 KB (default 128 KB -128 * 1024
). - Set the
httpMessageCacheSize
property equal to 1,000 (default100
). - Set the
disconnectDelay
property to 30 seconds (default is five seconds -5 * 1000
).
GO TO FULL VERSION