Springboot + vue3 + vite 实现 Redis消息订阅 + WebSocket

一、 项目背景:

最近在做驿站的项目,前端展示设备列表,包括设备名称、编号、是否在线、开关状态。通过网页上的开关按钮来实现远程控制设备的开关功能。

  • 关于WebSocket
    WebSocket是一种在单个TCP连接上进行全双工通信的协议。WebSocket通信协议于2011年被IETF定为标准RFC 6455,并由RFC7936补充规范。WebSocket API也被W3C定为标准。
    WebSocket使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在WebSocket API中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。
    什么时候使用WebSocket
    延迟本身不是决定因素。如果消息量相对较低(例如,监视网络故障),HTTP流或轮询可以提供有效的解决方案。

低延迟、高频率和高容量的组合,是使用WebSocket的最佳选择。

WebSocket连接头

GET /spring-websocket-portfolio/portfolio HTTP/1.1
Host: localhost:8080
Upgrade: websocket 
Connection: Upgrade 
Sec-WebSocket-Key: Uc9l9TMkWGbHFD2qnFHltg==
Sec-WebSocket-Protocol: v10.stomp, v11.stomp
Sec-WebSocket-Version: 13
Origin: http://localhost:8080

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

HTTP/1.1 101 Switching Protocols 
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: 1qVdfYHU9hPOl4JYYNXF623Gzn0=
Sec-WebSocket-Protocol: v10.stomp

二、架构介绍:

后端采用SpringBoot + redis + stomp 实现跟设备的消息订阅。
前端采用:Vue3 +Vite + sockjs-client + stompjs

三、流程介绍:

后端设备服务将设备的所有信息都存到redis 里面,
web 服务取出所有设备最新信息展示到前台。点击开关按钮,将开关状态发送到后台controller, 在controller 里执行redis 主题发布,设备收到订阅主题后,执行相关的动作,执行完成后给予WEB服务反馈。web 服务收到反馈后将相应的状态信息通过 websocket 发布到主题里面,前端页面收到订阅消息后执行相应的状态同步

  1. 后台通过redisTemplate.convertAndSend 发布redis订阅主题,
    通过redis 配置监听和handler来处理接收到的消息。
  2. 通过配置 WebSocket stomp 来配置前端页面的订阅endpoint,广播、点对点发布等。
    通过SimpMessagingTemplate.convertAndSend 实现后台WEBSocket 订阅消息发布。

四、配置过程:

1.后端配置:

1.1 配置redis 订阅消息类:实现通过redis 订阅消息的功能

RedisSubConfig.java

package com.dechnic.waystation.config;

import com.dechnic.waystation.service.handler.RedisMessageHandler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Scope;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.listener.ChannelTopic;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.data.redis.listener.adapter.MessageListenerAdapter;

import javax.annotation.Resource;

/**
 * @description:
 * @author:houqd
 * @time: 2022/5/27 15:27
 */
@Configuration
public class RedisSubConfig {
   
    
    
    @Resource
    RedisMessageHandler redisReceiver;

    private static class  ContainerHolder {
   
    
    
        private static RedisMessageListenerContainer container = new RedisMessageListenerContainer();
        public static RedisMessageListenerContainer getInstance(){
   
    
    
            return container;
        }
    }



    /**
     * redis 消息监听容器
     *
     * @param connectionFactory
     * @param ctrlRetryListener
     * @return
     */
    @Bean
    RedisMessageListenerContainer container(RedisConnectionFactory connectionFactory, MessageListenerAdapter ctrlRetryListener, MessageListenerAdapter realDataListener) {
   
    
    
//        RedisMessageListenerContainer container = new RedisMessageListenerContainer();
        RedisMessageListenerContainer container = ContainerHolder.getInstance();
        container.setConnectionFactory(connectionFactory);
        container.addMessageListener(ctrlRetryListener, new ChannelTopic(CustomConfig.REDIS_CHANNEL_CTRL_RETRY));
        container.addMessageListener(realDataListener, new ChannelTopic(CustomConfig.REDIS_CHANNEL_DATA));
        return container;
    }

    @Bean("ctrlRetryListener")
    MessageListenerAdapter ctrlRetryListener() {
   
    
    
        return new MessageListenerAdapter(redisReceiver, "ctrlRetryMessage");
    }

    @Bean("realDataListener")
    MessageListenerAdapter realDataListener() {
   
    
    
        return new MessageListenerAdapter(redisReceiver, "realDataMessage");
    }


}

1.2 redis 消息处理类 RedisMessageHandler.java

实现接收到redis 订阅主题后,具体业务逻辑的处理

package com.dechnic.waystation.service.handler;

import com.dechnic.waystation.domain.VDeviceInfo;
import com.dechnic.waystation.model.CtrlMsg;
import com.dechnic.waystation.model.CtrlRetryMsg;
import com.dechnic.waystation.service.IVDeviceInfoService;
import com.dechnic.waystation.util.MapperUtil;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.enums.DeviceType;
import com.ruoyi.common.props.WebSocketProps;
import com.ruoyi.framework.aspectj.DataScopeAspect;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.DependsOn;
import org.springframework.context.annotation.Import;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.stereotype.Service;

