SockJS Fallback

在公共网络上,不受您控制的限制性代理可能会阻止 WebSocket 交互,原因可能是它们未配置为传递“升级”标头,或因为它们关闭了看似空闲的长寿命连接。 解决此问题的方法是 WebSocket 模拟,即尝试先使用 WebSocket,然后采用基于 HTTP 的技术来模拟 WebSocket 交互并公开相同的应用程序级 API。 在 Servlet 堆栈上,Spring Framework 为 SockJS 协议提供服务器(以及客户端)支持。

Overview

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

SockJS 由以下部分组成:

  • SockJS 协议 定义为可执行 叙述式测试 的形式。

  • SockJS JavaScript 客户端——一个可用于浏览器的客户端库。

  • SockJS 服务器实现,包括 Spring Framework 中的 @(18) 模块。

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

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

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

其中:

  • @(20) 对于在集群中路由请求非常有用,但不会用在其他地方。

  • @(21) 关联属于 SockJS 会话的 HTTP 请求。

  • @(22) 指示传输类型(例如,@(23)、@(24) 和其他)。

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

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

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

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

Enabling SockJS

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

  • 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 服务环境中相对简单。

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

IE 8 and 9

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

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

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

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

Spring Security 3.2+ 提供了在每个响应上设置 X-Frame-Options 的支持。默认情况下,Spring Security Java 配置将其设置为 DENY。在 3.2 中,Spring Security XML 命名空间在默认情况下不设置该标头,但可以配置它来这样做。将来,它可能会将其设置为默认值。 请参阅 Spring Security 文档的 默认安全头 来了解如何配置设置 `X-Frame-Options`头的详细信息。您还可以在 gh-2718 中查看更多背景信息。

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

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

@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> 元素提供类似的选项。

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

Heartbeats

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

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

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

Client Disconnects

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

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

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

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

SockJS and CORS

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

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

SockJS 期待以下标头和值:

  • @(25): 从 @(26) 请求头的值初始化。

  • @(27): 始终设置为 @(28)。

  • @(29): 从同等请求头的值初始化。

  • @(30): 传输支持的 HTTP 方法(请参阅 @(31) 枚举)。

  • @(32): 设置为 31536000(1 年)。

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

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

SockJsClient

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

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

您可以使用以下配置 WebSocketTransport

  • JSR-356 运行时中的 @(33)。

  • `JettyWebSocketClient`通过使用 Jetty 9+ 本机 WebSocket API。

  • Spring 的任何 `WebSocketClient`实施。

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

  • RestTemplateXhrTransport`为 HTTP 请求使用 Spring 的 `RestTemplate

  • JettyXhrTransport`为 HTTP 请求使用 Jetty 的 `HttpClient

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

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 上对其进行配置。

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

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

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

@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 streamBytesLimit`属性设置为 512KB(默认值为 128KB —— `128 * 1024)。
2 httpMessageCacheSize`属性设置为 1,000(默认值为 `100)。
3 disconnectDelay`属性设置为 30 属性秒(默认值为 5 秒 —— `5 * 1000)。