26. WebSocket支持

这部分参考文档涵盖了Spring Framework对Web应用程序中WebSocket样式消息传递的支持,包括使用STOMP作为应用程序级WebSocket子协议。

Section 26.1, “Introduction” Build 了思考WebSocket的思维框架,涵盖了采用挑战,设计考虑因素以及何时适合的想法。

Section 26.2, “WebSocket API” 回顾了服务器端的Spring WebSocket API,而 Section 26.3, “SockJS Fallback” 解释了SockJS协议,并展示了如何配置和使用它。

Section 26.4.1, “Overview” 介绍了STOMP消息传递协议。 Section 26.4.3, “Enable STOMP” 演示了如何在Spring中配置STOMP支持。 Section 26.4.5, “Annotated Controllers” 以及以下各节介绍了如何编写带注释的消息处理方法,发送消息,选择消息代理选项以及如何使用特殊的 "user" 目标。最后, Section 26.4.19, “Testing” 列出了三种测试STOMP / WebSocket应用程序的方法。

26.1 简介

WebSocket协议 RFC 6455 为Web应用程序定义了一项重要的新功能:客户端和服务器之间的全双工,双向通信。这是一项激动人心的新功能,其历史悠久的技术使网络更具互动性,包括Java Applets,XMLHttpRequest,Adobe Flash,ActiveXObject,各种Comet技术,服务器发送事件等。

WebSocket协议的正确介绍超出了本文档的范围。但至少重要的是要理解HTTP仅用于初始握手,它依赖于HTTP内置的机制来请求协议升级(或者在这种情况下是协议交换机)服务器可以使用HTTP状态响应101 (切换协议)如果同意的话。假设握手成功,HTTP升级请求下的TCP套接字保持打开状态,客户端和服务器都可以使用它来相互发送消息。

Spring Framework 4包含一个新的 spring-websocket 模块,具有全面的WebSocket支持。它与Java WebSocket API标准( JSR-356 )兼容,并且还提供了额外的增值,如本文其余部分所述。

26.1.1 WebSocket后备选项

采用的一个重要挑战是在某些浏览器中缺乏对WebSocket的支持。值得注意的是,支持WebSocket的第一个Internet Explorer版本是版本10(有关浏览器版本的支持,请参阅 http://caniuse.com/websockets )。此外,一些限制性代理可以以这样的方式配置,这些方式要么阻止尝试进行HTTP升级,要么在一段时间之后断开连接,因为它已经保持打开太长时间。有关Peter Lubbers关于此主题的概述,请参阅InfoQ文章 "How HTML5 Web Sockets Interact With Proxy Servers"

因此,要在今天构建WebSocket应用程序,需要使用回退选项以便在必要时模拟WebSocket API。 Spring Framework提供了基于 SockJS protocol 的透明回退选项。可以通过配置启用这些选项,否则不需要修改应用程序。

26.1.2 消息传递架构

除了短期到中期的采用挑战之外,使用WebSocket还提出了重要的设计注意事项,这些注意事项对于早期识别非常重要,特别是与我们今天构建Web应用程序的知识形成鲜明对比。

如今,REST是一种广泛接受,理解并支持的用于构建Web应用程序的体系结构。它是一个依赖于拥有许多URL(名词),少数HTTP方法(动词)以及其他原则(如使用超媒体(链接),剩余无状态等)的架构。

相比之下,WebSocket应用程序可能仅将单个URL用于初始HTTP握手。此后,所有消息在同一TCP连接上共享和流动。这指向完全不同的,异步的,事件驱动的消息传递体系结构。一种更接近传统消息传递应用程序(例如JMS,AMQP)。

Spring Framework 4包含一个新的 spring-messaging 模块,其中包含来自 Spring Integration 项目的关键抽象,例如 MessageMessageChannelMessageHandler 以及其他可作为此类消息传递体系结构基础的模块。该模块还包括一组用于将消息映射到方法的注释,类似于基于Spring MVC注释的编程模型。

26.1.3 WebSocket中的子协议支持

WebSocket确实暗示了消息传递体系结构,但并未强制要求使用任何特定的消息传递协议。它是TCP上的一个非常薄的层,它将字节流转换为消息流(文本或二进制),而不是更多。应用程序可以解释消息的含义。

与HTTP(一种应用程序级协议)不同,在WebSocket协议中,传入消息中没有足够的信息可供框架或容器知道如何路由或处理它。因此,对于除了非常简单的应用程序之外的任何东西,WebSocket都可以说是太低了。它可以做到,但它可能会导致在顶部创建一个框架。这与当今大多数Web应用程序使用Web框架而不仅仅是Servlet API编写的方式相当。

因此,WebSocket RFC定义了 sub-protocols 的使用。在握手期间,客户端和服务器可以使用头部 Sec-WebSocket-Protocol 来同意子协议,即要使用的更高的应用级协议。不需要使用子协议,但即使不使用,应用程序仍然需要选择客户端和服务器都能理解的消息格式。该格式可以是自定义的,特定于框架的,也可以是标准的消息传递协议。

Spring Framework支持使用 STOMP - 一种简单的消息传递协议,最初创建用于脚本语言,其框架受HTTP启发。 STOMP得到广泛支持,非常适合在WebSocket和Web上使用。

26.1.4 我应该使用WebSocket吗?

考虑到使用WebSocket的所有设计考虑因素,可以合理地问“何时使用?”。

WebSocket的最佳选择是在Web应用程序中,客户端和服务器需要以高频率和低延迟交换事件。主要候选人包括但不限于金融,游戏,协作等应用程序。这些应用对时间延迟非常敏感,并且还需要以高频率交换各种消息。

但是,对于其他应用程序类型,情况可能并非如此。例如,显示突发新闻的新闻或社交Feed可能完全可以通过每隔几分钟进行一次简单的轮询。延迟很重要,但如果新闻需要几分钟才会出现,这是可以接受的。

即使在延迟是至关重要的情况下,如果消息量相对较低(例如监视网络故障), long polling 的使用应被视为一种相对简单的替代方案,可靠地工作并且在效率方面具有可比性(同样假设消息相对较低)。

它是低延迟和高频率消息的组合,可以使WebSocket协议的使用变得至关重要。即使在这样的应用程序中,仍然选择是否应该通过WebSocket消息完成所有客户端 - 服务器通信,而不是使用HTTP和REST。答案因应用程序而异;但是,有些功能可能会通过WebSocket和REST API公开,以便为客户提供替代方案。此外,REST API调用可能需要向通过WebSocket连接的感兴趣的客户端广播消息。

Spring Framework允许 @Controller@RestController 类具有HTTP请求处理和WebSocket消息处理方法。此外,Spring MVC请求处理方法或任何应用程序方法可以轻松地向所有感兴趣的WebSocket客户端或特定用户广播消息。

26.2 WebSocket API

Spring Framework提供了一个WebSocket API,旨在适应各种WebSocket引擎。目前,该列表包括WebSocket运行时,如Tomcat 7.0.47,Jetty 9.1,GlassFish 4.1,WebLogic 12.1.3和Undertow 1.0(以及WildFly 8.0)。随着更多WebSocket运行时变得可用,可以添加额外的支持。

如引言中所述,直接使用WebSocket API对于应用程序来说水平太低 - 在对消息格式做出假设之前,框架几乎无法解释消息或通过注释路由它们。这就是应用程序应该考虑使用子协议和Spring的STOMP而不是WebSocket支持的原因。使用更高级别的协议时,WebSocket API的详细信息变得不那么相关,就像使用HTTP时TCP通信的细节不会暴露给应用程序一样。然而,本节将介绍直接使用WebSocket的详细信息。

26.2.1 WebSocketHandler

创建WebSocket服务器就像实现 WebSocketHandler 一样简单,或者更可能扩展 TextWebSocketHandlerBinaryWebSocketHandler

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-config和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服务环境中相对简单。

26.2.2 WebSocket握手

自定义初始HTTP WebSocket握手请求的最简单方法是通过 HandshakeInterceptor ,它暴露 "before" 和 "after" 握手方法。这样的拦截器可用于排除握手或使 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 ,包括验证客户端来源,协商子协议等。如果应用程序需要配置自定义 RequestUpgradeStrategy 以适应WebSocket服务器引擎和尚不支持的版本,则可能还需要使用此选项(有关此主题的更多信息,请参阅 Section 26.2.4, “Deployment” )。 Java-config和XML命名空间都可以配置自定义 HandshakeHandler

26.2.3 WebSocketHandler装饰

Spring提供了一个 WebSocketHandlerDecorator 基类,可用于装饰带有其他行为的 WebSocketHandler 。使用WebSocket Java-config或XML命名空间时,默认情况下会提供并添加日志记录和异常处理实现。 ExceptionWebSocketHandlerDecorator 捕获由任何WebSocketHandler方法引起的所有未捕获的异常,并关闭状态为 1011 的WebSocket会话,指示服务器错误。

26.2.4 部署

Spring WebSocket API很容易集成到Spring MVC应用程序中,其中 DispatcherServlet 既可以提供HTTP WebSocket握手,也可以提供其他HTTP请求。通过调用 WebSocketHttpRequestHandler ,也可以轻松地集成到其他HTTP处理方案中。这很方便易懂。但是,特殊注意事项适用于JSR-356运行时。

Java WebSocket API(JSR-356)提供了两种部署机制。第一个涉及启动时的Servlet容器类路径扫描(Servlet 3功能);另一个是在Servlet容器初始化时使用的注册API。这些机制都不能使用单个 "front controller" 进行所有HTTP处理 - 包括WebSocket握手和所有其他HTTP请求 - 例如Spring MVC的 DispatcherServlet

这是JSR-356的一个重要限制,Spring的WebSocket支持通过提供特定于服务器的 RequestUpgradeStrategy 来解决,即使在JSR-356运行时运行也是如此。

已经创建了一个克服Java WebSocket API上述限制的请求,可以在WEBSOCKET_SPEC-211上进行跟踪。另请注意,Tomcat和Jetty已经提供了本机API替代方案,可以轻松克服限制。我们希望更多的服务器能够遵循他们的示例,无论它在Java WebSocket API中何时被解决。

第二个考虑因素是,支持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>

26.2.5 配置WebSocket引擎

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

对于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;
    }

}