import javax.annotation.PostConstruct;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.stream.Collectors;

/**
 * @description: 接收订阅消息
 * @author:houqd
 * @time: 2022/5/27 15:42
 */
@Slf4j
@Service
public class RedisMessageHandler {
   
    
    
    @Autowired
    SimpMessagingTemplate simpMessagingTemplate;
    @Autowired
    WebSocketProps webSocketProps;
    private IVDeviceInfoService vDeviceInfoService;

    @Autowired
    private void setvDeviceInfoService(IVDeviceInfoService vDeviceInfoService){
   
    
    
        this.vDeviceInfoService = vDeviceInfoService;
    }


    private  List<VDeviceInfo> vDeviceInfoList = null;
    private  List<String> deviceCodeList=null;


//    @PostConstruct
//    public void init(){
   
    
    
//         vDeviceInfoList = vDeviceInfoService.selectVDeviceInfoList(null);
//         if (vDeviceInfoList!=null && !vDeviceInfoList.isEmpty()){
   
    
    
//             deviceCodeList = vDeviceInfoList.stream().map(vDeviceInfo -> vDeviceInfo.getDeviceCode()).collect(Collectors.toList());
//         }
//    }


    /**
     * 控制反馈
     * @param message
     */
    public  void ctrlRetryMessage(String message){
   
    
    
        log.debug("设备反馈:"+message);
        CtrlRetryMsg ctrlRetryMsg = MapperUtil.jsonToObject(message, CtrlRetryMsg.class);
       if (ctrlRetryMsg.getDevType().equals(DeviceType.ACS.name())){
   
    
    // 门禁
           simpMessagingTemplate.convertAndSend(webSocketProps.getAcsTopic(), ctrlRetryMsg);
       }else if (ctrlRetryMsg.getDevType().equals(DeviceType.AIR.name())){
   
    
    // 空调
           simpMessagingTemplate.convertAndSend(webSocketProps.getAirTopic(), ctrlRetryMsg);
       }else if (ctrlRetryMsg.getDevType().equals(DeviceType.LIGHT.name())){
   
    
    // 灯
           simpMessagingTemplate.convertAndSend(webSocketProps.getLightTopic(), ctrlRetryMsg);
       }

    }

    public void realDataMessage(String message){
   
    
    
        log.debug("实时数据:"+message);
        LinkedHashMap resultMap = MapperUtil.jsonToObject(message, LinkedHashMap.class);
        String deviceCode = (String) resultMap.get("deviceCode");
        String deviceType = (String) resultMap.get("deviceType");
        if (deviceType!=null&&deviceType.equals(DeviceType.AIR.name())){
   
    
    
            // 空调设备
            simpMessagingTemplate.convertAndSend(webSocketProps.getAirRealDataTopic(),resultMap);
        }

    }


}


1.3 WebSocketConfig 配置类WebSocketConfig.java

package com.dechnic.waystation.config;

import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;

/**
 * @description:
 * @author:houqd
 * @time: 2022/5/28 16:22
 */
@Slf4j
@Configuration
//注解开启使用STOMP协议来传输基于代理(message broker)的消息,这时控制器支持使用@MessageMapping,就像使用@RequestMapping一样
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
   
    
    
    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
   
    
    
        registry.addEndpoint("/webSocket")//注册为STOMP的端点
                .setAllowedOriginPatterns("*")//可以跨域
                .withSockJS();//支持sockJs
    }

    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
   
    
    
        // 设置广播节点
        registry.enableSimpleBroker("/topic");
//        // 客户端向服务端发送消息需有/app 前缀
//        registry.setApplicationDestinationPrefixes("/app");
//        // 指定用户发送(一对一)的前缀 /user/
//        registry.setUserDestinationPrefix("/user");
    }
}

解释:

使用@EnableWebSocketMessageBroker开启WebSocket的子协议STOMP,配置类需要实现WebSocketMessageBrokerConfigurer接口,重写其中的注册STOMP节点方法和配置信息代理者方法

在注册STOMP节点方法中我们需要:

添加监听节点addEndpoint 设置跨域setAllowedOriginPatterns
设置使用SockJSwithSockJS(你也可以选择使用原生方式) 配置信息代理者中需要:

设置目的地前缀setApplicationDestinationPrefixes 设置代理者(代理者对应订阅者)

1.4 SecurityConfig 放开 webSocket 相关资源访问权限

.antMatchers("/login","/webSocket/**").permitAll()

完整代码:

package com.dechnic.oms.framework.config;

import com.dechnic.oms.framework.security.filter.JwtAuthenticationTokenFilter;
import com.dechnic.oms.framework.security.filter.OpenApiFilter;
import com.dechnic.oms.framework.security.handler.AuthenticationEntryPointImpl;
import com.dechnic.oms.framework.security.handler.LogoutSuccessHandlerImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.method.configuration.

猜你喜欢

转载自blog.csdn.net/u014212540/article/details/125070118#comments_26602728