Spring Framework 参考文档(WebSocket STOMP)

WebSocket STOMP

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

概述

STOMP(简单的面向文本的消息传递协议)最初是为脚本语言(如Ruby、Python和Perl)创建的,用于连接到企业消息代理,它被设计用于处理常用消息传递模式的最小子集,STOMP可以用于任何可靠的双向流网络协议,如TCP和WebSocket,虽然STOMP是一个面向文本的协议,但消息payload可以是文本或二进制。

STOMP是基于帧的协议,其帧是基于HTTP建模的,下面的清单显示了一个STOMP帧的结构:

COMMAND
header1:value1
header2:value2

Body^@

客户端可以使用SENDSUBSCRIBE命令发送或订阅消息,以及描述消息是关于什么并且谁应该接收它的destination header。这支持一种简单的发布-订阅机制,你可以使用该机制将消息通过代理发送到其他连接的客户端,或将消息发送到服务器,以请求执行某些工作。

当你使用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.*

^@

下面的示例显示了发送交易请求的客户端,服务器可以通过@MessageMapping方法处理该请求:

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

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

执行之后,服务器可以向客户端广播交易确认消息和详细信息。

destination的含义在STOMP规范中故意保持不透明,它可以是任何字符串,完全由STOMP服务器来定义它们支持的destination的语义和语法。然而,destination通常是像路径字符串,其中包含/topic/..意味着发布-订阅(一对多)和/queue/意味着点对点(一对一)消息交换。

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

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

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

服务器不能发送未经请求的消息,来自服务器的所有消息都必须响应特定的客户端订阅,服务器消息的subscription-id header必须与客户端订阅的id header匹配。

前面的概述旨在提供对STOMP协议的最基本的理解,我们建议全面审查协议规范

优点

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

  • 无需创建自定义消息传递协议和消息格式。
  • STOMP客户端,包含一个Spring Framework中的Java客户端。
  • 你可以(可选地)使用消息代理(如RabbitMQ、ActiveMQ等)来管理订阅和广播消息。
  • 应用程序逻辑可以组织在任意数量的@Controller实例中,可以基于STOMP destination header将消息路由到它们,而不必针对给定连接使用单个WebSocketHandler处理原始WebSocket消息。
  • 你可以使用Spring Security来基于STOMP destination和消息类型保护消息。

启用STOMP

spring-messagingspring-websocket模块提供WebSocket支持的STOMP,一旦有了这些依赖项,就可以通过WebSocket使用SockJS Fallback公开STOMP端点,如下例所示:

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)客户端为WebSocket握手需要连接到的端点的HTTP URL。
  • /app开头的destination header的STOMP消息被路由到@Controller类中的@MessageMapping方法。
  • 使用内置的消息代理进行订阅和广播,并将destination header 以/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前缀没有任何特殊含义,它们只是区分发布-订阅和点对点消息传递(也就是说,许多订阅者和一个消费者)的一种约定,当你使用外部代理时,请检查代理的STOMP页面,以了解它支持哪种STOMP destination和前缀。

要从浏览器连接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 header,即使有,它们也会在服务器端被忽略(或者更确切地说,被覆盖),有关身份验证的更多信息,请参阅连接到代理和身份验证。

更多的示例代码:

消息流

一旦公开了STOMP端点,Spring应用程序就成为连接的客户端的STOMP代理,本节描述服务器端的消息流。

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

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

message-flow-simple-broker.png

前面的图显示了三个消息通道:

  • clientInboundChannel:传递从WebSocket客户端收到的消息
  • clientOutboundChannel:用于向WebSocket客户端发送服务器消息。
  • brokerChannel:用于从服务器端应用程序代码中向消息代理发送消息。

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

message-flow-broker-relay.png

前两个图的主要区别是使用“代理转播”将消息通过TCP传递到外部STOMP代理,并将消息从代理向下传递到订阅客户端。

当从WebSocket连接接收到消息时,它们被解码到STOMP帧,转换为Spring Message表示,并发送到clientInboundChannel进行进一步处理。例如,以/app开头的destination header的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;
    }

}