或WebSocket 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>

对于客户端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)));
    }

}

或WebSocket 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>

26.2.6 配置允许的来源

从Spring Framework 4.1.5开始,WebSocket和SockJS的默认行为是仅接受相同的原始请求。也可以允许所有或指定的起源列表。此检查主要是为浏览器客户端设计的。没有什么可以阻止其他类型的客户端修改 Origin 标头值(有关详细信息,请参阅 RFC 6454: The Web Origin Concept )。

3种可能的行为是:

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

  • 允许指定的原始列表:每个提供的允许来源必须以 http://https:// 开头。在此模式下,启用SockJS时,将禁用基于IFrame和JSONP的传输。因此,启用此模式时,不支持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>

26.3 SockJS后备

introduction 中所述,WebSocket尚未在所有浏览器中受支持,并且可能被限制性网络代理排除。这就是为什么Spring提供了基于 SockJS protocol (版本0.3.3)尽可能接近地模拟WebSocket API的回退选项。

26.3.1 概述

SockJS的目标是让应用程序使用WebSocket API,但在运行时必要时可以回退到非WebSocket替代品,即无需更改应用程序代码。

SockJS包括:

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

  • SockJS JavaScript client - 用于浏览器的客户端库。

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

  • 截至4.1 spring-websocket 还提供了一个SockJS Java客户端。

SockJS专为在浏览器中使用而设计。它竭尽全力使用各种技术支持各种浏览器版本。有关SockJS传输类型和浏览器的完整列表,请参阅 SockJS client 页面。传输分为3大类:WebSocket,HTTP Streaming和HTTP Long Polling。有关这些类别的概述,请参阅 this blog post

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(“打开”帧),消息作为[“message1”,“message2”](JSON编码数组)发送,字母h(“心跳”帧)如果没有消息流默认为25秒,字母c(“关闭”框架)关闭会话。

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

26.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服务环境相对简单。

在浏览器端,应用程序可以使用模拟W3C WebSocket API的 sockjs-client (版本1.0.x)并与服务器通信,以根据其运行的浏览器选择最佳传输选项。查看 sockjs-client 页面和传输列表浏览器支持的类型。客户端还提供了几个配置选项,例如,指定要包含的传输。

26.3.3 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客户端应在IE 8和9中选择 xdr-streaming

如果您确实使用基于iframe的传输,并且在任何情况下,通过将HTTP响应头 X-Frame-Options 设置为 DENYSAMEORIGINALLOW-FROM <origin> ,可以指示可以指示浏览器阻止在给定页面上使用IFrame。这用于防止 clickjacking

Spring Security 3.2支持在每个响应上设置X-Frame-Options。默认情况下,Spring Security Java配置将其设置为DENY。在3.2中,Spring Security XML命名空间默认情况下不设置该标头,但可以配置为执行此操作,并且将来可以默认设置它。见7.1节。有关如何配置X-Frame-Options标头设置的详细信息,请参见Spring Security文档的“默认安全标头”。您还可以查看或观看SEC-2501以获取更多背景信息。

如果您的应用程序添加 X-Frame-Options 响应标头(应该!)并依赖于基于iframe的传输,则需要将标头值设置为 SAMEORIGINALLOW-FROM <origin> 。除此之外,Spring SockJS支持还需要知道SockJS客户端的位置,因为它是从iframe加载的。默认情况下,iframe设置为从CDN位置下载SockJS客户端。最好将此选项配置为与应用程序源相同的URL。

在Java配置中,这可以如下所示完成。 XML命名空间通过 <websocket:sockjs> 元素提供类似的选项:

@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");
    }

    // ...

}

在初始开发期间,请启用SockJS客户端开发模式,以防止浏览器缓存否则将被缓存的SockJS请求(如iframe)。有关如何启用它的详细信息,请参阅SockJS客户端页面。

26.3.4 心跳

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

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

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

26.3.5 客户断开

HTTP流式传输和HTTP长轮询SockJS传输要求连接保持打开时间比平时长。有关这些技术的概述,请参阅 this blog post

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

一个特定的问题是Servlet API不为已经消失的客户端提供通知,请参阅 SERVLET_SPEC-44 。但是,Servlet容器会在后续尝试写入响应时引发异常。由于Spring的SockJS服务支持服务器发送的心跳(默认情况下每25秒),这意味着如果更频繁地发送消息,通常会在该时间段内或更早的时间内检测到客户端断开连接。

因此,网络IO故障可能仅仅因为客户端已断开连接而发生,这可能会使用不必要的堆栈跟踪填充日志。 Spring尽最大努力识别代表客户端断开连接(特定于每个服务器)的此类网络故障,并使用AbstractSockJsSession中定义的专用日志类别DISCONNECTED_CLIENT_LOG_CATEGORY记录最小消息。如果需要查看堆栈跟踪,请将该日志类别设置为TRACE。

26.3.6 SockJS和CORS

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

也可以通过Spring的SockJsService中的 suppressCors 属性禁用这些CORS头的添加。

以下是SockJS预期的 Headers 和值列表:

  • "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 endpoints 前缀排除URL,从而让Spring的 SockJsService 处理它。

26.3.7 SockJsClient

提供了SockJS Java客户端,以便在不使用浏览器的情况下连接到远程SockJS endpoints 。当需要通过公共网络在2个服务器之间进行双向通信时,即在网络代理可能妨碍使用WebSocket协议的情况下,这尤其有用。 SockJS Java客户端对于测试目的也非常有用,例如模拟大量并发用户。

SockJS Java客户端支持“websocket”,“xhr-streaming”和“xhr-polling”传输。其余的仅适用于浏览器。

WebSocketTransport 可配置为:

JSR-356运行时中的

  • StandardWebSocketClient

  • JettyWebSocketClient 使用Jetty 9本机WebSocket API

  • Spring的任何实现 WebSocketClient

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

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

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

