4. WebSockets

4. WebSockets

4.1. WebSocket 介绍

WebSocket 协议RFC 6455提供了一种标准化方法,可以通过单个 TCP 连接在 Client 端和服务器之间构建全双工双向通信通道。它是与 HTTP 不同的 TCP 协议,但旨在通过端口 80 和 443 在 HTTP 上工作,并允许重复使用现有的防火墙规则。

WebSocket 交互始于一个 HTTP 请求,该请求使用 HTTP UpgradeHeaders 进行升级,或者在这种情况下切换到 WebSocket 协议。以下示例显示了这种交互:

GET /spring-websocket-portfolio/portfolio HTTP/1.1
Host: localhost:8080
Upgrade: websocket (1)
Connection: Upgrade (2)
Sec-WebSocket-Key: Uc9l9TMkWGbHFD2qnFHltg==
Sec-WebSocket-Protocol: v10.stomp, v11.stomp
Sec-WebSocket-Version: 13
Origin: http://localhost:8080
  • (1) UpgradeHeaders。
  • (2) 使用Upgrade连接。

 具有 WebSocket 支持的服务器代替通常的 200 状态代码,返回类似于以下内容的输出: 

HTTP/1.1 101 Switching Protocols (1)
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: 1qVdfYHU9hPOl4JYYNXF623Gzn0=
Sec-WebSocket-Protocol: v10.stomp
  • (1) 协议切换

成功握手后,HTTP 升级请求的基础 TCP 套接字将保持打开状态,Client 端和服务器均可 continue 发送和接收消息。

WebSockets 的工作原理的完整介绍超出了本文档的范围。请参阅 RFC 6455,HTML5 的 WebSocket 章节或 Web 上的许多简介和教程中的任何一个。

请注意,如果 WebSocket 服务器在 Web 服务器(例如 nginx)后面运行,则可能需要对其进行配置,以将 WebSocket 升级请求传递到 WebSocket 服务器。同样,如果应用程序在云环境中运行,请检查与 WebSocket 支持相关的云提供商的说明。

  

4.1.1. HTTP 与 WebSocket

 尽管 WebSocket 设计为与 HTTP 兼容并以 HTTP 请求开头,但重要的是要了解这两个协议导致了截然不同的体系结构和应用程序编程模型。

在 HTTP 和 REST 中,应用程序被建模为许多 URL。为了与应用程序交互,Client 端访问那些 URL,即请求-响应样式。服务器根据 HTTP URL,方法和 Headers 将请求路由到适当的处理程序。

相比之下,在 WebSockets 中,初始连接通常只有一个 URL。随后,所有应用程序消息在同一 TCP 连接上流动。这指向了完全不同的异步,事件驱动的消息传递体系结构。

WebSocket 也是一种低级传输协议,与 HTTP 不同,它不对消息的内容规定任何语义。这意味着除非 Client 端和服务器就消息语义达成一致,否则就无法路由或处理消息。

WebSocketClient 端和服务器可以通过 HTTP 握手请求上的Sec-WebSocket-ProtocolHeaders 协商使用更高级别的消息传递协议(例如 STOMP)。在这种情况下,他们需要提出自己的约定。

 4.1.2. 何时使用 WebSockets

 WebSockets 可以使网页具有动态性和交互性。但是,在许多情况下,结合使用 Ajax 和 HTTP 流或长时间轮询可以提供一种简单有效的解决方案。

例如,新闻,邮件和社交订阅源需要动态更新,但是每隔几分钟这样做是完全可以的。另一方面,协作,游戏和金融应用程序需要更接近实时。

仅延迟并不是决定因素。如果消息量相对较少(例如,监视网络故障),则 HTTP 流或轮询可以提供有效的解决方案。低延迟,高频率和高音量的结合才是使用 WebSocket 的最佳案例。

还请记住,在 Internet 上,控件之外的限制性代理可能会阻止 WebSocket 交互,这可能是因为未将它们配置为传递UpgradeHeaders,或者是因为它们关闭了长期处于空闲状态的连接。这意味着与面向公众的应用程序相比,将 WebSocket 用于防火墙内部的应用程序是一个更直接的决定。

4.2. WebSocket API

 Spring 框架提供了一个 WebSocket API,可用于编写处理 WebSocket 消息的 Client 端和服务器端应用程序。

 