前面的示例支持以下流:

  1. 客户端连接到http://localhost:8080/portfolio,一旦建立了WebSocket连接,STOMP帧就开始在其上流动。
  2. 客户端发送带有/topic/greeting的destination header的SUBSCRIBE帧,接收和解码后,消息被发送到clientInboundChannel,然后被路由到消息代理,消息代理存储客户端订阅。
  3. 客户端向/app/greeting发送一个SEND帧,/app前缀有助于将其路由到带注解的控制器。去掉/app前缀后,destination剩余的/greeting部分映射到GreetingController中的@MessageMapping方法。
  4. GreetingController返回的值被转换为一个Spring Message,其payload基于返回值和/topic/greeting的默认destination header(由/app替换为/topic的输入destination派生而来)。
  5. 消息代理找到所有匹配的订阅者,并通过clientOutboundChannel向每个订阅者发送MESSAGE帧,消息从该通道编码为STOMP帧并在WebSocket连接上发送。

下一节将详细介绍带注解的方法,包括支持的参数和返回值。

带注解的控制器

应用程序可以使用带注解的@Controller类来处理来自客户端的消息,这些类可以声明@MessageMapping@SubscribeMapping@ExceptionHandler方法,如下面的主题所述:

@MessageMapping

你可以使用@MessageMapping来注解路由基于它们的destination消息的方法,在方法级别和类型级别都支持它。在类型级别,@MessageMapping用于表达控制器中所有方法之间的共享映射。

默认情况下,映射值是Ant风格的路径模式(例如/thing*/thing/**),包括对模板变量的支持(例如/thing/{id}),这些值可以通过@DestinationVariable方法参数引用,应用程序还可以为映射切换到点分隔的destination约定,点作为分隔符章节进行了解释。

支持的方法参数

下表描述了方法参数:

方法参数 描述
Message 用于访问完整的消息

发送消息

如果希望从应用程序的任何部分向连接的客户端发送消息,该怎么办?任何应用程序组件都可以向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)来限定它。


上一篇:SockJS Fallback

风继续吹
技术分享与交流

态度决定一切

2.4k 声望
1.4k 粉丝
0 条评论
推荐阅读
Activiti 用户指南(网关)
网关用于控制执行流程(或如BPMN 2.0所述,执行令牌),网关能够使用或生成令牌。网关以菱形图形显示,内部带有一个图标,该图标显示网关的类型。

博弈阅读 2.5k

我服了!SpringBoot升级后这服务我一个星期都没跑起来!(上)
最近由于各方面的原因在准备升级 Spring Cloud 和 Spring Boot,经过一系列前置的调研和分析,决定把Spring Boot 相关版本从 2.1.6 升级到 2.7.5,Spring Cloud 相关版本从 Greenwich.SR1 升级为 2021.0.4。

艾小仙2阅读 800

从源码层面深度剖析Spring循环依赖
作者:郭艳红以下举例皆针对单例模式讨论图解参考 [链接]1、Spring 如何创建Bean?对于单例Bean来说,在Spring容器整个生命周期内,有且只有一个对象。Spring 在创建 Bean 过程中,使用到了三级缓存,即 Default...

京东云开发者3阅读 565

封面图
SpringMVC-ResponseBodyAdvice
ResponseBodyAdvice接口可以在将handler方法的返回值写入response前对返回值进行处理,例如将返回值封装成一个与客户端约定好的对象以便于客户端处理响应数据。本篇文章将学习如果使用ResponseBodyAdvice以及其实...

半夏之沫2阅读 4.3k

Mybatis缓存机制
Mybatis内置了强大的事务性查询缓存机制,正确使用Mybatis的缓存机制可以有效提高应用的性能。因为一般情况下我们应用的大部分性能消耗都和数据库查询有关,如果能够有效命中缓存、适当避免或减少与数据库的交互...

2阅读 943

SpringMVC-@InitBinder
由@InitBinder注解修饰的方法用于初始化WebDataBinder对象,能够实现:从request获取到handler方法中由@RequestParam注解或@PathVariable注解修饰的参数后,假如获取到的参数类型与handler方法上的参数类型不匹配...

半夏之沫1阅读 2.8k

手把手带你开发starter,点对点带你讲解原理
现在我们就来回忆一下,在还没有Spring-boot框架的时候,我们使用Spring 开发项目,如果需要某一个框架,例如mybatis,我们的步骤一般都是:

京东云开发者2阅读 485

封面图

态度决定一切

2.4k 声望
1.4k 粉丝
宣传栏