Springboot + vue3 + vite implements Redis message subscription + WebSocket

1. Project background:

Recently, I am working on a station project, and the front end displays a list of devices, including device names, numbers, whether they are online, and switch status. The switch function of the remote control device is realized through the switch button on the webpage.

  • About WebSocket
    WebSocket is a protocol for full-duplex communication over a single TCP connection. The WebSocket communication protocol was defined as the standard RFC 6455 by the IETF in 2011, and was supplemented by RFC7936. The WebSocket API is also standardized by the W3C.
    WebSocket makes the data exchange between the client and the server easier, allowing the server to actively push data to the client. In the WebSocket API, the browser and the server only need to complete a handshake, and a persistent connection can be created directly between the two, and two-way data transmission can be performed.
    When to use WebSocket
    Latency itself is not the deciding factor. If the volume of messages is relatively low (for example, monitoring network failures), HTTP streaming or polling can provide an effective solution.

The combination of low latency, high frequency, and high capacity is the best choice for using WebSocket.

WebSocket connection header

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

Server returns status code
Servers with WebSocket support return output similar to the following instead of the usual 200 status code

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

2. Architecture introduction:

The backend uses SpringBoot + redis + stomp to implement message subscription with the device.
Front-end uses: Vue3 +Vite + sockjs-client + stompjs

3. Process introduction:

The back-end device service stores all the information of the device in redis, and
the web service retrieves the latest information of all devices and displays it to the foreground. Click the switch button to send the switch status to the background controller, execute the redis topic publishing in the controller, after the device receives the subscription topic, execute the relevant actions, and give the WEB service feedback after the execution is completed. After receiving the feedback, the web service publishes the corresponding state information to the topic through websocket, and the front-end page performs corresponding state synchronization after receiving the subscription message

  1. The background publishes redis subscription topics through redisTemplate.convertAndSend, and
    configures listeners and handlers through redis to process received messages.
  2. Configure WebSocket stomp to configure the subscription endpoint of the front-end page, broadcast, point-to-point publishing, etc.
    Use SimpMessagingTemplate.convertAndSend to realize background WEBSocket subscription message publishing.

Fourth, the configuration process:

1. Backend configuration:

1.1 Configure redis subscription message class: realize the function of subscribing to messages through 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 message processing class RedisMessageHandler.java

Realize the processing of specific business logic after receiving the redis subscription topic

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

explain:

Use @EnableWebSocketMessageBroker to enable the WebSocket sub-protocol STOMP, the configuration class needs to implement the WebSocketMessageBrokerConfigurer interface, and rewrite the registration STOMP node method and configuration information agent method

In the register STOMP node method we need:

Add the listener node addEndpoint Set the cross-domain setAllowedOriginPatterns
setting Use SockJSwithSockJS (you can also choose to use the native method) Required in the configuration information agent:

Set the destination prefix setApplicationDestinationPrefixes Set the proxy (the proxy corresponds to the subscriber)

1.4 SecurityConfig releases access to webSocket-related resources

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

Full code:

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.

Guess you like

Origin blog.csdn.net/u014212540/article/details/125070118#comments_26602728