Spring stomp 服务端处理取消订阅事件

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;

/**
 * stomp 控制类
 * @author yanyulin
 * @date 2023-1-11 13:45:25
 **/
@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;
    }

    /**
     * 订阅
     * @param topic
     */
    @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);
    }

    /**
     * 心跳机制保活,记录心跳发送来的秒数(如果1分钟没有心跳,取消所有订阅)
     */
    @MessageMapping("/heartBeat")
    public void heartBeatMapping() {
        log.info(">>>>>>用户发来心跳,时间:{}", LocalDateTime.now());
        isActive = System.currentTimeMillis()/1000;
    }

    /**
     * 监听订阅事件,将id写入内存
     * @param event
     */
    @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());
    }

    /**
     * 监听取消订阅事件,前端取消订阅时,向盒子发请求取消订阅
     * @param event
     */
    @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;

/**
 * 订阅保持定时任务
 * @author yanyulin
 * @date 2023-1-11 16:53:00
 */
@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;
    }

    /**
     * 1分钟执行一次,如果1分钟没有心跳,取消所有订阅
     */
    @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);
        }
    }
}

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

坚持是一种态度

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值