此版本仍在开发中,尚不被认为是稳定的。对于最新的稳定版本,请使用 Spring Framework 6.2.10! |
SockJS 回退
在公共互联网上,超出您控制范围的限制性代理可能会阻止 WebSocket
交互,因为它们未配置为传递Upgrade
header 或
因为它们关闭了看似空闲的长期连接。
这个问题的解决方案是 WebSocket 仿真——即尝试使用 WebSocket 首先,然后回退到模拟 WebSocket 的基于 HTTP 的技术 交互并公开相同的应用程序级 API。
在 Servlet 堆栈上,Spring Framework 提供服务器(和客户端)支持 对于 SockJS 协议。
概述
SockJS 的目标是让应用程序使用 WebSocket API,但回退到 在运行时必要时使用非 WebSocket 替代方案,而无需 更改应用程序代码。
SockJS 包括:
-
SockJS JavaScript 客户端 — 用于浏览器的客户端库。
-
SockJS 服务器实现,包括 Spring Framework 中的一个
spring-websocket
模块。 -
SockJS Java 客户端中的
spring-websocket
模块(从 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
(“open”帧)最初,消息以a["message1","message2"]
(JSON 编码数组)、字母h
(“检测信号”帧),如果没有消息流
25 秒(默认情况下),字母c
(“关闭”框架)以关闭会话。
若要了解详细信息,请在浏览器中运行示例并观察 HTTP 请求。
SockJS 客户端允许修复传输列表,因此可以
一次查看每个运输。SockJS 客户端还提供了一个调试标志
这在浏览器控制台中启用有用的消息。在服务器端,您可以启用TRACE
日志记录org.springframework.web.socket
.
有关更多详细信息,请参阅 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
.
在浏览器端,应用程序可以使用sockjs-client
(版本 1.0.x)。它
模拟 W3C WebSocket API 并与服务器通信以选择最佳
transport 选项,具体取决于运行它的浏览器。请参阅 sockjs-client 页面和
浏览器支持的传输类型。客户端还提供了几个
配置选项 - 例如,指定要包含的传输。
IE 8 和 9
Internet Explorer 8 和 9 仍在使用中。他们是 拥有 SockJS 的关键原因。本节介绍重要的 有关在这些浏览器中运行的注意事项。
SockJS 客户端通过使用 Microsoft 的XDomainRequest
.
这适用于跨域,但不支持发送 cookie。
Cookie 对于 Java 应用程序通常是必不可少的。
但是,由于 SockJS 客户端可以与许多服务器一起使用
类型(不仅仅是 Java 类型),它需要知道 cookie 是否重要。
如果是这样,SockJS 客户端更喜欢 Ajax/XHR 进行流式传输。否则,它
依赖于基于 iframe 的技术。
第一个/info
来自 SockJS 客户端的请求是
可能影响客户选择交通工具的信息。
其中一个细节是服务器应用程序是否依赖于 cookie
(例如,用于身份验证目的或使用粘性会话进行集群)。
Spring 的 SockJS 支持包括一个名为sessionCookieNeeded
.
默认情况下,它是启用的,因为大多数 Java 应用程序依赖于JSESSIONID
饼干。如果您的应用程序不需要它,您可以关闭此选项,
然后,SockJS 客户端应该选择xdr-streaming
在 IE 8 和 9 中。
如果您确实使用基于 iframe 的传输,请记住
可以通过以下方式指示浏览器阻止在给定页面上使用 IFrames
设置 HTTP 响应标头X-Frame-Options
自DENY
,SAMEORIGIN
或ALLOW-FROM <origin>
.这用于防止点击劫持。
Spring Security 3.2+ 支持将 |
如果您的应用程序将X-Frame-Options
响应标头(理所当然!
并且依赖于基于 iframe 的传输,您需要将标头值设置为SAMEORIGIN
或ALLOW-FROM <origin>
. Spring SockJS支持还需要知道 SockJS 客户端的位置,因为它是加载的从 iframe。默认情况下,iframe 设置为下载 SockJS 客户端从 CDN 位置。最好将此选项配置为使用与应用程序来自同一来源的 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 针对公共 Internet 应用程序的建议。
在 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 ).如果您需要查看堆栈跟踪,您可以将
log 类别添加到 TRACE。 |
SockJS 和 CORS
如果您允许跨源请求(请参阅允许的源),则 SockJS
协议使用 CORS 在 XHR 流和轮询传输中提供跨域支持。
因此,会自动添加 CORS 标头,除非
检测到响应。因此,如果应用程序已配置为提供 CORS 支持
(例如,通过 Servlet 过滤器),Spring 的SockJsService
跳过这部分。
也可以通过设置suppressCors
Spring 的 SockJsService 中的属性。
SockJS 需要以下标头和值:
-
Access-Control-Allow-Origin
:从Origin
request 标头。 -
Access-Control-Allow-Credentials
:始终设置为true
. -
Access-Control-Request-Headers
:从等效请求标头中的值初始化。 -
Access-Control-Allow-Methods
:传输支持的 HTTP 方法(请参阅TransportType
枚举)。 -
Access-Control-Max-Age
:设置为 31536000(1 年)。
有关确切的实现,请参阅addCorsHeaders
在AbstractSockJsService
和
这TransportType
enum 的 enum 中。
或者,如果 CORS 配置允许,请考虑排除带有
SockJS 端点前缀,从而让 Spring 的SockJsService
处理它。
SockJsClient
Spring 提供了一个 SockJS Java 客户端,无需连接到远程 SockJS 端点 使用浏览器。当需要双向时,这尤其有用 两个服务器之间通过公共网络进行通信(即网络代理可以 排除使用 WebSocket 协议)。SockJS Java 客户端也非常有用 用于测试目的(例如,模拟大量并发用户)。
SockJS Java 客户端支持websocket
,xhr-streaming
和xhr-polling
运输。其余的仅在浏览器中使用才有意义。
您可以配置WebSocketTransport
跟:
-
StandardWebSocketClient
在 JSR-356 运行时中。 -
JettyWebSocketClient
通过使用 Jetty 9+ 原生 WebSocket API。 -
Spring 的任何实现
WebSocketClient
.
一XhrTransport
,根据定义,两者都支持xhr-streaming
和xhr-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 秒 —5 * 1000 ). |