> For the complete documentation index, see [llms.txt](https://docs.flydean.com/spring-framework-documentation5/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.flydean.com/spring-framework-documentation5/webservlet/4.websockets/4.3sockjs-fallback.md).

# 4.3 SockJS Fallback

在公共Internet上，控件外的限制性代理可能会阻止WebSocket交互，这可能是因为它们未配置为传递Upgrade标头，或者是因为它们关闭了长期处于空闲状态的连接。

解决此问题的方法是WebSocket仿真，即先尝试使用WebSocket，然后尝试使用基于HTTP的技术来模拟WebSocket交互并公开相同的应用程序级API。

在Servlet堆栈上，Spring框架为SockJS协议提供服务器（和客户端）支持。

## 4.3.1 Overview

SockJS的目标是让应用程序使用WebSocket API，但在运行时必要时使用非WebSocket替代方案，而无需更改应用程序代码。

SockJS包括：

* 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结构：

`http://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（“heartbeat”帧）发送持续25秒（默认情况下），然后使用字母c（“close”框）关闭会话。

要了解更多信息，请在浏览器中运行示例并查看HTTP请求。 SockJS客户端允许修复传输列表，因此可以一次查看每个传输。 SockJS客户端还提供了调试标志，该标志可在浏览器控制台中启用有用的消息。在服务器端，您可以为org.springframework.web.socket启用TRACE日志记录。有关更多详细信息，请参见SockJS协议旁白测试。

## 4.3.2 Enabling SockJS

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

```java
@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配置：

```markup
<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页面和浏览器支持的传输类型列表。 客户端还提供了几个配置选项，例如用于指定要包括的传输。

## 4.3.3 IE 8 and 9

Internet Explorer 8和9仍在使用。它们是拥有SockJS的关键原因。本节涵盖有关在这些浏览器中运行的重要注意事项。

SockJS客户端通过使用Microsoft的XDomainRequest在IE 8和9中支持Ajax / XHR流。跨域有效，但不支持发送Cookie。 Cookies对于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的支持。 默认情况下，Spring Security Java配置将其设置为DENY。 在3.2中，Spring Security XML名称空间默认情况下不设置该标头，但可以配置为这样做。 将来，它可能会默认设置。
>
> 有关如何配置X-Frame-Options标头的设置的详细信息，请参见Spring Security文档的Default Security Headers。 您也可以查看SEC-2501以获取更多背景信息。

如果您的应用程序添加了X-Frame-Options响应标头（应如此！）并依赖于基于iframe的传输，则需要将标头值设置为SAMEORIGIN或ALLOW-FROM \<origin>。 Spring SockJS支持还需要知道SockJS客户端的位置，因为它是从iframe加载的。 默认情况下，iframe设置为从CDN位置下载SockJS客户端。 最好将此选项配置为使用与应用程序源相同的URL。

以下示例显示了如何在Java配置中执行此操作：

```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客户端开发模式，以防止浏览器缓存本应缓存的SockJS请求（如iframe）。 有关如何启用它的详细信息，请参见SockJS客户端页面。

## 4.3.4 Heartbeats

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

> 在WebSocket和SockJS上使用STOMP时，如果STOMP客户端和服务器协商要交换的心跳，则会禁用SockJS心跳。

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

## 4.3.5 Client Disconnects

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。

## 4.3.6 SockJS and CORS

如果允许跨域请求（请参阅“允许的起源”），则SockJS协议将CORS用于XHR流和轮询传输中的跨域支持。因此，除非在响应中检测到CORS标头的存在，否则将自动添加CORS标头。因此，如果已经将应用程序配置为提供CORS支持（例如，通过Servlet过滤器），则Spring的SockJsService会跳过这一部分。

也可以通过在Spring的SockJsService中设置preventCors属性来禁用添加这些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处理它。

## 4.3.7 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的任何实现。

根据定义，XhrTransport支持xhr-streaming和xhr-polling，因为从客户端的角度来看，除了用于连接服务器的URL之外没有其他区别。当前有两种实现：

* RestTemplateXhrTransport使用Spring的RestTemplate进行HTTP请求。
* JettyXhrTransport使用Jetty的HttpClient进行HTTP请求。

以下示例显示了如何创建SockJS客户端并连接到SockJS端点：

```java
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进行操作：

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

以下示例显示了与服务器端SockJS相关的属性（有关详细信息，请参见javadoc），您还应考虑自定义：

```java
@Configuration
public class WebSocketConfig extends WebSocketMessageBrokerConfigurationSupport {

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/sockjs").withSockJS()
            .setStreamBytesLimit(512 * 1024) 
            .setHttpMessageCacheSize(1000) 
            .setDisconnectDelay(30 * 1000); 
    }

    // ...
}
```
