最近在做驿站的项目,前端展示设备列表,包括设备名称、编号、是否在线、开关状态。通过网页上的开关按钮来实现远程控制设备的开关功能。
低延迟、高频率和高容量的组合,是使用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 发布到主题里面,前端页面收到订阅消息后执行相应的状态同步
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");
}
}
实现接收到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);
}
}
}
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 设置代理者(代理者对应订阅者)
.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.