4.2.1. WebSocketHandler

创建 WebSocket 服务器就像实现WebSocketHandler一样简单,或者更可能地扩展TextWebSocketHandlerBinaryWebSocketHandler。以下示例使用TextWebSocketHandler: 

 

import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.TextMessage;

public class MyHandler extends TextWebSocketHandler {

    @Override
    public void handleTextMessage(WebSocketSession session, TextMessage message) {
        // ...
    }

}

有专用的 WebSocket Java 配置和 XML 名称空间支持,用于将前面的 WebSocket 处理程序映射到特定的 URL,如以下示例所示:  

import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;

@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {

    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry.addHandler(myHandler(), "/myHandler");
    }

    @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:handlers>

    <bean id="myHandler" class="org.springframework.samples.MyHandler"/>

</beans>

以下示例供 Spring MVC 应用程序使用,并且应包含在DispatcherServlet的配置中。但是,Spring 的 WebSocket 支持不依赖于 Spring MVC。在WebSocketHttpRequestHandler的帮助下将WebSocketHandler集成到其他 HTTP 服务环境中相对简单。  

4.2.2. WebSocket 握手

 定制初始 HTTP WebSocket 握手请求的最简单方法是通过HandshakeInterceptor,它公开了在“之前”和“之后”握手的方法。您可以使用此类拦截器来排除握手或使任何属性对WebSocketSession可用。下面的示例使用内置的拦截器将 HTTP 会话属性传递到 WebSocket 会话:

@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {

    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry.addHandler(new MyHandler(), "/myHandler")
            .addInterceptors(new HttpSessionHandshakeInterceptor());
    }

}

下面的示例显示与前面的示例等效的 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:handshake-interceptors>
            <bean class="org.springframework.web.socket.server.support.HttpSessionHandshakeInterceptor"/>
        </websocket:handshake-interceptors>
    </websocket:handlers>

    <bean id="myHandler" class="org.springframework.samples.MyHandler"/>

</beans>

一个更高级的选项是扩展执行 WebSocket 握手步骤的DefaultHandshakeHandler,包括验证 Client 端来源,协商子协议以及其他详细信息。如果应用程序需要配置自定义RequestUpgradeStrategy以适应尚不支持的 WebSocket 服务器引擎和版本,则它可能还需要使用此选项(有关此主题的更多信息,请参见Deployment)。 Java 配置和 XML 名称空间均可配置自定义HandshakeHandler

Tip

Spring 提供了一个WebSocketHandlerDecoratorBase Class,您可以使用该 Base Class 来装饰WebSocketHandler并具有其他行为。使用 WebSocket Java 配置或 XML 名称空间时,默认情况下会提供并添加日志记录和异常处理实现。 ExceptionWebSocketHandlerDecorator捕获由任何WebSocketHandler方法引起的所有未捕获的异常,并关闭状态为1011的 WebSocket 会话,这表明服务器错误。

4.2.3. 部署

易于将 Spring WebSocket API 集成到 Spring MVC 应用程序中,其中DispatcherServlet服务于 HTTP WebSocket 握手和其他 HTTP 请求。通过调用WebSocketHttpRequestHandler,也很容易将其集成到其他 HTTP 处理方案中。这是方便且易于理解的。但是,对于 JSR-356 运行时,需要特别注意。  

 Java WebSocket API(JSR-356)提供了两种部署机制。第一个涉及启动时的 Servlet 容器 Classpath 扫描(Servlet 3 功能)。另一个是在 Servlet 容器初始化时使用的注册 API。这两种机制都无法对所有 HTTP 处理(包括 WebSocket 握手和所有其他 HTTP 请求)(例如 Spring MVC 的DispatcherServlet)使用单个“前端控制器”。

这是 JSR-356 的一个重大限制,即使在 JSR-356 运行时中运行,Spring 的 WebSocket 支持使用特定于服务器的RequestUpgradeStrategy实现解决。 Tomcat,Jetty,GlassFish,WebLogic,WebSphere 和 Undertow(和 WildFly)目前存在此类策略。 

 

Note

已经创建了克服 Java WebSocket API 中的上述限制的请求,可以在eclipse-ee4j/websocket-api#211处进行跟踪。 Tomcat,Undertow 和 WebSphere 提供了自己的 API 替代方案,使之可以做到这一点,而 Jetty 也可以实现。我们希望更多的服务器可以做到这一点。

