对于最新的稳定版本,请使用 Spring Framework 7.0.6!spring-doc.cadn.net.cn

SockJS 回退选项

通过公共互联网,您无法控制的限制性代理可能会阻止WebSocket交互,原因可能是它们未配置为传递Upgrade标头,或者因为它们会关闭看起来处于空闲状态的长连接。spring-doc.cadn.net.cn

这个问题的解决方案是WebSocket模拟——也就是说,首先尝试使用WebSocket,然后回退到基于HTTP的技术,这些技术模拟WebSocket交互并提供相同的应用程序级API。spring-doc.cadn.net.cn

在Servlet堆栈上,Spring框架提供了对SockJS协议的服务器(以及客户端)支持。spring-doc.cadn.net.cn

概述

SockJS 的目标是让应用程序使用 WebSocket API,但在运行时必要时可以无缝切换到非 WebSocket 替代方案,而无需更改应用程序代码。spring-doc.cadn.net.cn

SockJS 包括:spring-doc.cadn.net.cn

SockJS 专为浏览器使用而设计。它使用多种技术来支持广泛版本的浏览器。 有关 SockJS 传输类型和浏览器的完整列表,请参见 SockJS 客户端 页面。传输 分为三个一般类别:WebSocket、HTTP 流式传输和 HTTP 长轮询。 有关这些类别的概述,请参见 这篇博客文章spring-doc.cadn.net.cn

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

所有传输请求具有以下URL结构:spring-doc.cadn.net.cn

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

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

HTTP传输需要更多的请求。例如,Ajax/XHR流依赖于一个长连接请求来处理服务器到客户端的消息,并且需要额外的HTTP POST请求来处理客户端到服务器的消息。长轮询与此类似,不同之处在于它在每次服务器到客户端发送消息后结束当前请求。spring-doc.cadn.net.cn

SockJS 添加了最小的消息帧。例如,服务器最初发送字母 o (“打开”帧),消息作为 a["message1","message2"] (JSON编码的数组) 发送,如果25秒内没有消息流动,则发送字母 h (“心跳”帧),并发送字母 c (“关闭”帧) 来关闭会话。spring-doc.cadn.net.cn

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

启用 SockJS

您可以通过Java配置启用SockJS,如下例所示:spring-doc.cadn.net.cn

@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配置等价写法:spring-doc.cadn.net.cn

<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服务环境中相对简单。spring-doc.cadn.net.cn

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

IE 8和9

Internet Explorer 8 和 9 仍在使用。它们是 SockJS 存在的关键原因之一。本节涵盖了在这些浏览器中运行的重要注意事项。spring-doc.cadn.net.cn

SockJS客户端通过使用Microsoft的XDomainRequest在IE 8和9中支持Ajax/XHR流。这可以跨域工作,但不支持发送cookie。
Cookie对于Java应用程序通常至关重要。
但是,由于SockJS客户端可以与多种服务器类型(不仅仅是Java服务器)一起使用,因此需要知道cookie是否重要。
如果重要,SockJS客户端更倾向于使用Ajax/XHR进行流传输。否则,它依赖于基于iframe的技术。spring-doc.cadn.net.cn

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。spring-doc.cadn.net.cn

如果你确实使用了基于iframe的传输,请记住,浏览器可以通过将HTTP响应头X-Frame-Options设置为DENYSAMEORIGINALLOW-FROM <origin>来指示阻止在给定页面上使用IFrames。这是用来防止点击劫持spring-doc.cadn.net.cn

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

查看 默认安全头 的 Spring Security 文档以了解如何配置 X-Frame-Options 头的设置。你还可以查看 gh-2718 以获取更多背景信息。spring-doc.cadn.net.cn

如果你的应用程序添加了X-Frame-Options响应头(正如它应该做的那样!)并且依赖于基于iframe的传输,你需要将头值设置为SAMEORIGINALLOW-FROM <origin>。Spring SockJS支持还需要知道SockJS客户端的位置,因为它是从iframe加载的。默认情况下,iframe被设置为从CDN位置下载SockJS客户端。最好将此选项配置为与应用程序相同的源。spring-doc.cadn.net.cn

