CodeGym /Java Course /Module 5. Spring /Fallback via SockJS protocol

Fallback via SockJS protocol

Module 5. Spring
Level 10 , Lesson 16
Available

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 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.

During pre-development, enable the 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.

When using STOMP over WebSocket and SockJS, if the STOMP client and server agree to exchange heartbeat messages, SockJS heartbeat messages are disabled.

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).

As a result, network I/O failures may occur due to the client connection being lost, which may cause the log to fill with unnecessary stack trace data. Spring makes every effort to identify those network failures that represent a loss of connection to the client (specific to each server) and writes a compressed message to the log using a special log category, 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 value Origin.

  • Access-Control-Allow-Credentials: Always set to true.

  • 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 type TransportType).

  • 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 uses RestTemplate from Spring for HTTP requests.

  • JettyXhrTransport uses HttpClient 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");
SockJS uses arrays in JSON format to generate messages. The default is the Jackson 2 library, which should be in the classpath. In addition, you can configure a custom implementation of 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); 
    }
    // ...
}
  1. Set the streamBytesLimit property to 512 KB (default 128 KB - 128 * 1024).
  2. Set the httpMessageCacheSize property equal to 1,000 (default 100).
  3. Set the disconnectDelay property to 30 seconds (default is five seconds - 5 * 1000).
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION