SockJS Fallback

在公共网络上,不受您控制的限制性代理可能会阻止 WebSocket 交互,原因可能是它们未配置为传递“升级”标头,或因为它们关闭了看似空闲的长寿命连接。

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.

解决此问题的方法是 WebSocket 模拟,即尝试先使用 WebSocket,然后采用基于 HTTP 的技术来模拟 WebSocket 交互并公开相同的应用程序级 API。

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.

在 Servlet 堆栈上,Spring Framework 为 SockJS 协议提供服务器(以及客户端)支持。

On the Servlet stack, the Spring Framework provides both server (and also client) support for the SockJS protocol.

Overview

SockJS 的目标是让应用程序能够在运行时使用 WebSocket API,但在必要时自动退回到非 WebSocket 备用项,而无需更改应用程序代码。

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 由以下部分组成:

SockJS consists of:

  • The SockJS protocol defined in the form of executable narrated tests.

  • The 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 设计用于浏览器,它使用各种技术来支持多种浏览器版本。有关 SockJS 传输类型和浏览器的完整列表,请参阅https://github.com/sockjs/sockjs-client[SockJS 客户端]页面。传输分为三大类:WebSocket、HTTP 流媒体和 HTTP 长轮询。有关这些类别的概述,请参阅https://spring.io/blog/2012/05/08/spring-mvc-3-2-preview-techniques-for-real-time-updates/[这篇博文]。

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 SockJS client page. Transports fall in three general categories: WebSocket, HTTP Streaming, and HTTP Long Polling. For an overview of these categories, see this blog post.

SockJS 客户端首先发送 GET /info 以从服务器获取基本信息。之后,它必须决定使用哪种传输。如果可能,将使用 WebSocket。如果不能,则在大多数浏览器中至少会提供一个 HTTP 流选项。如果不能,则使用 HTTP(长)轮询。

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.

所有传输请求具有以下 URL 结构:

All transport requests have the following URL structure:

https://host:port/myApp/myEndpoint/{server-id}/{session-id}/{transport}

其中:

where:

  • {server-id} is useful for routing requests in a cluster but is not used otherwise.

  • {session-id} correlates HTTP requests belonging to a SockJS session.

  • {transport} indicates the transport type (for example, websocket, xhr-streaming, and others).

WebSocket 传输只需要一个 HTTP 请求来完成 WebSocket 握手。此后,所有消息都通过该套接字进行交换。

The WebSocket transport needs only a single HTTP request to do the WebSocket handshake. All messages thereafter are exchanged on that socket.

HTTP 传输需要更多请求。例如,Ajax/XHR 流依靠一个长期运行的请求来传送服务器到客户端的消息,以及用于传送客户端到服务器消息的其他 HTTP POST 请求。长轮询类似,只是它在每次服务器到客户端发送之后结束当前请求。

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 会添加最少的邮件框架。例如,服务器最初会发送字母 o("`open`"框架),邮件作为 a["message1","message2"]`发送(JSON 编码数组),如果没有邮件在 25 秒(默认值)内流动,则发送字母 `h("`heartbeat`"框架),字母 c("`close`"框架)用于关闭会话。

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.

要了解更多信息,请在浏览器中运行示例并观察 HTTP 请求。SockJS 客户端允许修复传输列表,因此可以一次看到每个传输。SockJS 客户端还提供一个调试标志,该标志在浏览器控制台中启用有用的消息。在服务器端,你可以为`org.springframework.web.socket`启用`TRACE`日志记录。要了解更详细的信息,请参阅 SockJS 协议 narrated test

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

Enabling SockJS

您可以通过配置启用 SockJS,如下面的示例所示:

You can enable SockJS through configuration, as the following example shows:

  • Java

  • Kotlin

  • Xml

@Configuration
@EnableWebSocket
public class WebSocketConfiguration implements WebSocketConfigurer {

	@Override
	public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
		registry.addHandler(myHandler(), "/myHandler").withSockJS();
	}

	@Bean
	public WebSocketHandler myHandler() {
		return new MyHandler();
	}

}
@Configuration
@EnableWebSocket
class WebSocketConfiguration : WebSocketConfigurer {
	override fun registerWebSocketHandlers(registry: WebSocketHandlerRegistry) {
		registry.addHandler(myHandler(), "/myHandler").withSockJS()
	}

	@Bean
	fun myHandler(): WebSocketHandler {
		return MyHandler()
	}
}
<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.docs.web.websocket.websocketserverhandler.MyHandler"/>

</beans>

之前示例用于 Spring MVC 应用程序,应包含在配置 DispatcherServlet中。但是,Spring 的 WebSocket 和 SockJS 支持并不依赖 Spring MVC。在其https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/socket/sockjs/support/SockJsHttpRequestHandler.html[SockJsHttpRequestHandler]的帮助下,集成到其它 HTTP 服务环境中相对简单。

The preceding example 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.

在浏览器端,应用程序可以使用https://github.com/sockjs/sockjs-client[sockjs-client](1.0.x 版本)。它模拟 W3C WebSocket API,并与服务器通信以选择最佳传输选项,具体取决于其所在浏览器。请参阅https://github.com/sockjs/sockjs-client[sockjs-client]页面和浏览器支持的传输类型列表。客户端还提供几个配置选项——例如,指定要包含哪些传输。

On the browser side, applications can use the 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 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.

IE 8 and 9

Internet Explorer 8 和 9 仍在使用中。它们是使用 SockJS 的主要原因。本部分涵盖了在这些浏览器中运行时的重要注意事项。

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.

SockJS 客户端通过使用 Microsoft 的 XDomainRequest在 IE 8 和 9 中支持 Ajax/XHR 流媒体。这可在不同域中运行,但不支持发送 cookie。对于 Java 应用程序而言,cookie 通常非常重要。但是,由于 SockJS 客户端可用在许多服务器类型(不仅仅是 Java 类型)中,因此它需要知道 cookie 是否重要。如果重要,则 SockJS 客户端首选使用 Ajax/XHR 进行流媒体处理。否则,它依靠基于 iframe 的技术。

The SockJS client supports Ajax/XHR streaming in IE 8 and 9 by using Microsoft’s 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.

SockJS 客户端的第一个 /info 请求是对信息的请求,该信息可以影响客户端对传输的选取。其中一个详细信息是服务器应用程序是否依赖于 Cookie(例如,用于认证目的或具有粘性会话的群集)。Spring 的 SockJS 支持包含一个名为 sessionCookieNeeded 的属性。它在默认情况下处于启用状态,因为大多数 Java 应用程序依赖于 JSESSIONID Cookie。如果您的应用程序不需要它,则可以关闭该选项,然后 SockJS 客户端在 IE 8 和 9 中应选择 xdr-streaming

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.

如果您确实使用基于 iframe 的传输,请记住可以通过将 HTTP 响应头 X-Frame-Options`设置为 `DENY,`SAMEORIGIN`或 `ALLOW-FROM <origin>`来指示浏览器在给定页面上阻塞iframe 的使用。此方法用于防止 clickjacking

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

Spring Security 3.2+ 提供了在每个响应上设置 X-Frame-Options 的支持。默认情况下,Spring Security Java 配置将其设置为 DENY。在 3.2 中,Spring Security XML 命名空间在默认情况下不设置该标头,但可以配置它来这样做。将来,它可能会将其设置为默认值。

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.

请参阅 Spring Security 文档的 默认安全头 来了解如何配置设置 `X-Frame-Options`头的详细信息。您还可以在 gh-2718 中查看更多背景信息。

See 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 gh-2718 for additional background.

如果您的应用程序添加了 X-Frame-Options 响应头(它应该添加该标头!)并依赖于基于 iframe 的传输,则需要将标头值设置为 SAMEORIGINALLOW-FROM <origin>。Spring SockJS 支持还需要知道 SockJS 客户端的位置,因为它是从 iframe 加载的。默认情况下,iframe 被设置为从 CDN 位置下载 SockJS 客户端。最好配置该选项以使用与应用程序相同的原点的 URL。

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.

以下示例展示了如何在 Java 配置中执行此操作:

The following example shows how to do so in 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");
	}

	// ...

}

XML 命名空间通过 <websocket:sockjs> 元素提供类似的选项。

The XML namespace provides a similar option through the <websocket:sockjs> element.

在初始开发期间,启用 SockJS 客户端 devel 模式以防止浏览器缓存 SockJS 请求(例如 iframe),否则会缓存这些请求。有关如何启用它的详细信息,请参阅 [SockJS 客户端](https://github.com/sockjs/sockjs-client) 页面。

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 SockJS client page.

Heartbeats

SockJS 协议要求服务器发送心跳消息以防止代理确定连接挂起。Spring SockJS 配置有一个名为 `heartbeatTime`的属性,您可以使用它来自定义频率。默认情况下,在 25 秒后才会发送心跳,前提是该连接上未发送任何其他邮件。此 25 秒值符合下列https://datatracker.ietf.org/doc/html/rfc6202[IETF 建议],适用于公共互联网应用程序。

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 IETF recommendation for public Internet applications.

在通过 WebSocket 和 SockJS 使用 STOMP 时,如果 STOMP 客户端和服务器协商要交换 heartbeat,则禁用 SockJS heartbeat。

When using STOMP over WebSocket and SockJS, if the STOMP client and server negotiate heartbeats to be exchanged, the SockJS heartbeats are disabled.

Spring SockJS 支持还允许您配置 TaskScheduler 以调度心跳任务。任务调度程序由线程池支持,其默认设置基于可用的处理器数量。您应该根据您的特定需求考虑自定义设置。

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.

Client Disconnects

HTTP 流媒体和 HTTP 长轮询 SockJS 传输要求连接保持打开的时间比通常情况下更长。有关这些技术的概述,请参阅https://spring.io/blog/2012/05/08/spring-mvc-3-2-preview-techniques-for-real-time-updates/[这篇博文]。

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.

在 Servlet 容器中,这通过 Servlet 3 异步支持来实现,该支持允许退出 Servlet 容器线程、处理请求,并继续从另一个线程写入响应。

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.

一个具体问题是,Servlet API 未针对已离开的客户端提供通知。请参阅 eclipse-ee4j/servlet-api#44。但是,Servlet 容器会在后续尝试写入响应时引发异常。由于 Spring 的 SockJS 服务支持服务器发送心跳(默认每 25 秒一次),这意味着通常会在该时间段内(或在更早时间,如果邮件发送频率更高)检测到客户端断开连接。

A specific issue is that the Servlet API does not provide notifications for a client that has gone away. See 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).

因此,网络 I/O 故障可能发生是因为客户端已断开连接,这可能会用不必要的堆栈跟踪填充日志。Spring 尽最大努力识别表示客户端断开的此类网络故障(特定于每台服务器),并使用专用日志类别记录一则最小化消息,DISCONNECTED_CLIENT_LOG_CATEGORY(在 AbstractSockJsSession 中定义)。如果您需要查看堆栈跟踪,您可以将该日志类别设置为 TRACE。

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.

SockJS and CORS

如果你允许跨源请求(参见Allowed Origins),SockJS 协议在 XHR 流传输和轮询传输中使用 CORS 来支持跨域。因此,CORS 头会自动添加,除非检测到响应中存在 CORS 头。因此,如果应用程序已经配置为提供 CORS 支持(例如,通过 Servlet Filter),Spring 的`SockJsService`将跳过此部分。

If you allow cross-origin requests (see 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.

还可以通过在 Spring 的 SockJsService 中设置 suppressCors 属性来禁用添加这些 CORS 标头。

It is also possible to disable the addition of these CORS headers by setting the suppressCors property in Spring’s SockJsService.

SockJS 期待以下标头和值:

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

有关确切的实现,请参见源代码中的 AbstractSockJsService 中的 addCorsHeadersTransportType 枚举。

For the exact implementation, see addCorsHeaders in AbstractSockJsService and the TransportType enum in the source code.

或者,如果 CORS 配置允许,可以考虑排除带有 SockJS 端点的 URL 前缀,从而让 Spring 的 SockJsService 处理它。

Alternatively, if the CORS configuration allows it, consider excluding URLs with the SockJS endpoint prefix, thus letting Spring’s SockJsService handle it.

SockJsClient

Spring 提供了一个 SockJS Java 客户端,无需使用浏览器即可连接到远程 SockJS 端点。当两个服务器需要通过公共网络进行双向通信(即网络代理可能禁止使用 WebSocket 协议)时,这可能尤其有用。SockJS Java 客户端对于测试目的也十分有用(例如,为了模拟大量并发用户)。

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

SockJS Java 客户端支持 websocketxhr-streamingxhr-polling 协议。其余协议仅适用于浏览器。

The SockJS Java client supports the websocket, xhr-streaming, and xhr-polling transports. The remaining ones only make sense for use in a browser.

您可以使用以下配置 WebSocketTransport

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.

按定义,XhrTransport 既支持 xhr-streaming 也支持 xhr-polling,因为从客户端角度看,二者的唯一区别在于用于连接到服务器的 URL。目前有两种实现:

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.

以下示例展示了如何创建 SockJS 客户端并连接到 SockJS 端点:

The following example shows how to create a SockJS client and connect to a 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 使用 JSON 格式化数组来作为消息。默认情况下,使用 Jackson 2,它需要位于类路径中。或者,您可以配置自定义实现 SockJsMessageCodec,然后在 SockJsClient 上对其进行配置。

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.

要使用 SockJsClient 模拟大量并发用户,您需要配置底层 HTTP 客户端(对于 XHR 协议)允许足够的连接和线程。以下示例展示了如何借助 Jetty 进行操作:

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:

HttpClient jettyHttpClient = new HttpClient();
jettyHttpClient.setMaxConnectionsPerDestination(1000);
jettyHttpClient.setExecutor(new QueuedThreadPool(1000));

以下示例展示了您还应考虑自定义的服务器端 SockJS 相关属性(有关详细信息,请参阅 javadoc):

The following example shows the server-side SockJS-related properties (see javadoc for details) that you should also consider customizing:

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