Spring STOMP介绍
- STOMP(Simple Text Oriented Messaging Protocol,简单文本消息传输协议)是一个基于文本的协议,用于在客户端和消息队列之间进行异步通信。STOMP被设计用于在Web应用程序中传输异步消息,支持多种编程语言和消息代理系统
- Spring STOMP(Simple Text Oriented Messaging Protocol)是基于文本的轻量级消息传递协议,用于建立WebSocket的子协议。
- Spring STOMP提供了一种可靠和高效的方式来在Web应用程序之间传递消息。它可以用于实现各种消息传递应用,包括聊天应用、实时更新、通知系统和多人游戏等。与传统的HTTP轮询相比,使用STOMP协议可以减少传输数据的量、降低服务器压力和提高实时性。
- Spring STOMP提供了两个核心的组件:STOMP消息代理和STOMP客户端。STOMP消息代理是一个中间件,它接收、路由和转发消息。STOMP客户端是一个Web浏览器或Java应用程序,它可以连接到消息代理并发送或接收消息。
- 另外,Spring STOMP还提供了一些内置的注解和类,帮助开发人员快速构建消息传递应用。通过使用这些注解和类,开发人员可以快速开发出高效、可扩展和易于维护的WebSocket应用程序
应用场景
- 讲解下我们的应用场景,平台程序作为硬件产品(边缘盒子)的上位机,可以单独部署到台式机或者笔记本上
- 在需要时把平台程序启动起来,就可以在浏览器访问操作界面,配置上硬件产品(边缘盒子)的IP、端口等信息,就可以与硬件通信,进行设置的修改和数据的接收展示
- 按照约定的通信协议,平台软件与硬件产品使用
socket
进行通信(其实这个程序也至于硬件通信,存储直接读写文件,也不需要数据库等中间件) - 首先需要平台发送订阅请求到硬件产品,它才会把一些过车数据等实时数据上报到平台(平台接收、解析、处理、展示)
- 同样的,取消订阅,也需要平台发送请求给硬件产品,才能取消订阅,硬件产品才会停止向平台发送消息
- 除了平台程序与硬件产品的通信,我们还需要解决浏览器前端页面与服务端代码的通信,在前端首先就直接使用websocket了。由于我们平台程序使用Java语言编写,服务端直接使用 Spring STOMP。
- 前端界面发起订阅和取消订阅时,本来不需要服务端监听处理,服务端自动就会生效。但是由于我们服务端需要继续向硬件产品发送订阅/取消订阅请求,就必须要监听了再执行自己的业务了
- spring 集成 stomp,发送消息,进行前后端websocket通信,服务端通过Maven引入相关jar包
- 前端引入相关js,进行订阅和取消订阅
- 下面主要讲一下服务端代码实现
相关代码
- 建立一个 stomp 控制类,接收订阅请求,监听事件
- 使用
@SubscribeMapping
接收订阅请求 - 使用
@MessageMapping
接收前端send请求,此处用于接收前端定时主动发起的心跳请求 - 使用
@EventListener
注解监听stomp事件,此处我暂时只监听订阅/取消订阅两个事件 - 使用
handleSessionSubscribeEvent(SessionSubscribeEvent event)
监听订阅 - 使用
handleSessionUnsubscribeEvent(SessionUnsubscribeEvent event)
监听取消订阅。这个方法有@EventListener
注解,是核心方法,当有客户端取消订阅时,该方法被自动调用
package com.cy.controller;
import com.alibaba.fastjson.JSON;
import com.cy.config.DataConfig;
import com.cy.service.UpperComputerService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.event.EventListener;
import org.springframework.messaging.MessageHeaders;
import org.springframework.messaging.handler.annotation.DestinationVariable;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.simp.annotation.SubscribeMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.socket.messaging.SessionSubscribeEvent;
import org.springframework.web.socket.messaging.SessionUnsubscribeEvent;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;
@RestController
public class StompSocketHandler {
public static Long isActive = 0L;
public static Map<String, String> subscribeTopics = new HashMap<>();
private static final Logger log = LoggerFactory.getLogger(StompSocketHandler.class);
private final UpperComputerService upperComputerService;
public StompSocketHandler(UpperComputerService upperComputerService) {
this.upperComputerService = upperComputerService;
}
@SubscribeMapping("{topic}")
public void subscribeMapping(@DestinationVariable("topic") final String topic) {
int equipmentNo = 0;
if (DataConfig.thresholdList.size() > 0) {
equipmentNo = DataConfig.thresholdList.get(0).getEquipmentNo();
}
int action = 1;
log.info(">>>>>>用户已订阅,equipmentNo={}, topic={}, action={}", equipmentNo, topic, action);
upperComputerService.subscribe(equipmentNo, topic, action);
}
@MessageMapping("/heartBeat")
public void heartBeatMapping() {
log.info(">>>>>>用户发来心跳,时间:{}", LocalDateTime.now());
isActive = System.currentTimeMillis()/1000;
}
@EventListener
private void handleSessionSubscribeEvent(SessionSubscribeEvent event) {
log.info("Subscribe event : {}", JSON.toJSONString(event));
MessageHeaders headers = event.getMessage().getHeaders();
subscribeTopics.put(event.getMessage().getHeaders().get("simpSubscriptionId").toString(), headers.get("simpDestination").toString());
}
@EventListener
private void handleSessionUnsubscribeEvent(SessionUnsubscribeEvent event) {
log.info("Unsubscribe event : {}", JSON.toJSONString(event));
int equipmentNo = 0;
if (DataConfig.thresholdList.size() > 0) {
equipmentNo = DataConfig.thresholdList.get(0).getEquipmentNo();
}
int action = 0;
String topic = subscribeTopics.get(event.getMessage().getHeaders().get("simpSubscriptionId").toString());
log.info(">>>>>>用户已取消订阅,equipmentNo={}, topic={}, action={}", equipmentNo, topic, action);
upperComputerService.subscribe(equipmentNo, topic, action);
}
}
- 我们的业务场景是一个或少量用户使用,所以当浏览器关闭后,心跳请求会结束一段时间后,服务端要能取消订阅
- 增加一个定时任务,监听心跳请求,当1分钟没接收到心跳请求时,需要把所有已订阅的取消掉
package com.cy.config;
import com.cy.controller.StompSocketHandler;
import com.cy.service.UpperComputerService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.time.Instant;
import java.util.HashSet;
import java.util.Set;
@Component
@EnableScheduling
public class SubscribeHoldTask {
private static final Logger log = LoggerFactory.getLogger(SubscribeHoldTask.class);
private final UpperComputerService upperComputerService;
public SubscribeHoldTask(UpperComputerService upperComputerService) {
this.upperComputerService = upperComputerService;
}
@Scheduled(cron = "0 0/1 * * * ?")
public void calculate() {
log.info("----{}----订阅保持定时任务, 1分钟执行一次,如果1分钟没有心跳,取消所有订阅", Instant.now());
try {
long now = System.currentTimeMillis()/1000;
long isActive = StompSocketHandler.isActive;
if (now - isActive > 60) {
Set<String> topics = new HashSet<>();
StompSocketHandler.subscribeTopics.forEach((k, v) -> topics.add(v));
if (topics.size() > 0) {
int equipmentNo = 0;
if (DataConfig.thresholdList.size() > 0) {
equipmentNo = DataConfig.thresholdList.get(0).getEquipmentNo();
}
int action = 0;
for (String topic : topics) {
upperComputerService.subscribe(equipmentNo, topic, action);
log.info(">>>>>>断开连接,取消订阅,equipmentNo={}, topic={}", equipmentNo, topic);
}
}
StompSocketHandler.subscribeTopics.clear();
}
} catch (Exception e) {
log.error("SubscribeHoldTask::calculate() error..", e);
}
}
}