SockJS 备用方案
通过公共互联网,您无法控制的限制性代理可能会阻止 WebSocket 交互,因为它们要么未配置为传递Upgrade
标头,要么因为它们关闭了看起来处于空闲状态的长连接。
此问题的解决方案是 WebSocket 模拟,即首先尝试使用 WebSocket,然后回退到模拟 WebSocket 交互并公开相同应用程序级 API 的基于 HTTP 的技术。
在 Servlet 堆栈上,Spring 框架同时提供服务器(以及客户端)对 SockJS 协议的支持。
概述
SockJS 的目标是让应用程序使用 WebSocket API,但在运行时根据需要回退到非 WebSocket 替代方案,而无需更改应用程序代码。
SockJS 由以下部分组成:
-
用于浏览器的客户端库 SockJS JavaScript 客户端。
-
SockJS 服务器实现,包括 Spring 框架
spring-websocket
模块中的一个。 -
spring-websocket
模块中的 SockJS Java 客户端(自版本 4.1 起)。
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}
指示传输类型(例如,websocket
、xhr-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
您可以通过配置启用 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。借助SockJsHttpRequestHandler
,将其集成到其他 HTTP 服务环境中相对简单。
在浏览器端,应用程序可以使用sockjs-client
(版本 1.0.x)。它模拟 W3C WebSocket API 并与服务器通信以选择最佳传输选项,具体取决于其运行的浏览器。请参阅sockjs-client页面和浏览器支持的传输类型列表。客户端还提供了一些配置选项,例如指定要包含哪些传输。
IE 8 和 9
Internet Explorer 8 和 9 仍在使用。它们是拥有 SockJS 的主要原因。本节涵盖了在这些浏览器中运行的重要注意事项。
SockJS 客户端通过使用 Microsoft 的XDomainRequest
在 IE 8 和 9 中支持 Ajax/XHR 流式传输。这跨域有效,但不支持发送 Cookie。Cookie 通常对于 Java 应用程序至关重要。但是,由于 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。这用于防止点击劫持。
Spring Security 3.2+ 提供了对在每个响应上设置 |
如果您的应用程序添加了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("https://127.0.0.1:8080/myapp/js/sockjs-client.js");
}
// ...
}
XML 命名空间通过<websocket:sockjs>
元素提供了类似的选项。
在初始开发期间,请启用 SockJS 客户端的devel 模式,该模式可以防止浏览器缓存 SockJS 请求(如 iframe),否则这些请求会被缓存。有关如何启用它的详细信息,请参阅SockJS 客户端页面。 |
心跳
SockJS 协议要求服务器发送心跳消息,以防止代理认为连接已挂起。Spring SockJS 配置有一个名为heartbeatTime
的属性,您可以使用它来自定义频率。默认情况下,在 25 秒后发送心跳,假设在此连接上没有发送其他消息。这个 25 秒的值符合以下针对公共互联网应用程序的IETF 建议。
当通过 WebSocket 和 SockJS 使用 STOMP 时,如果 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
中的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 运行时中使用
StandardWebSocketClient
。 -
通过使用 Jetty 9+ 本地 WebSocket API 使用
JettyWebSocketClient
。 -
Spring 的
WebSocketClient
的任何实现。
从客户端的角度来看,除了用于连接到服务器的 URL 之外,XhrTransport
根据定义支持xhr-streaming
和xhr-polling
,因为没有其他区别。目前有两个实现
-
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 秒 — 5 * 1000 )。 |