另一个要考虑的因素是,希望支持 JSR-356 的 Servlet 容器执行ServletContainerInitializer(SCI)扫描,这可能会减慢应用程序的启动速度,在某些情况下会大大降低。如果在升级到支持 JSR-356 的 Servlet 容器版本后观察到重大影响,则应该可以通过使用web.xml中的<absolute-ordering />元素选择性地启用或禁用 Web 片段(和 SCI 扫描),如以下示例所示显示:

<web-app xmlns="http://java.sun.com/xml/ns/javaee"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="
        http://java.sun.com/xml/ns/javaee
        http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
    version="3.0">

    <absolute-ordering/>

</web-app>

然后,您可以按名称有选择地启用 Web 片段,例如 Spring 自己的SpringServletContainerInitializer,该片段提供对 Servlet 3 Java 初始化 API 的支持。以下示例显示了如何执行此操作:  

<web-app xmlns="http://java.sun.com/xml/ns/javaee"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="
        http://java.sun.com/xml/ns/javaee
        http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
    version="3.0">

    <absolute-ordering>
        <name>spring_web</name>
    </absolute-ordering>

</web-app>

4.2.4. 服务器配置

每个基础的 WebSocket 引擎都公开控制运行时 Feature 的配置属性,例如消息缓冲区大小的大小,空闲超时等。

对于 Tomcat,WildFly 和 GlassFish,可以将ServletServerContainerFactoryBean添加到 WebSocket Java 配置中,如以下示例所示:

@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {

    @Bean
    public ServletServerContainerFactoryBean createWebSocketContainer() {
        ServletServerContainerFactoryBean container = new ServletServerContainerFactoryBean();
        container.setMaxTextMessageBufferSize(8192);
        container.setMaxBinaryMessageBufferSize(8192);
        return container;
    }

}

下面的示例显示与前面的示例等效的 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">

    <bean class="org.springframework...ServletServerContainerFactoryBean">
        <property name="maxTextMessageBufferSize" value="8192"/>
        <property name="maxBinaryMessageBufferSize" value="8192"/>
    </bean>

</beans>

Note

对于 Client 端 WebSocket 配置,应使用WebSocketContainerFactoryBean(XML)或ContainerProvider.getWebSocketContainer()(Java 配置)。

对于 Jetty,您需要提供一个预先配置的 Jetty WebSocketServerFactory,然后通过 WebSocket Java 配置将其插入 Spring 的DefaultHandshakeHandler。以下示例显示了如何执行此操作:

@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {

    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry.addHandler(echoWebSocketHandler(),"/echo").setHandshakeHandler(handshakeHandler());
    }

    @Bean
    public DefaultHandshakeHandler handshakeHandler() {

        WebSocketPolicy policy = new WebSocketPolicy(WebSocketBehavior.SERVER);
        policy.setInputBufferSize(8192);
        policy.setIdleTimeout(600000);

        return new DefaultHandshakeHandler(
                new JettyRequestUpgradeStrategy(new WebSocketServerFactory(policy)));
    }

}

下面的示例显示与前面的示例等效的 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="/echo" handler="echoHandler"/>
        <websocket:handshake-handler ref="handshakeHandler"/>
    </websocket:handlers>

    <bean id="handshakeHandler" class="org.springframework...DefaultHandshakeHandler">
        <constructor-arg ref="upgradeStrategy"/>
    </bean>

    <bean id="upgradeStrategy" class="org.springframework...JettyRequestUpgradeStrategy">
        <constructor-arg ref="serverFactory"/>
    </bean>

    <bean id="serverFactory" class="org.eclipse.jetty...WebSocketServerFactory">
        <constructor-arg>
            <bean class="org.eclipse.jetty...WebSocketPolicy">
                <constructor-arg value="SERVER"/>
                <property name="inputBufferSize" value="8092"/>
                <property name="idleTimeout" value="600000"/>
            </bean>
        </constructor-arg>
    </bean>

</beans>

4.2.5. 允许的来源

从 Spring Framework 4.1.5 开始,WebSocket 和 SockJS 的默认行为是仅接受同源请求。也可以允许所有或指定的来源列表。此检查主要用于浏览器 Client 端。没有任何措施可以阻止其他类型的 Client 端修改OriginHeaders 值(有关更多详细信息,请参见RFC 6454:Web 起源概念)。  