以下示例展示了如何在Java配置中实现这一点:spring-doc.cadn.net.cn

@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>元素提供了类似的选择。spring-doc.cadn.net.cn

在初始开发期间,请启用SockJS客户端devel模式,该模式防止浏览器缓存SockJS请求(如iframe),否则这些请求将被缓存。有关如何启用它的详细信息,请参阅SockJS客户端页面。

心跳

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

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

Spring SockJS支持还允许您配置TaskScheduler以调度心跳任务。任务调度器由线程池支持,默认设置基于可用处理器的数量。您应该根据您的具体需求考虑自定义这些设置。spring-doc.cadn.net.cn

客户端断开连接

HTTP流媒体和HTTP长轮询SockJS传输需要连接保持比通常更长的时间。有关这些技术的概述,请参阅这篇博客文章spring-doc.cadn.net.cn

在Servlet容器中,这是通过Servlet 3的异步支持来实现的,该支持允许退出Servlet容器线程,处理请求,并从另一个线程继续向响应写入。spring-doc.cadn.net.cn

一个特定的问题是Servlet API没有提供客户端断开连接的通知。参见eclipse-ee4j/servlet-api#44。 然而,Servlet容器在后续尝试向响应写入时会抛出异常。由于Spring的SockJS服务支持服务器发送的心跳(默认每25秒一次),这意味着通常在该时间段内可以检测到客户端断开连接(如果消息发送更频繁,则可以更早检测到)。spring-doc.cadn.net.cn

因此,可能会因为客户端断开连接而导致网络I/O失败,这可能会在日志中填充不必要的堆栈跟踪。Spring尽最大努力识别表示客户端断开连接的此类网络故障(针对每个服务器)并使用专用的日志类别DISCONNECTED_CLIENT_LOG_CATEGORY(在AbstractSockJsSession中定义)记录最小的消息。如果你需要查看堆栈跟踪,可以将该日志类别设置为TRACE。

SockJS与CORS

如果你允许跨域请求(参见允许的源),SockJS协议在XHR流式传输和轮询传输中使用CORS进行跨域支持。因此,除非检测到响应中存在CORS头,否则会自动添加CORS头。所以,如果一个应用程序已经配置为提供CORS支持(例如,通过Servlet Filter),Spring的SockJsService会跳过这一部分。spring-doc.cadn.net.cn

也可以通过在Spring的SockJsService中设置suppressCors属性来禁用添加这些CORS标头。spring-doc.cadn.net.cn

SockJS 需要以下头信息和值:spring-doc.cadn.net.cn

有关确切的实现,请参见源代码中的 addCorsHeadersAbstractSockJsServiceTransportType 枚举。spring-doc.cadn.net.cn

Alternatively, if the CORS configuration allows it, consider excluding URLs with the SockJS endpoint prefix, thus letting Spring’s SockJsService handle it.spring-doc.cadn.net.cn

SockJsClient

Spring 提供了一个 SockJS Java 客户端,用于在不使用浏览器的情况下连接到远程 SockJS 端点。这在需要通过公共网络进行两个服务器之间的双向通信时特别有用(即,在网络代理可能阻止使用 WebSocket 协议的情况下)。SockJS Java 客户端在测试中也非常有用(例如,模拟大量并发用户)。spring-doc.cadn.net.cn

SockJS Java客户端支持websocketxhr-streamingxhr-polling传输。其余的传输方式仅在浏览器中使用才有意义。spring-doc.cadn.net.cn

您可以使用以下方式配置WebSocketTransportspring-doc.cadn.net.cn

一个 XhrTransport,根据定义,同时支持 xhr-streamingxhr-polling,因为从客户端的角度来看,除了用于连接服务器的URL之外,没有其他区别。目前有两种实现:spring-doc.cadn.net.cn

以下示例展示了如何创建一个SockJS客户端并连接到SockJS端点:spring-doc.cadn.net.cn

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进行配置:spring-doc.cadn.net.cn

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

以下示例展示了服务器端与 SockJS 相关的属性(有关详细信息,请参阅 javadoc), 这些属性你也应该考虑进行自定义:spring-doc.cadn.net.cn

@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)。