下面的示例显示了如何创建SockJS客户端并连接到SockJS endpoints :

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);
    }

    // ...
}

26.4 STOMP

WebSocket协议定义了两种类型的消息,文本和二进制,但它们的内容是未定义的。定义了客户端和服务器协商子协议的机制 - 即更高级别的消息传递协议,在WebSocket之上使用以定义每个消息可以发送什么类型的消息,每个消息的格式和内容是什么,等等上。子协议的使用是可选的,但无论是客户端还是服务器都需要就定义消息内容的某些协议达成一致。

26.4.1 概述

STOMP 是一个简单的面向文本的消息传递协议,最初是为Ruby,Python等脚本语言创建的。和Perl连接到企业消息代理。它旨在解决常用消息传递模式的子集。 STOMP可用于任何可靠的双向流网络协议,如TCP和WebSocket。虽然STOMP是面向文本的协议,但消息的有效负载可以是文本或二进制。

STOMP是一种基于帧的协议,其帧在HTTP上建模。 STOMP框架的结构:

COMMAND
header1:value1
header2:value2

Body^@

客户端可以使用SEND或SUBSCRIBE命令发送或订阅消息以及“目标”标头,该标头描述消息的内容以及应由谁接收消息。这启用了一个简单的发布 - 订阅机制,可用于通过代理将消息发送到其他连接的客户端,或者向服务器发送消息以请求执行某些工作。

使用Spring的STOMP支持时,Spring WebSocket应用程序充当客户端的STOMP代理。消息被路由到 @Controller 消息处理方法或简单的内存中间代理,该代理跟踪订阅并向订阅用户广播消息。您还可以将Spring配置为使用专用的STOMP代理(例如RabbitMQ,ActiveMQ等)来实现消息的实际广播。在这种情况下,Spring维护与代理的TCP连接,向其中继消息,并将消息从其传递到连接的WebSocket客户端。因此,Spring Web应用程序可以依赖于基于HTTP的统一安全性,通用验证以及熟悉的编程模型消息处理工作。

这是订阅接收股票报价的客户的示例,服务器可以周期性地发出该报价。通过计划任务通过 SimpMessagingTemplate 向经纪人发送消息:

SUBSCRIBE
id:sub-1
destination:/topic/price.stock.*

^@

以下是客户端发送 Transaction 请求的示例,服务器可以通过 @MessageMapping 方法处理该 Transaction 请求,稍后在执行后,向客户端广播 Transaction 确认消息和详细信息:

SEND
destination:/queue/trade
content-type:application/json
content-length:44

{"action":"BUY","ticker":"MMM","shares",44}^@

在STOMP规范中故意将目的地的含义保持不透明。它可以是任何字符串,完全由STOMP服务器来定义它们支持的目标语义和语法。然而,很常见的是,目标是类似路径的字符串,其中 "/topic/.." 表示发布 - 订阅(一对多),而 "/queue/" 表示点对点(一对一)消息交换。

STOMP服务器可以使用MESSAGE命令向所有订户广播消息。以下是服务器向订阅客户端发送股票报价的示例:

MESSAGE
message-id:nxahklf6-1
subscription:sub-1
destination:/topic/price.stock.MMM

{"ticker":"MMM","price":129.45}^@

知道服务器无法发送未经请求的消息非常重要。来自服务器的所有消息必须响应特定的客户端订阅,并且服务器消息的“subscription-id”头必须与客户端订阅的“id”头匹配。

以上概述旨在提供对STOMP协议的最基本的了解。建议完整地查看协议 specification

26.4.2 好处

使用STOMP作为子协议使Spring Framework和Spring Security能够提供比使用原始WebSocket更丰富的编程模型。关于HTTP与原始TCP的关系以及它如何使Spring MVC和其他Web框架能够提供丰富的功能,可以做出同样的观点。以下是一系列好处:

  • 无需发明自定义消息传递协议和消息格式。

  • STOMP客户端可用,包括Spring Framework中的 Java client

  • 可以使用诸如RabbitMQ,ActiveMQ等消息代理(可选)来管理订阅和广播消息。

  • 应用程序逻辑可以组织在任意数量的_671939中,并根据STOMP目标标头和处理原始WebSocket消息(针对给定连接使用单个 WebSocketHandler )路由到它们的消息。

  • 使用Spring Security根据STOMP目标和消息类型保护消息。

26.4.3 启用STOMP

spring-messagingspring-websocket 模块中提供了对WebSocket支持的STOMP。一旦拥有了这些依赖项,就可以通过WebSocket使用 Section 26.3, “SockJS Fallback” 公开STOMP endpoints ,如下所示:

import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/portfolio").withSockJS();
    }

    @Override
    public void configureMessageBroker(MessageBrokerRegistry config) {
        config.setApplicationDestinationPrefixes("/app");
        config.enableSimpleBroker("/topic", "/queue");
    }
}

"/portfolio" 是WebSocket(或SockJS)客户端需要连接到的 endpoints 的HTTP URL,以进行WebSocket握手。

目标头以 "/app" 开头的STOMP消息被路由到 @Controller 类中的 @MessageMapping 方法。

使用内置的消息代理进行订阅和广播;将目标 Headers 以“/ topic”或“/ queue”开头的消息路由到代理。

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:message-broker application-destination-prefix="/app">
        <websocket:stomp-endpoint path="/portfolio">
            <websocket:sockjs/>
        </websocket:stomp-endpoint>
        <websocket:simple-broker prefix="/topic, /queue"/>
    </websocket:message-broker>

</beans>

对于内置的简单代理,“/ topic”和“/ queue”前缀没有任何特殊含义。它们仅仅是区分pub-sub与点对点消息传递的惯例(即许多订阅者与一个消费者)。使用外部代理时,请检查代理的STOMP页面以了解哪种STOMP目标和它支持的前缀。

要从浏览器连接,对于SockJS,您可以使用 sockjs-client 。对于STOMP,许多应用程序使用了 jmesnil/stomp-websocket 库(也称为stomp.js),该库功能齐全,已在 生产环境 中使用多年但不再维护。目前, JSteunou/webstomp-client 是该库中最积极维护和不断发展的继承者,下面的示例代码基于:

var socket = new SockJS("/spring-websocket-portfolio/portfolio");
var stompClient = webstomp.over(socket);

stompClient.connect({}, function(frame) {
}

或者如果通过WebSocket连接(没有SockJS):

var socket = new WebSocket("/spring-websocket-portfolio/portfolio");
var stompClient = Stomp.over(socket);

stompClient.connect({}, function(frame) {
}

请注意,上面的 stompClient 不需要指定 loginpasscode 标头。即使它确实如此,它们也会在服务器端被忽略或被覆盖。有关身份验证的详细信息,请参阅 Section 26.4.9, “Connect to Broker”Section 26.4.11, “Authentication” 部分。

有关更多示例代码,请参阅:

26.4.4 消息流

一旦暴露了STOMP endpoints ,Spring应用程序就成为连接客户端的STOMP代理。本节介绍服务器端的消息流。

spring-messaging 模块包含对源自 Spring Integration 的消息传递应用程序的基础支持,后来被提取并合并到Spring Framework中,以便在许多 Spring projects 和应用程序场景中得到更广泛的使用。下面列出了一些可用的消息传递抽象:

Java配置(即 @EnableWebSocketMessageBroker )和XML命名空间配置(即 <websocket:message-broker> )都使用上述组件来组合消息工作流。下图显示了启用简单的内置消息代理时使用的组件:

message flow simple broker

上图中有3个消息通道:

  • "clientInboundChannel" - 用于传递从WebSocket客户端收到的消息。

  • "clientOutboundChannel" - 用于向WebSocket客户端发送服务器消息。

  • "brokerChannel" - 用于从服务器端的应用程序代码向消息代理发送消息。

下图显示了配置外部代理(例如RabbitMQ)以管理订阅和广播消息时使用的组件:

message flow broker relay

上图中的主要区别是使用“代理中继”通过TCP将消息传递到外部STOMP代理,以及将消息从代理传递到订阅的客户端。

当从WebSocket connectin接收消息时,它们被解码为STOMP帧,然后变为Spring Message 表示,并发送到 "clientInboundChannel" 进行进一步处理。例如,目标头以 "/app" 开头的STOMP消息可以路由到带注释的控制器中的 @MessageMapping 方法,而 "/topic""/queue" 消息可以直接路由到消息代理。

从客户端处理STOMP消息的带注释的 @Controller 可以通过 "brokerChannel" 向消息代理发送消息,并且代理将通过 "clientOutboundChannel" 将消息广播给匹配的订阅者。同一个控制器也可以响应HTTP请求执行相同的操作,因此客户端可以执行HTTP POST,然后 @PostMapping 方法可以向消息代理发送消息以向订阅的客户端广播。

让我们通过一个简单的例子来追踪流程。鉴于以下服务器设置:

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/portfolio");
    }

    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
        registry.setApplicationDestinationPrefixes("/app");
        registry.enableSimpleBroker("/topic");
    }

}