三种可能的行为是:

  • 仅允许同源请求(默认):在此模式下,启用 SockJS 时,Iframe HTTP 响应 HeadersX-Frame-Options设置为SAMEORIGIN,并且 JSONP 传输被禁用,因为它不允许检查请求的来源。因此,启用此模式时,不支持 IE6 和 IE7.

  • 允许指定来源列表:每个允许的来源必须以http://https://开头。在此模式下,启用 SockJS 时,将禁用 IFrame 传输。因此,启用此模式时,不支持 IE6 至 IE9.

  • 允许所有来源:要启用此模式,您应提供*作为允许的来源值。在这种模式下,所有传输都可用。

您可以配置 WebSocket 和 SockJS 允许的来源,如以下示例所示:  

import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;

@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {

    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry.addHandler(myHandler(), "/myHandler").setAllowedOrigins("http://mydomain.com");
    }

    @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 allowed-origins="http://mydomain.com">
        <websocket:mapping path="/myHandler" handler="myHandler" />
    </websocket:handlers>

    <bean id="myHandler" class="org.springframework.samples.MyHandler"/>

</beans>

4.3. SockJS 后备

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

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

在 Servlet 堆栈上,Spring 框架为 SockJS 协议提供服务器(以及 Client 端)支持。

4.3.1. Overview

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

SockJS 包括:

  • 以可执行文件narrated tests的形式定义的SockJS protocol

  • SockJS JavaScriptClient 端Client 端库,供浏览器使用。

  • SockJS 服务器实现,包括 Spring Framework spring-websocket模块中的一个。

  • spring-websocket模块中的 SockJS JavaClient 端(从 4.1 版开始)。

SockJSClient 端首先发送GET /info以从服务器获取基本信息。在那之后,它必须决定使用哪种交通工具。如果可能,请使用 WebSocket。如果没有,在大多数浏览器中,至少有一个 HTTP 流选项。如果不是,则使用 HTTP(长)轮询。

所有传输请求都具有以下 URL 结构:

  http://host:port/myApp/myEndpoint/{server-id}/{session-id}/{transport}

where:

  • {server-id}对于在集群中路由请求很有用,但否则不使用。

  • {session-id}关联属于 SockJS 会话的 HTTP 请求。

  • {transport}表示传输类型(例如websocketxhr-streaming等)。

WebSocket 传输仅需要单个 HTTP 请求即可进行 WebSocket 握手。此后所有消息在该套接字上交换。

HTTP 传输需要更多请求。例如,Ajax/XHR 流依赖于对服务器到 Client 端消息的一个长时间运行的请求,以及对 Client 端到服务器消息的其他 HTTP POST 请求。长轮询与长轮询类似,不同之处在于长轮询在每次服务器到 Client 端发送后结束当前请求。

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

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

 

4.3.2. 启用 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页和浏览器支持的传输类型列表。Client 端还提供了几个配置选项,例如用于指定要包括的传输。

4.3.3. IE 8 和 9

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

SockJSClient 端通过使用 Microsoft 的XDomainRequest在 IE 8 和 9 中支持 Ajax/XHR 流。这适用于所有域,但不支持发送 Cookie。 Cookies 对于 Java 应用程序通常是必不可少的。但是,由于 SockJSClient 端可用于多种服务器类型(不仅是 Java 服务器类型),因此需要知道 cookie 是否重要。如果是这样,则 SockJSClient 端更喜欢 Ajax/XHR 进行流传输。否则,它依赖于基于 iframe 的技术。

SockJSClient 端发出的第一个/info请求是对可能影响 Client 端选择传输方式的信息的请求。这些详细信息之一是服务器应用程序是否依赖 Cookie(例如,出于身份验证目的或具有粘性会话的群集)。 Spring 的 SockJS 支持包括名为sessionCookieNeeded的属性。由于大多数 Java 应用程序都依赖JSESSIONID cookie,因此默认情况下启用该功能。如果您的应用程序不需要它,则可以关闭此选项,然后 SockJSClient 端应在 IE 8 和 9 中选择xdr-streaming

