SockJS Fallback
在公共网络上,不受您控制的限制性代理可能会阻止 WebSocket 交互,原因可能是它们未配置为传递“升级”标头,或因为它们关闭了看似空闲的长寿命连接。 解决此问题的方法是 WebSocket 模拟,即尝试先使用 WebSocket,然后采用基于 HTTP 的技术来模拟 WebSocket 交互并公开相同的应用程序级 API。 在 Servlet 堆栈上,Spring Framework 为 SockJS 协议提供服务器(以及客户端)支持。
Overview
SockJS 的目标是让应用程序能够在运行时使用 WebSocket API,但在必要时自动退回到非 WebSocket 备用项,而无需更改应用程序代码。
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。
如果您的应用程序添加了 X-Frame-Options
响应头(它应该添加该标头!)并依赖于基于 iframe 的传输,则需要将标头值设置为 SAMEORIGIN
或 ALLOW-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 客户端 |
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 尽最大努力识别表示客户端断开的此类网络故障(特定于每台服务器),并使用专用日志类别记录一则最小化消息, |
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
中的 addCorsHeaders
和 TransportType
枚举。
或者,如果 CORS 配置允许,可以考虑排除带有 SockJS 端点的 URL 前缀,从而让 Spring 的 SockJsService
处理它。
SockJsClient
Spring 提供了一个 SockJS Java 客户端,无需使用浏览器即可连接到远程 SockJS 端点。当两个服务器需要通过公共网络进行双向通信(即网络代理可能禁止使用 WebSocket 协议)时,这可能尤其有用。SockJS Java 客户端对于测试目的也十分有用(例如,为了模拟大量并发用户)。
SockJS Java 客户端支持 websocket
、xhr-streaming
和 xhr-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,它需要位于类路径中。或者,您可以配置自定义实现 |
要使用 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 )。 |