@Controller
public class GreetingController {

    @MessageMapping("/greeting") {
    public String handle(String greeting) {
        return "[" + getTimestamp() + ": " + greeting;
    }

}
  • 客户端连接到 "http://localhost:8080/portfolio" ,一旦 Build 了WebSocket连接,STOMP帧就开始在其上流动。

  • 客户端发送带有目标头 "/topic/greeting" 的SUBSCRIBE帧。收到并解码后,消息将发送到 "clientInboundChannel" ,然后路由到存储客户端订阅的消息代理。

  • 客户端将发送帧发送到 "/app/greeting""/app" 前缀有助于将其路由到带注释的控制器。删除 "/app" 前缀后,目标的剩余 "/greeting" 部分将映射到 GreetingController 中的 @MessageMapping 方法。

  • GreetingController 返回的值变为Spring Message ,其有效负载基于返回值和 "/topic/greeting" 的默认目标头(从输入目标派生, "/app" 替换为 "/topic" )。生成的消息将发送到 "brokerChannel" 并由消息代理处理。

  • 消息代理找到所有匹配的订阅者,并通过 "clientOutboundChannel" 向每个订阅者发送一个MESSAGE帧,消息被编码为STOMP帧并在WebSocket连接上发送。

下一节提供了有关注释方法的更多详细信息,包括支持的参数类型和返回值。

26.4.5 带注释的控制器

应用程序可以使用带注释的 @Controller 类来处理来自客户端的消息。这样的课程可以声明 @MessageMapping@SubscribeMapping@ExceptionHandler 方法,如下所述。

@MessageMapping

@MessageMapping 注释可用于根据目标路由消息的方法。它在方法级别和类型级别受支持。在类型级别 @MessageMapping 用于表示控制器中所有方法的共享映射。

默认情况下,目标映射应为Ant样式,路径模式,例如, "/foo*" , "/foo/**" 。模式包括对模板变量的支持,例如 "/foo/{id}" ,可以使用 @DestinationVariable 方法参数引用。

应用程序可以选择切换到以点分隔的目标约定。请参见第26.4.10节“作为分隔符的点”。

@MessageMapping 方法可以使用以下参数进行灵活签名:

方法参数描述
Message用于访问完整的消息。
MessageHeaders用于访问 Message 中的 Headers 。
MessageHeaderAccessorSimpMessageHeaderAccessorStompHeaderAccessor用于通过类型化访问器方法访问标头。
@Payload用于访问消息的有效负载,通过已配置的 MessageConverter 进行转换(例如,从JSON转换)。不需要存在此注释,因为如果没有其他参数匹配,则默认采用此注释。 Payload参数可以用 @javax.validation.Valid 或Spring的 @Validated 注释,以便自动验证。
@Header如有必要,使用 org.springframework.core.convert.converter.Converter 访问特定标头值以及类型转换。
@Headers用于访问邮件中的所有 Headers 。此参数必须可分配给 java.util.Map
@DestinationVariable用于访问从消息目标中提取的模板变量。根据需要,值将转换为声明的方法参数类型。
java.security.Principal反映在WebSocket HTTP握手时登录的用户。

@MessageMapping 方法返回一个值时,默认情况下,该值通过已配置的 MessageConverter 序列化为有效负载,然后作为 Message 发送到 "brokerChannel" ,从那里向用户广播。出站消息的目标与入站消息的目标相同,但前缀为 "/topic"

您可以使用 @SendTo 方法批注来自定义将有效负载发送到的目标。 @SendTo 也可以在类级别使用,以共享发送消息的默认目标目标。 @SendToUser 是仅向与消息关联的用户发送消息的变体。有关详细信息,请参阅 Section 26.4.13, “User Destinations”

@MessageMapping 方法的返回值可以用 ListenableFutureCompletableFutureCompletionStage 包装,以便异步生成有效负载。

作为从 @MessageMapping 方法返回有效负载的替代方法,您还可以使用 SimpMessagingTemplate 发送消息,这也是返回值在封面下处理的方式。见 Section 26.4.6, “Send Messages”

@SubscribeMapping

@SubscribeMapping 注释与 @MessageMapping 结合使用,以缩小映射到订阅消息的范围。在这种情况下, @MessageMapping 注释指定目标,而 @SubscribeMapping 仅表示对订阅消息感兴趣。

对于映射和输入参数, @SubscribeMapping 方法通常与任何 @MessageMapping 方法没有区别。例如,您可以将它与类型级别 @MessageMapping 组合以表示共享目标前缀,并且您可以使用与任何@ MessageMapping`方法相同的 method arguments

@SubscribeMapping 的主要区别在于,方法的返回值被序列化为有效负载并且不是发送到 "brokerChannel" 而是发送到 "clientOutboundChannel" ,直接有效地回复客户端而不是通过代理进行广播。这对于实现一次性请求 - 回复消息交换非常有用,并且永远不会保留订阅。此模式的常见方案是在必须加载和显示数据时应用程序初始化。

@SubscribeMapping 方法也可以使用 @SendTo 进行批注,在这种情况下,返回值将使用显式指定的目标目标发送到 "brokerChannel"

@MessageExceptionHandler

应用程序可以使用 @MessageExceptionHandler 方法来处理 @MessageMapping 方法中的异常。感兴趣的异常可以在注释本身中声明,或者如果您想要访问异常实例,则可以通过方法参数声明:

@Controller
public class MyController {

    // ...

    @MessageExceptionHandler
    public ApplicationError handleException(MyException exception) {
        // ...
        return appError;
    }
}

@MessageExceptionHandler 方法支持灵活的方法签名,并支持相同的方法参数类型并返回值作为 @MessageMapping 方法。

通常 @MessageExceptionHandler 方法适用于声明它们的 @Controller 类(或类层次结构)。如果您希望这些方法在控制器之间全局应用更多,则可以在标有 @ControllerAdvice 的类中声明它们。这与Spring MVC中的 similar support 相当。

26.4.6 发送消息

如果您想从应用程序的任何部分向连接的客户端发送消息,该怎么办?任何应用程序组件都可以向 "brokerChannel" 发送消息。最简单的方法是注入 SimpMessagingTemplate ,并使用它来发送消息。通常,应该很容易按类型注入,例如:

@Controller
public class GreetingController {

    private SimpMessagingTemplate template;

    @Autowired
    public GreetingController(SimpMessagingTemplate template) {
        this.template = template;
    }

    @RequestMapping(path="/greetings", method=POST)
    public void greet(String greeting) {
        String text = "[" + getTimestamp() + "]:" + greeting;
        this.template.convertAndSend("/topic/greetings", text);
    }

}

但是如果存在相同类型的另一个bean,它也可以通过其名称“brokerMessagingTemplate”进行限定。

26.4.7 简单经纪人

内置的简单消息代理处理来自客户端的订阅请求,将它们存储在内存中,并将消息广播到具有匹配目标的连接客户端。代理支持类似路径的目标,包括对Ant样式目标模式的预订。

应用程序也可以使用点分隔目标(vs斜杠)。请参见第26.4.10节“作为分隔符的点”。

26.4.8 外部经纪人

简单的代理非常适合入门,但仅支持STOMP命令的子集(例如,没有ack,收据等),依赖于简单的消息发送循环,并且不适合于 集群 。作为替代方案,应用程序可以升级到使用功能齐全的消息代理。

检查STOMP文档以查找您选择的消息代理(例如 RabbitMQActiveMQ 等),安装代理,并在启用STOMP支持的情况下运行它。然后在Spring配置中启用STOMP代理中继,而不是简单代理。

以下是启用功能齐全的代理的示例配置:

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/portfolio").withSockJS();
    }

    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
        registry.enableStompBrokerRelay("/topic", "/queue");
        registry.setApplicationDestinationPrefixes("/app");
    }

}

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:message-broker application-destination-prefix="/app">
        <websocket:stomp-endpoint path="/portfolio" />
            <websocket:sockjs/>
        </websocket:stomp-endpoint>
        <websocket:stomp-broker-relay prefix="/topic,/queue" />
    </websocket:message-broker>

</beans>

上述配置中的 "STOMP broker relay" 是Spring MessageHandler ,它通过将消息转发到外部消息代理来处理消息。为此,它 Build 到代理的TCP连接,将所有消息转发给它,然后通过其WebSocket会话将从代理接收的所有消息转发给客户端。从本质上讲,它充当 "relay" ,可以在两个方向上转发消息。

Spring使用org.projectreactor:reactor-net和io.netty:netty-all来管理与代理的TCP连接,这两者都需要作为项目依赖项添加。 Spring Framework 4.3.x中的STOMP代理支持与2.0.x版的Reactor兼容。因此,它不需要与需要Reactor 3.x的 spring - Cloud - 流 - 反应模块结合使用。 Spring Framework 5依赖于具有独立版本控制的Reactor 3和Reactor Netty,用于与STOMP代理的TCP连接,但也为反应式编程模型提供广泛的支持。

此外,应用程序组件(例如,HTTP请求处理方法,业务服务等)也可以向代理中继发送消息,如 Section 26.4.6, “Send Messages” 中所述,以便向订阅的WebSocket客户端广播消息。

实际上,代理中继实现了健壮且可扩展的消息广播。

26.4.9 连接到经纪人

STOMP代理中继维护与代理的单个 "system" TCP连接。此连接仅用于源自服务器端应用程序的消息,而不用于接收消息。您可以为此连接配置STOMP凭据,即STOMP帧 loginpasscode 标头。它在XML命名空间和Java配置中作为 systemLogin / systemPasscode 属性公开,默认值为 guest / guest

STOMP代理中继还为每个连接的WebSocket客户端创建单独的TCP连接。您可以配置STOMP凭据以用于代表客户端创建的所有TCP连接。它在XML命名空间和Java配置中作为 clientLogin / clientPasscode 属性公开,默认值为 guest / guest

STOMP代理中继始终在每个CONNECT帧上设置登录和密码头,它代表客户端转发给代理。因此,WebSocket客户端无需设置这些标头;他们会被忽略。正如第26.4.11节“身份验证”所解释的那样,WebSocket客户端应该依赖HTTP身份验证来保护WebSocket endpoints 并 Build 客户端身份。

STOMP代理中继还通过“系统”TCP连接向消息代理发送和接收心跳。您可以配置发送和接收心跳的间隔(默认情况下每个10秒)。如果与代理的连接丢失,代理中继将继续尝试每5秒重新连接一次,直到成功为止。

任何Spring bean都可以实现 ApplicationListener<BrokerAvailabilityEvent> ,以便在与代理的 "system" 连接丢失并重新 Build 时接收通知。例如,股票报价服务广播股票报价可以在没有活动的 "system" 连接时停止尝试发送消息。

默认情况下,STOMP代理中继始终连接,并在连接丢失时根据需要重新连接到同一主机和端口。如果您希望提供多个地址,则在每次尝试连接时,您都可以配置供应商地址,而不是固定的主机和端口。例如:

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {

	// ...

	@Override
	public void configureMessageBroker(MessageBrokerRegistry registry) {
		registry.enableStompBrokerRelay("/queue/", "/topic/").setTcpClient(createTcpClient());
		registry.setApplicationDestinationPrefixes("/app");
	}

	private Reactor2TcpClient<byte[]> createTcpClient() {

		Supplier<InetSocketAddress> addressSupplier = new Supplier<InetSocketAddress>() {
			@Override
			public InetSocketAddress get() {
				// Select address to connect to ...
			}
		};

		StompDecoder decoder = new StompDecoder();
		Reactor2StompCodec codec = new Reactor2StompCodec(new StompEncoder(), decoder);
		return new Reactor2TcpClient<>(addressSupplier, codec);
	}

}

STOMP代理中继也可以配置 virtualHost 属性。该属性的值将被设置为每 CONNECT 帧的 host 报头和在哪里被 Build 的TCP连接的实际的主机是从主机提供基于 Cloud 的STOMP服务不同可以是例如在 Cloud 环境是有用的。

26.4.10 点作为分隔符

当消息路由到 @MessageMapping 方法时,它们与 AntPathMatcher 匹配,并且默认情况下,模式应使用斜杠 "/" 作为分隔符。这是Web应用程序中的一个很好的约定,类似于HTTP URL。但是,如果您更习惯于消息传递约定,则可以切换到使用dot "." 作为分隔符。

在Java配置中:

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

    // ...

    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
        registry.setPathMatcher(new AntPathMatcher("."));
        registry.enableStompBrokerRelay("/queue", "/topic");
        registry.setApplicationDestinationPrefixes("/app");
    }
}

在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:message-broker application-destination-prefix="/app" path-matcher="pathMatcher">
        <websocket:stomp-endpoint path="/stomp"/>
        <websocket:stomp-broker-relay prefix="/topic,/queue" />
    </websocket:message-broker>

    
    <bean id="pathMatcher" class="org.springframework.util.AntPathMatcher">
        <constructor-arg index="0" value="."/>
    </bean>
    

</beans>

之后,控制器可以使用点 "." 作为 @MessageMapping 方法中的分隔符:

@Controller
@MessageMapping("foo")
public class FooController {

    @MessageMapping("bar.{baz}")
    public void handleBaz(@DestinationVariable String baz) {
        // ...
    }
}

客户端现在可以向 "/app/foo.bar.baz123" 发送消息。

在上面的示例中,我们没有更改“代理中继”上的前缀,因为它们完全依赖于外部消息代理。检查您正在使用的代理的STOMP文档页面,以查看它为目标标头支持的约定。

另一方面, "simple broker" 确实依赖于已配置的 PathMatcher ,因此如果您切换也将应用于代理的分隔符,并且方式将消息中的目标与订阅中的模式匹配。

26.4.11 身份验证

WebSocket消息传递会话中的每个STOMP都以HTTP请求开始 - 可以是升级到WebSockets的请求(即WebSocket握手),或者在SockJS回退一系列SockJS HTTP传输请求的情况下。

Web应用程序已经具有用于保护HTTP请求的身份验证和授权。通常,用户通过Spring Security使用某种机制(例如登录页面,HTTP基本身份验证或其他)进行身份验证。经过身份验证的用户的安全上下文保存在HTTP会话中,并与同一个基于cookie的会话中的后续请求相关联。

因此,对于WebSocket握手或SockJS HTTP传输请求,通常会有通过 HttpServletRequest#getUserPrincipal() 可访问的经过身份验证的用户。 Spring自动将该用户与为其创建的WebSocket或SockJS会话相关联,随后通过用户头与该会话上传输的所有STOMP消息相关联。

简而言之,典型的Web应用程序除了它已经为安全做的事情之外,还没有什么特别需要做的。用户在HTTP请求级别进行身份验证,通过基于cookie的HTTP会话维护安全上下文,然后将该Web会话与为该用户创建的WebSocket或SockJS会话相关联,并在流经应用程序的每个 Message 上标记用户标头。

请注意,STOMP协议在 CONNECT 帧上确实有 "login" 和 "passcode" 标头。这些最初设计用于并且仍然需要例如用于TCP上的STOMP。但是,对于STOMP over WebSocket,Spring默认忽略STOMP协议级别的授权标头,并假定用户已在HTTP传输级别进行了身份验证,并期望WebSocket或SockJS会话包含经过身份验证的用户。

Spring Security提供WebSocket子协议授权,该授权使用ChannelInterceptor根据其中的用户头来授权消息。此外,Spring Session还提供WebSocket集成,以确保在WebSocket会话仍处于活动状态时用户HTTP会话不会过期。

26.4.12 令牌认证

Spring Security OAuth 提供对基于令牌的安全性的支持,包括JSON Web令牌(JWT)。这可以用作Web应用程序中的身份验证机制,包括STOMP over WebSocket交互,正如上一节所述,即通过基于cookie的会话维护身份。

同时,基于cookie的会话并不总是最适合,例如在不希望完全维护服务器端会话的应用程序中,或者在通常使用标头进行身份验证的移动应用程序中。

WebSocket protocol RFC 6455 "doesn’t prescribe any particular way that servers can authenticate clients during the WebSocket handshake." 实际上,浏览器客户端只能使用标准身份验证标头(即基本HTTP身份验证)或cookie,并且不能提供自定义标头。同样,SockJS JavaScript客户端不提供使用SockJS传输请求发送HTTP标头的方法,请参阅 sockjs-client issue 196 。相反,它确实允许发送可用于发送令牌但具有其自身缺点的查询参数,例如,因为令牌可能无意中使用服务器日志中的URL进行了记录。

以上限制适用于基于浏览器的客户端,不适用于基于Spring Java的STOMP客户端,该客户端支持使用WebSocket和SockJS请求发送标头。

因此,希望避免使用cookie的应用程序可能没有HTTP协议级别的身份验证的良好替代方案。他们可能更喜欢在STOMP消息传递协议级别使用标头进行身份验证,而不是使用Cookie。执行此操作有两个简单的步骤:

  • 使用STOMP客户端在连接时传递身份验证标头。

  • 使用 ChannelInterceptor 处理身份验证标头。

下面是注册自定义身份验证拦截器的示例服务器端配置。请注意,拦截器只需要在CONNECT Message 上进行身份验证并设置用户头。 Spring将记录并保存经过身份验证的用户,并将其与同一会话中的后续STOMP消息相关联:

@Configuration
@EnableWebSocketMessageBroker
public class MyConfig extends AbstractWebSocketMessageBrokerConfigurer {

    @Override
    public void configureClientInboundChannel(ChannelRegistration registration) {
        registration.setInterceptors(new ChannelInterceptorAdapter() {
            @Override
            public Message<?> preSend(Message<?> message, MessageChannel channel) {
                StompHeaderAccessor accessor =
                        MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
                if (StompCommand.CONNECT.equals(accessor.getCommand())) {
                    Authentication user = ... ; // access authentication header(s)
                    accessor.setUser(user);
                }
                return message;
            }
        });
    }
}

另请注意,在使用Spring Security的邮件授权时,您需要确保在Spring Security之前订购身份验证 ChannelInterceptor config。最好通过在 AbstractWebSocketMessageBrokerConfigurer 标记为 @Order(Ordered.HIGHEST_PRECEDENCE + 99) 的子类中声明自定义拦截器来完成。

26.4.13 用户目的地

应用程序可以发送针对特定用户的消息,Spring的STOMP支持可识别以 "/user/" 为前缀的目标。例如,客户端可能订阅目标 "/user/queue/position-updates" 。该目的地将由 UserDestinationMessageHandler 处理并转换为用户会话唯一的目的地,例如, "/queue/position-updates-user123" 。这提供了订阅一般命名的目的地的便利性,同时确保不与订阅相同目的地的其他用户发生冲突,使得每个用户可以接收唯一的库存位置更新。

在发送方,可以将消息发送到诸如 "/user/{username}/queue/position-updates" 之类的目的地,然后由 UserDestinationMessageHandler 将其转换为一个或多个目的地,一个用于与用户相关联的每个会话。这允许应用程序中的任何组件发送针对特定用户的消息,而不必知道除其名称和通用目标之外的任何内容。通过注释和消息传递模板也支持此功能。

例如,消息处理方法可以向与通过 @SendToUser 注释处理的消息关联的用户发送消息(在类级别上也支持共享公共目标):

@Controller
public class PortfolioController {

    @MessageMapping("/trade")
    @SendToUser("/queue/position-updates")
    public TradeResult executeTrade(Trade trade, Principal principal) {
        // ...
        return tradeResult;
    }
}

如果用户具有多个会话,则默认情况下,所有订阅给定目标的会话都是目标。但有时,可能需要仅定位发送正在处理的消息的会话。这可以通过将 broadcast 属性设置为false来完成,例如:

@Controller
public class MyController {

    @MessageMapping("/action")
    public void handleAction() throws Exception{
        // raise MyBusinessException here
    }

    @MessageExceptionHandler
    @SendToUser(destinations="/queue/errors", broadcast=false)
    public ApplicationError handleException(MyBusinessException exception) {
        // ...
        return appError;
    }
}

虽然用户目的地通常意味着经过身份验证的用户,但并不严格要求。与经过身份验证的用户无关的WebSocket会话可以订阅用户目标。在这种情况下,@ SendToUser注释的行为与broadcast = false完全相同,即仅针对发送正在处理的消息的会话。

例如,通过注入由Java配置或XML命名空间创建的 SimpMessagingTemplate ,也可以从任何应用程序组件向用户目标发送消息(如果需要 @Qualifier ,则bean名称为 "brokerMessagingTemplate" ):

@Service
public class TradeServiceImpl implements TradeService {

	private final SimpMessagingTemplate messagingTemplate;

	@Autowired
	public TradeServiceImpl(SimpMessagingTemplate messagingTemplate) {
		this.messagingTemplate = messagingTemplate;
	}

	// ...

	public void afterTradeExecuted(Trade trade) {
		this.messagingTemplate.convertAndSendToUser(
				trade.getUserName(), "/queue/position-updates", trade.getResult());
	}
}

将用户目标与外部消息代理一起使用时,请检查代理文档,了解如何管理非活动队列,以便在用户会话结束时删除所有唯一用户队列。例如,当使用/exchange/amq.direct/position-updates等目标时,RabbitMQ会创建自动删除队列。因此,在这种情况下,客户端可以订阅/user/exchange/amq.direct/position-updates。同样,ActiveMQ具有用于清除非活动目标的配置选项。

在多应用程序服务器方案中,用户目标可能仍未解析,因为用户连接到不同的服务器。在这种情况下,您可以配置目标以广播未解析的消息,以便其他服务器有机会尝试。这可以通过Java配置中 MessageBrokerRegistryuserDestinationBroadcast 属性和XML中 message-broker 元素的 user-destination-broadcast 属性来完成。

26.4.14 事件和拦截

发布了几个 ApplicationContext 事件(如下所列),可以通过实现Spring的 ApplicationListener 接口来接收它们。

  • BrokerAvailabilityEvent - 表示代理何时可用/不可用。虽然 "simple" 代理在启动时立即可用,并且在应用程序运行时仍然如此,但STOMP "broker relay" 可能会失去与全功能代理的连接,例如,如果代理重新启动。代理中继具有重新连接逻辑,并在它返回时重新 Build 与代理的 "system" 连接,因此只要状态从连接变为断开连接,就会发布此事件,反之亦然。使用 SimpMessagingTemplate 的组件应订阅此事件,并避免在代理不可用时发送消息。在任何情况下他们应该准备好在发送消息时处理 MessageDeliveryException

  • SessionConnectEvent - 在收到指示新客户端会话开始的新STOMP CONNECT时发布。该事件包含表示连接的消息,包括会话ID,用户信息(如果有)以及客户端可能已发送的任何自定义标头。这对于跟踪客户端会话很有用。订阅此事件的组件可以使用 SimpMessageHeaderAccessorStompMessageHeaderAccessor 包装所包含的消息。

  • SessionConnectedEvent - 在经纪人发送STOMP CONNECTED框架以响应CONNECT之后不久发布的 SessionConnectEvent 。此时,可以认为STOMP会话已完全 Build 。

  • SessionSubscribeEvent - 在收到新的STOMP SUBSCRIBE时发布。

  • SessionUnsubscribeEvent - 在收到新的STOMP UNSUBSCRIBE时发布。

  • SessionDisconnectEvent - 在STOMP会话结束时发布。 DISCONNECT可能已从客户端发送,也可能在WebSocket会话关闭时自动生成。在某些情况下,每个会话可能会多次发布此事件。对于多个断开连接事件,组件应该是幂等的。

使用功能齐全的代理时,STOMP“代理中继”会自动重新连接“系统”连接,以防代理暂时不可用。但是,客户端连接不会自动重新连接。假设启用了心跳,客户端通常会注意到代理在10秒内没有响应。客户端需要实现自己的重新连接逻辑。

上述事件反映了STOMP连接生命周期中的点。它们并不意味着为客户端发送的每条消息提供通知。相反,应用程序可以注册 ChannelInterceptor 来拦截每个传入和传出的STOMP消息。例如,拦截入站消息:

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {

    @Override
    public void configureClientInboundChannel(ChannelRegistration registration) {
        registration.setInterceptors(new MyChannelInterceptor());
    }
}

自定义 ChannelInterceptor 可以扩展空方法基类 ChannelInterceptorAdapter 并使用 StompHeaderAccessorSimpMessageHeaderAccessor 来访问有关该消息的信息。

public class MyChannelInterceptor extends ChannelInterceptorAdapter {

    @Override
    public Message<?> preSend(Message<?> message, MessageChannel channel) {
        StompHeaderAccessor accessor = StompHeaderAccessor.wrap(message);
        StompCommand command = accessor.getStompCommand();
        // ...
        return message;
    }
}

请注意,就像上面的 SesionDisconnectEvent 一样,可能已经从客户端发送了DISCONNECT消息,或者也可能在WebSocket会话关闭时自动生成。在某些情况下,拦截器可能会在每个会话中多次拦截此消息。对于多个断开连接事件,组件应该是幂等的。

26.4.15 STOMP客户端

Spring通过WebSocket客户端提供STOMP,通过TCP客户端提供STOMP。

要开始创建和配置 WebSocketStompClient

WebSocketClient webSocketClient = new StandardWebSocketClient();
WebSocketStompClient stompClient = new WebSocketStompClient(webSocketClient);
stompClient.setMessageConverter(new StringMessageConverter());
stompClient.setTaskScheduler(taskScheduler); // for heartbeats

在上面的示例中, StandardWebSocketClient 可以替换为 SockJsClient ,因为这也是 WebSocketClient 的实现。 SockJsClient 可以使用基于WebSocket或HTTP的传输作为后备。有关详细信息,请参阅 Section 26.3.7, “SockJsClient”

接下来 Build 连接并为STOMP会话提供处理程序:

String url = "ws://127.0.0.1:8080/endpoint";
StompSessionHandler sessionHandler = new MyStompSessionHandler();
stompClient.connect(url, sessionHandler);

当会话准备好使用时,会通知处理程序:

public class MyStompSessionHandler extends StompSessionHandlerAdapter {

	@Override
	public void afterConnected(StompSession session, StompHeaders connectedHeaders) {
		// ...
	}
}

Build 会话后,可以发送任何有效负载,并使用配置的 MessageConverter 序列化:

session.send("/topic/foo", "payload");

您也可以订阅目的地。 subscribe 方法需要处理订阅消息的处理程序并返回可用于取消订阅的 Subscription 句柄。对于每个收到的消息,处理程序可以指定有效负载应该反序列化的目标对象类型:

session.subscribe("/topic/foo", new StompFrameHandler() {

	@Override
	public Type getPayloadType(StompHeaders headers) {
		return String.class;
	}

	@Override
	public void handleFrame(StompHeaders headers, Object payload) {
		// ...
	}

});

要启用STOMP心跳,请使用 TaskScheduler 配置 WebSocketStompClient ,并可选择自定义心跳间隔,写入不活动10秒,导致心跳发送,读取不活动10秒,关闭连接。

当使用WebSocketStompClient进行性能测试以模拟来自同一台计算机的数千个客户端时,请考虑关闭心跳,因为每个连接都会调度自己的心跳任务,并且没有针对在同一台计算机上运行的大量客户端进行优化。

STOMP协议还支持收据,其中客户端必须添加 "receipt" 标头,服务器在处理发送或订阅后以RECEIPT帧响应。为了支持这一点, StompSession 提供 setAutoReceipt(boolean) ,导致在每个后续发送或订阅时添加 "receipt" 标头。或者,您也可以手动将 "receipt" 标头添加到 StompHeaders 。发送和订阅都返回 Receiptable 的实例,该实例可用于注册接收成功和失败回调。对于此功能,客户端必须配置 TaskScheduler 和收据到期前的时间(默认为15秒)。

请注意, StompSessionHandler 本身是 StompFrameHandler ,除了处理消息的异常的 handleException 回调之外,它还允许它处理ERROR帧,并且对于包括 ConnectionLostException 在内的传输级错误, handleTransportError

26.4.16 WebSocket范围

每个WebSocket会话都有一个属性映射。映射作为标头附加到入站客户端消息,可以从控制器方法访问,例如:

@Controller
public class MyController {

	@MessageMapping("/action")
	public void handle(SimpMessageHeaderAccessor headerAccessor) {
		Map<String, Object> attrs = headerAccessor.getSessionAttributes();
		// ...
	}
}

也是可以在 websocket 范围内声明一个Spring管理的bean。 WebSocket范围的bean可以注入控制器和 "clientInboundChannel" 上注册的任何通道拦截器。这些通常是单身,比任何单个WebSocket会话都更长寿。因此,您需要为WebSocket范围的bean使用范围代理模式:

@Component
@Scope(scopeName = "websocket", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class MyBean {

    @PostConstruct
    public void init() {
        // Invoked after dependencies injected
    }

    // ...

    @PreDestroy
    public void destroy() {
        // Invoked when the WebSocket session ends
    }
}

@Controller
public class MyController {

    private final MyBean myBean;

    @Autowired
    public MyController(MyBean myBean) {
        this.myBean = myBean;
    }

    @MessageMapping("/action")
    public void handle() {
        // this.myBean from the current WebSocket session
    }
}

与任何自定义作用域一样,Spring在第一次从控制器访问时初始化一个新的 MyBean 实例,并将该实例存储在WebSocket会话属性中。随后返回相同的实例,直到会话结束。 WebSocket范围的bean将调用所有Spring生命周期方法,如上面的示例所示。

26.4.17 表现

在性能方面没有银弹。许多因素可能会影响它,包括消息的大小,数量,应用程序方法是否执行需要阻止的工作,以及网络速度等外部因素。本部分的目标是提供可用配置选项的概述以及有关如何推理扩展的一些想法。

在消息传递应用程序中,消息通过用于由线程池支持的异步执行的通道传递。配置此类应用程序需要充分了解通道和消息流。因此,建议审查 Section 26.4.4, “Flow of Messages”

显而易见的起点是配置支持 "clientInboundChannel""clientOutboundChannel" 的线程池。默认情况下,两者都配置为可用处理器数量的两倍。

如果注释方法中的消息处理主要是CPU绑定的,则 "clientInboundChannel" 的线程数应保持接近处理器数。如果他们所做的工作更多是IO绑定并且需要阻塞或等待数据库或其他外部系统,则需要增加线程池大小。

ThreadPoolExecutor有3个重要属性。这些是核心和最大线程池大小以及队列存储没有可用线程的任务的容量。常见的混淆点是配置核心池大小(例如10)和最大池大小(例如20)导致具有10到20个线程的线程池。实际上,如果容量保留为其默认值Integer.MAX_VALUE,则线程池将永远不会超出核心池大小,因为所有其他任务都将排队。请查看ThreadPoolExecutor的Javadoc,了解这些属性如何工作并理解各种排队策略。

"clientOutboundChannel" 方面,它是关于向WebSocket客户端发送消息的全部内容。如果客户端位于快速网络上,则线程数应保持接近可用处理器的数量。如果它们很慢或带宽较低,则消耗消息所需的时间会更长,并给线程池带来负担。因此,增加线程池大小是必要的。

尽管 "clientInboundChannel" 的工作负载可以预测 - 毕竟它基于应用程序的作用 - 如何配置 "clientOutboundChannel" 更难,因为它基于应用程序无法控制的因素。因此,有两个与发送消息相关的附加属性。那些是 "sendTimeLimit""sendBufferSizeLimit" 。这些用于配置允许发送多长时间以及在向客户端发送消息时可以缓冲多少数据。

一般的想法是,在任何给定时间,只有一个线程可用于发送给客户端。同时所有其他消息都会被缓冲,您可以使用这些属性来决定允许发送消息的时间长度以及可以平均缓冲多少数据。有关重要的其他详细信息,请查看此配置的XML架构的Javadoc和文档。

这是示例配置:

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

    @Override
    public void configureWebSocketTransport(WebSocketTransportRegistration registration) {
        registration.setSendTimeLimit(15 * 1000).setSendBufferSizeLimit(512 * 1024);
    }

    // ...

}
<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:message-broker>
        <websocket:transport send-timeout="15000" send-buffer-size="524288" />
        <!-- ... -->
    </websocket:message-broker>

</beans>

上面显示的WebSocket传输配置还可用于配置传入STOMP消息的最大允许大小。虽然理论上WebSocket消息的大小几乎是无限的,但实际上WebSocket服务器会施加限制 - 例如,Tomcat上的8K和Jetty上的64K。因此,诸如stomp.js之类的STOMP客户端在16K边界处拆分较大的STOMP消息,并将它们作为多个WebSocket消息发送,因此需要服务器缓冲和重新组装。

Spring的STOMP over WebSocket支持这样做,因此应用程序可以配置STOMP消息的最大大小,而不管WebSocket服务器特定的消息大小。请记住,必要时将自动调整WebSocket消息大小,以确保它们至少可以携带16K WebSocket消息。

这是示例配置:

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

    @Override
    public void configureWebSocketTransport(WebSocketTransportRegistration registration) {
        registration.setMessageSizeLimit(128 * 1024);
    }

    // ...

}
<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:message-broker>
        <websocket:transport message-size="131072" />
        <!-- ... -->
    </websocket:message-broker>

</beans>

关于扩展的一个重点是使用多个应用程序实例。目前,使用简单代理无法做到这一点。但是,当使用RabbitMQ等功能齐全的代理时,每个应用程序实例都会连接从一个应用程序实例广播的代理和消息可以通过代理广播到通过任何其他应用程序实例连接的WebSocket客户端。

26.4.18 监测

使用 @EnableWebSocketMessageBroker<websocket:message-broker> 密钥基础结构组件时,会自动收集统计信息和计数器,以便深入了解应用程序的内部状态。该配置还声明了一个类型为 WebSocketMessageBrokerStats 的bean,它在一个位置收集所有可用信息,默认情况下每30分钟将其记录在 INFO 级别一次。这个bean可以通过Spring的 MBeanExporter 导出到JMX,以便在运行时查看,例如通过JDK的 jconsole 。以下是可用信息的摘要。

  • 客户端WebSocket会话

  • 当前

    • 表示当前有多少客户端会话,其中包括WebSocket与HTTP流和轮询SockJS会话进一步细分的计数。
  • 总计

    • 表示已 Build 的会话总数。
  • 异常关闭

  • 连接失败

    • 这些会话已经 Build 但在60秒内没有收到任何消息后关闭。这通常表示代理或网络问题。
  • 超出发送限制
    超过配置的发送超时或缓慢客户端可能发生的发送缓冲区限制后,

    • 会话关闭(请参阅上一节)。
  • 传输错误

    • 个会话在传输错误(例如无法读取或写入WebSocket连接或HTTP请求/响应)后关闭。
  • STOMP帧

    • 处理的CONNECT,CONNECTED和DISCONNECT帧总数,表示STOMP级别连接的客户端数量。请注意,当会话异常关闭或客户端关闭而不发送DISCONNECT帧时,DISCONNECT计数可能会更低。
  • STOMP经纪人接力

  • TCP连接

    • 表示代表客户端WebSocket会话 Build 到代理的TCP连接数。这应该等于客户端WebSocket会话的数量1个额外的共享 "system" 连接,用于从应用程序内发送消息。
  • STOMP帧

    • 代表客户端转发到代理或从代理接收的CONNECT,CONNECTED和DISCONNECT帧的总数。请注意,无论客户端WebSocket会话如何关闭,都会将DISCONNECT帧发送到代理。因此,较低的DISCONNECT帧计数表示代理主动关闭连接,可能是因为没有及时到达的心跳,无效的输入帧或其他。
  • 客户端入站通道
    来自线程池的

    • stats支持 "clientInboundChannel" ,提供对传入消息处理的运行状况的深入了解。在此排队的任务表明应用程序可能太慢而无法处理消息。如果有I / O绑定任务(例如,慢速数据库查询,对第三方REST API的HTTP请求等),请考虑增加线程池大小。
  • 客户出站 Channels
    来自支持 "clientOutboundChannel" 的线程池的

    • stats,提供对客户端广播消息运行状况的深入了解。在此排队的任务表明客户端消耗消息的速度太慢。解决此问题的一种方法是增加线程池大小以适应预期的并发慢客户端数量。另一种选择是减少发送超时和发送缓冲区大小限制(参见上一节)。
  • SockJS任务计划程序
    来自SockJS任务调度程序的线程池的

    • stats,用于发送心跳。请注意,在STOMP级别协商心跳时,将禁用SockJS心跳。

26.4.19 测试

使用Spring的STOMP over WebSocket支持测试应用程序有两种主要方法。第一种是编写服务器端测试,以验证控制器的功能及其带注释的消息处理方法。第二种是编写涉及运行客户端和服务器的完整端到端测试。

这两种方法并不相互排斥。相反,每个人都在整体测试策略中占有一席之地。服务器端测试更集中,更易于编写和维护。另一方面,端到端集成测试更完整,测试更多,但它们也更多地参与编写和维护。

最简单的服务器端测试形式是编写控制器单元测试。然而,由于控制器的大部分功能取决于其注释,因此这没有用。纯单元测试根本无法测试。

理想情况下,测试中的控制器应该在运行时调用,就像测试使用Spring MVC测试框架处理HTTP请求的控制器的方法一样。即,不运行Servlet容器,而是依赖Spring Framework来调用带注释的控制器。就像Spring MVC Test一样,有两种可能替代方案,使用“基于上下文”或“独立”设置:

  • 在Spring TestContext框架的帮助下加载实际的Spring配置,将 "clientInboundChannel" 作为测试字段注入,并使用它来发送要由控制器方法处理的消息。

  • 手动设置调用控制器所需的最小Spring框架基础结构(即 SimpAnnotationMethodMessageHandler ),并将控制器的消息直接传递给它。

这两种设置方案都在 tests for the stock portfolio 示例应用程序中进行了演示。

第二种方法是创建端到端集成测试。为此,您需要以嵌入模式运行WebSocket服务器,并将其作为WebSocket客户端连接到它,发送包含STOMP帧的WebSocket消息。 tests for the stock portfolio 示例应用程序还演示了使用Tomcat作为嵌入式WebSocket服务器和用于测试目的的简单STOMP客户端的这种方法。

Updated at: 5 months ago
25.10. Portlet应用程序部署Table of content27. CORS支持
Comment
You are not logged in.

There are no comments.