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,如以下示例所示:

@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配置:

<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配置中执行此操作:

@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端点:

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) 
            .setHttpMessageCacheSize(1000) 
            .setDisconnectDelay(30 * 1000); 
    }

    // ...
}

最后更新于