SockJS 回退

在公共互联网上,您无法控制的限制性代理可能会阻止WebSocket交互,要么是因为它们未配置传递 Upgrade 头,要么是因为它们关闭看起来处于空闲状态的长连接。

解决这个问题的方法是WebSocket仿真,即首先尝试使用WebSocket,然后在运行时必要时回退到模拟WebSocket交互并公开相同应用级别API的基于HTTP的技术。

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

概述

SockJS的目标是让应用程序在必要时在运行时使用WebSocket API,但无需更改应用程序代码即可回退到非WebSocket替代方案。

SockJS由以下组成:

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

  • SockJS JavaScript客户端 —— 用于在浏览器中使用的客户端库。

  • SockJS服务器实现,包括Spring Framework中的一个 spring-websocket 模块。

  • 自Spring版本4.1起,在 spring-websocket 模块中提供了一个SockJS Java客户端。

SockJS设计用于在浏览器中使用。它使用各种技术来支持各种浏览器版本。有关SockJS传输类型和浏览器的完整列表,请参阅 SockJS客户端 页面。传输分为三个一般类别:WebSocket、HTTP流和HTTP长轮询。有关这些类别的概述,请参阅 此博客文章

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

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

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

其中:

  • {server-id} 用于在集群中路由请求,但在其他情况下不使用。

  • {session-id} 对应于属于SockJS会话的HTTP请求。

  • {transport} 指示传输类型(例如,websocketxhr-streaming 等)。

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

HTTP传输需要更多请求。例如,Ajax/XHR流依赖于一个长时间运行的请求用于服务器到客户端的消息,并且还需要额外的HTTP POST请求用于客户端到服务器的消息。长轮询类似,只是在每次服务器到客户端发送后结束当前请求。

SockJS添加了最小的消息框架。例如,服务器最初发送字母 o(“打开”帧),消息以 a["message1","message2"](JSON编码数组)的形式发送,如果25秒内没有消息流动,则发送字母 h(“心跳”帧)(默认情况下),并发送字母 c(“关闭”帧)来关闭会话。

要了解更多信息,请在浏览器中运行示例并观察HTTP请求。SockJS客户端允许固定传输列表,因此可以逐个查看每种传输方式。SockJS客户端还提供了一个调试标志,可在浏览器控制台中启用有用的消息。在服务器端,您可以为 org.springframework.web.socket 启用 TRACE 日志记录。要了解更多详细信息,请参阅SockJS协议的 叙述测试

启用SockJS

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

@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();
	}

}

以下示例显示了前面示例的XML配置等效项:

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

前面的示例适用于Spring MVC应用程序,并应包含在 DispatcherServlet 的配置中。但是,Spring的WebSocket和SockJS支持不依赖于Spring MVC。通过 SockJsHttpRequestHandler 的帮助,将其集成到其他HTTP服务环境中相对简单。

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

IE 8 和 9

Internet Explorer 8 和 9 仍在使用中。它们是使用 SockJS 的一个关键原因。本节涵盖了在这些浏览器中运行时的重要考虑事项。

SockJS 客户端通过使用微软的 XDomainRequest 来支持 IE 8 和 9 中的 Ajax/XHR 流。这在跨域情况下有效,但不支持发送 cookies。Cookies 对于 Java 应用程序通常是必不可少的。然而,由于 SockJS 客户端可以与许多服务器类型一起使用(不仅仅是 Java 服务器),它需要知道 cookies 是否重要。如果是,SockJS 客户端会优先选择 Ajax/XHR 进行流处理。否则,它会依赖基于 iframe 的技术。

SockJS 客户端的第一个 /info 请求是一个请求信息的请求,可以影响客户端选择传输方式。其中一个细节是服务器应用程序是否依赖 cookies(例如,用于身份验证目的或与粘性会话集群)。Spring 的 SockJS 支持包括一个名为 sessionCookieNeeded 的属性。它默认启用,因为大多数 Java 应用程序依赖于 JSESSIONID cookie。如果您的应用程序不需要它,您可以关闭此选项,然后 SockJS 客户端应该选择在 IE 8 和 9 中使用 xdr-streaming

如果您使用基于 iframe 的传输,请记住浏览器可以通过设置 HTTP 响应头 X-Frame-OptionsDENYSAMEORIGINALLOW-FROM <origin> 来阻止在给定页面上使用 IFrames。这用于防止 点击劫持

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

有关如何配置设置 X-Frame-Options 头的详细信息,请参阅 Spring Security 文档中的 默认安全头。您还可以查看 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 客户端 页面。

心跳

SockJS 协议要求服务器发送心跳消息,以防止代理认为连接已挂起。Spring SockJS 配置有一个名为 heartbeatTime 的属性,您可以用来自定义频率。默认情况下,如果在该连接上没有发送其他消息,将在 25 秒后发送一个心跳。这个 25 秒的值符合以下 IETF 建议 适用于公共互联网应用程序。

在使用 STOMP over WebSocket 和 SockJS 时,如果 STOMP 客户端和服务器协商要交换心跳,则将禁用 SockJS 的心跳。

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

客户端断开连接

HTTP 流和 HTTP 长轮询的 SockJS 传输需要连接保持打开的时间比通常更长。有关这些技术的概述,请参阅 此博客文章

在 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 和 CORS

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

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

SockJS 预期以下头和值:

  • Access-Control-Allow-Origin:从 Origin 请求头的值初始化。

  • Access-Control-Allow-Credentials:始终设置为 true

  • Access-Control-Request-Headers:从等效请求头的值初始化。

  • Access-Control-Allow-Methods:传输支持的 HTTP 方法(参见 TransportType 枚举)。

  • Access-Control-Max-Age:设置为 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运行时中使用StandardWebSocketClient

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

  • 使用Spring的任何WebSocketClient的实现。

根据定义,XhrTransport支持xhr-streamingxhr-polling,因为从客户端的角度来看,除了用于连接到服务器的URL之外,没有任何区别。目前有两种实现:

  • RestTemplateXhrTransport使用Spring的RestTemplate进行HTTP请求。

  • JettyXhrTransport使用Jetty的HttpClient进行HTTP请求。

以下示例展示了如何创建一个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 * 1000)。