如果您确实使用基于 iframe 的传输,请记住,可以通过将 HTTP 响应 HeadersX-Frame-Options设置为DENYSAMEORIGINALLOW-FROM <origin>来指示浏览器阻止在给定页面上使用 iframe。这用于防止clickjacking

Note

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

有关如何配置X-Frame-OptionsHeaders 设置的详细信息,请参见 Spring Security 文档的默认安全标题。您还可以看到SEC-2501以获得其他背景。

如果您的应用程序添加了X-Frame-Options响应 Headers(应如此!)并依赖于基于 iframe 的传输,则需要将 Headers 值设置为SAMEORIGINALLOW-FROM <origin>。 Spring SockJS 支持还需要知道 SockJSClient 端的位置,因为它是从 iframe 加载的。默认情况下,iframe 设置为从 CDN 位置下载 SockJSClient 端。最好将此选项配置为使用与应用程序源相同的 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>元素提供了类似的选项。

Note

在最初的开发过程中,请启用 SockJSClient 端devel模式,以防止浏览器缓存本应缓存的 SockJS 请求(如 iframe)。有关如何启用它的详细信息,请参见SockJS client页。

  

4.3.4. Heartbeats

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

Note

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

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

4.3.5. Client 端断开连接

HTTP 流和 HTTP 长轮询 SockJS 传输要求连接保持打开的时间比平常更长。有关这些技术的概述,请参见此博客文章

在 Servlet 容器中,这是通过 Servlet 3 异步支持完成的,该支持允许退出 Servlet 容器线程,处理请求并 continue 写入另一个线程的响应。

一个特定的问题是,Servlet API 不会为已离开的 Client 端提供通知。参见eclipse-ee4j/servlet-api#44。但是,Servlet 容器在随后尝试写入响应时会引发异常。由于 Spring 的 SockJS 服务支持服务器发送的心跳(默认情况下每 25 秒发送一次),这意味着通常会在该时间段内(或更早,如果消息发送频率更高)检测到 Client 端断开连接。

Note

结果,由于 Client 端已断开连接,可能会发生网络 I/O 故障,这可能会在日志中填充不必要的堆栈跟踪。 Spring 会尽最大努力找出代表 Client 端断开连接(特定于每个服务器)的此类网络故障,并使用专用日志类别DISCONNECTED_CLIENT_LOG_CATEGORY(在AbstractSockJsSession中定义)来记录最少的消息。如果需要查看堆栈跟踪,可以将该日志类别设置为 TRACE。

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

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

SockJS 需要以下 Headers 和值:

  • Access-Control-Allow-Origin:从Origin请求 Headers 的值初始化。

  • Access-Control-Allow-Credentials:始终设置为true

  • Access-Control-Request-Headers:从等效请求 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 JavaClient 端,无需使用浏览器即可连接到远程 SockJS 端点。当需要通过公共网络在两个服务器之间进行双向通信时(这是网络代理可以阻止使用 WebSocket 协议的地方),这特别有用。 SockJS JavaClient 端对于测试也非常有用(例如,模拟大量并发用户)。

SockJS JavaClient 端支持websocketxhr-streamingxhr-polling传输。其余的仅在浏览器中使用才有意义。

您可以使用以下方式配置WebSocketTransport

  • 在 JSR-356 运行时中为StandardWebSocketClient

  • JettyWebSocketClient通过使用 Jetty 9 本机 WebSocket API。

  • Spring 的WebSocketClient的任何实现。

顾名思义,XhrTransport支持xhr-streamingxhr-polling,因为从 Client 端的角度来看,除了用于连接服务器的 URL 之外没有其他区别。当前有两种实现:

  • RestTemplateXhrTransport使用 Spring 的RestTemplate进行 HTTP 请求。

  • JettyXhrTransport使用 Jetty 的HttpClient进行 HTTP 请求。

以下示例显示了如何创建 SockJSClient 端并连接到 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");

Note

SockJS 对消息使用 JSON 格式的数组。默认情况下,使用 Jackson 2,并且需要在 Classpath 上。另外,您可以配置SockJsMessageCodec的自定义实现,并在SockJsClient上对其进行配置。

要使用SockJsClient模拟大量并发用户,您需要配置基础 HTTPClient 端(用于 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)。

 

posted @ 2024-10-15 14:06  节日快乐  阅读(21)  评论(0编辑  收藏  举报