1

SockJS Fallback

在公共Internet上,在你控制之外的限制性代理可能会妨碍WebSocket交互,要么是因为它们没有配置为传递Upgrade header,要么是因为它们关闭了看起来空闲的长连接。

这个问题的解决方案是WebSocket模拟 - 也就是说,首先尝试使用WebSocket,然后转而使用基于http的技术来模拟WebSocket交互并公开相同的应用程序级别的API。

在Servlet堆栈上,Spring Framework为SockJS协议提供了服务器(以及客户端)支持。

概述

SockJS的目标是让应用程序使用WebSocket API,但如果在运行时有必要,可以回退到非WebSocket替代方案,而不需要修改应用程序代码。

SockJS包括:

  • SockJS协议以可执行的叙述性测试的形式定义。
  • SockJS JavaScript客户端 — 用于浏览器的客户端库。
  • SockJS服务器实现,包括一个在Spring Framework 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}指示传输类型(例如websocketxhr-streaming和其他)。

WebSocket传输只需要一个HTTP请求来完成WebSocket握手,此后所有消息都在该socket上交换。

HTTP传输需要更多的请求,例如,Ajax/XHR流依赖于一个对服务器到客户端消息的长时间运行的请求,以及对客户端到服务器消息的额外HTTP POST请求。长轮询与此类似,只是它在每个服务器到客户端发送后结束当前请求。

SockJS添加了最少的消息框架,例如,服务器最初发送字母o(“open” frame),消息以["message1","message2"](json编码数组)的形式发送,如果在25秒内(默认情况下)没有消息流,则发送字母h("heartbeat" frame)和字母c("close" frame)来关闭会话。

要了解更多信息,请在浏览器中运行一个示例并观察HTTP请求,SockJS客户端允许修复传输列表,因此可以一次查看每个传输。SockJS客户端还提供了一个debug标志,它在浏览器控制台中启用有用的消息,在服务器端,你可以为org.springframework.web.socket启用TRACE日志记录,有关更多细节,请参阅SockJS协议叙述性测试

启用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
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/websocket
        http://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页面和浏览器支持的传输类型列表。客户端还提供了一些配置选项 - 例如,指定要包含哪些传输。

IE 8和9

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

SockJS客户端使用微软的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客户端应该选择IE8和IE9中的xdr-streaming

如果你确实使用基于iframe的传输,请记住,可以通过设置HTTP响应头X-Frame-OptionsDENYSAMEORIGINALLOW-FROM <origin>来指示浏览器阻止在给定页面上使用IFrames,这是用来防止点击劫持

Spring Security 3.2+支持在每个响应上设置X-Frame-Options,默认情况下,Spring Security Java配置将其设置为DENY,在3.2中,Spring Security XML命名空间默认不设置该header,但可以配置为这样做,将来,它可能会默认设置它。

有关如何配置X-Frame-Options header设置的详细信息,请参阅Spring Security文档的默认Security Headers,你还可以查看SEC-2501了解其他背景信息。

如果你的应用程序添加了X-Frame-Options响应header(它应该这样做!)并依赖于基于iframe的传输,你需要将header值设置为SAMEORIGINALLOW-FROM <origin>。Spring SockJS支持还需要知道SockJS客户端的位置,因为它是从iframe加载的,默认情况下,iframe被设置为从CDN位置下载SockJS客户端,将此选项配置为使用与应用程序相同源的URL是个好主意。

下面的示例展示了如何在Java配置中实现这一点:

@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {

    @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建议

在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协议将使用CORS来支持XHR流和轮询传输中的跨域支持,因此,除非检测到响应中存在CORS headers,否则将自动添加CORS headers,因此,如果应用程序已经配置为提供CORS支持(例如,通过Servlet过滤器),Spring的SockJsService就跳过了这一部分。

还可以通过在Spring的SockJsService中设置suppressCors属性来禁用这些CORS headers的添加。

SockJS期望以下headers和值:

  • Access-Control-Allow-Origin:从Origin请求header的值初始化。
  • Access-Control-Allow-Credentials:总是设为true
  • Access-Control-Request-Headers:从等效请求header的值初始化。
  • Access-Control-Allow-Methods:传输支持的HTTP方法(参见TransportType枚举)。
  • Access-Control-Max-Age:设置为31536000(1年)。

有关确切的实现,请参阅源代码中AbstractSockJsServiceTransportType枚举中的addCorsHeaders

或者,如果CORS配置允许,考虑使用SockJS端点前缀排除URL,从而让Spring的SockJsService处理它。

SockJsClient

Spring提供了一个SockJS Java客户端来连接到远程SockJS端点,而无需使用浏览器,当需要通过公共网络在两台服务器之间进行双向通信时(也就是说,网络代理可能会阻止WebSocket协议的使用),这一点尤其有用。对于测试目的(例如,模拟大量并发用户),SockJS Java客户端也非常有用。

SockJS Java客户端支持websocketxhr-streamingxhr-polling传输,剩下的一个只有在浏览器中使用才有意义。

你可以配置WebSocketTransport使用:

  • 在JSR-356运行时中的StandardWebSocketClient
  • 使用Jetty 9+原生WebSocket API的JettyWebSocketClient
  • Spring的WebSocketClient的任何实现。

根据定义,XhrTransport同时支持xhr-streamingxhr-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); 
    }

    // ...
}
  • streamBytesLimit属性设置为512KB(默认为128KB => 128 * 1024)。
  • httpMessageCacheSize属性设置为1000(默认为100)。
  • disconnectDelay属性设置为30秒(默认为5秒 => 5 * 1000)。

上一篇:WebSocket API
下一篇:STOMP

博弈
2.5k 声望1.5k 粉丝

态度决定一切