1. 后台实现
- 后台架构基于若依源码搭建。
- 网关配置,路由spring.cloud.gateway.routes,以及白名单
- 添加全局过滤器
public class WebSocketFilter implements GlobalFilter, Ordered {
public final static String DEFAULT_FILTER_PATH = "/bdnj-ws/info";
public final static String DEFAULT_FILTER_WEBSOCKET = "websocket";
/**
*
* @param exchange ServerWebExchange是一个HTTP请求-响应交互的契约。提供对HTTP请求和响应的访问,
* 并公开额外的 服务器 端处理相关属性和特性,如请求属性
* @param chain
* @return
*/
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String upgrade = exchange.getRequest().getHeaders().getUpgrade();
URI requestUrl = exchange.getRequiredAttribute(GATEWAY_REQUEST_URL_ATTR);
String scheme = requestUrl.getScheme();
//如果不是ws的请求直接通过
if (!"ws".equals(scheme) && !"wss".equals(scheme)) {
return chain.filter(exchange);
//如果是/ws/info的请求,把它还原成http请求。
} else if (DEFAULT_FILTER_PATH.equals(requestUrl.getPath())) {
String wsScheme = convertWsToHttp(scheme);
URI wsRequestUrl = UriComponentsBuilder.fromUri(requestUrl).scheme(wsScheme).build().toUri();
exchange.getAttributes().put(GATEWAY_REQUEST_URL_ATTR, wsRequestUrl);
//如果是sockJS降级后的http请求,把它还原成http请求,也就是地址{transport}不为websocket的所有请求
} else if (requestUrl.getPath().indexOf(DEFAULT_FILTER_WEBSOCKET)<0) {
String wsScheme = convertWsToHttp(scheme);
URI wsRequestUrl = UriComponentsBuilder.fromUri(requestUrl).scheme(wsScheme).build().toUri();
exchange.getAttributes().put(GATEWAY_REQUEST_URL_ATTR, wsRequestUrl);
}
return chain.filter(exchange);
}
@Override
public int getOrder() {
return Ordered.LOWEST_PRECEDENCE - 2;
}
static String convertWsToHttp(String scheme) {
scheme = scheme.toLowerCase();
return "ws".equals(scheme) ? "http" : "wss".equals(scheme) ? "https" : scheme;
}
}
- 配置 WebSocketConfig
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Autowired
private WebSocketInterceptor authChannelInterceptor;
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
System.out.println("开始websocket配置。。。。。。。。。。。。。");
registry.addEndpoint("/bdnj-ws") //开启/bullet端点
// .setAllowedOrigins("*") //允许跨域访问
.setAllowedOriginPatterns("*")
.withSockJS(); //使用sockJS
registry.addEndpoint("/bdnj-ws-app") //开启/bullet端点
// .setAllowedOrigins("*") //允许跨域访问
.setAllowedOriginPatterns("*");
}
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
//设置两个频道,topic用于广播,queue用于点对点发送
registry.enableSimpleBroker("/topic/", "/queue/");
//设置应用目的地前缀
registry.setApplicationDestinationPrefixes("/app");
//设置用户目的地前缀
registry.setUserDestinationPrefix("/user");
}
@Override
public void configureClientInboundChannel(ChannelRegistration registration) {
registration.interceptors(authChannelInterceptor);
}
//这个是为了解决和调度任务的冲突重写的bean
@Primary
@Bean
public TaskScheduler taskScheduler(){
ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler();
taskScheduler.setPoolSize(10);
taskScheduler.initialize();
return taskScheduler;
}
}
- 监听处理 IWebSocketManager
public interface IWebSocketManager {
boolean isOnline(String username);
void addUser(StompHeaderAccessor accessor);
void deleteUser(StompHeaderAccessor accessor);
}
- 监听 WebSocketInterceptor
@Component
@Order(Ordered.HIGHEST_PRECEDENCE + 99)
public class WebSocketInterceptor implements ChannelInterceptor {
@Autowired
private IWebSocketManager webSocketManager;
@Autowired
private RedisService redisService;
/**
* 连接前监听
*
* @param message
* @param channel
* @return
*/
@Override
public Message<?> preSend(Message<?> message, MessageChannel channel) {
StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
//1、判断是否首次连接
if (accessor != null && StompCommand.CONNECT.equals(accessor.getCommand())) {
//2、判断token
List<String> nativeHeader = accessor.getNativeHeader(TokenConstants.AUTHENTICATION);
if (nativeHeader != null && !nativeHeader.isEmpty()) {
String token = nativeHeader.get(0);
if (StringUtils.isNotEmpty(token) && token.startsWith(TokenConstants.PREFIX))
{
token = token.replaceFirst(TokenConstants.PREFIX, StringUtils.EMPTY);
}
if (StringUtils.isNotBlank(token)) {
Claims claims =null;
try {
claims = JwtUtils.parseToken(token);
}catch (Exception e){
e.printStackTrace();
return null;
}
if (claims == null)
{
return null;
}
String userkey = JwtUtils.getUserKey(claims);
boolean islogin = redisService.hasKey(getTokenKey(userkey));
if (!islogin)
{
return null;
}
String userid = JwtUtils.getUserId(claims);
String username = JwtUtils.getUserName(claims);
String userstatus = JwtUtils.getUserStatus(claims);
Principal principal = new Principal() {
@Override
public String getName() {
return userid+"_"+ accessor.getSessionId();
}
};
accessor.setUser(principal);
webSocketManager.addUser(accessor);
System.out.println("终端上线:"+principal.getName());
return message;
}
return message;
}
return null;
}
//不是首次连接,已经登陆成功
return message;
}
// 在消息发送后立刻调用,boolean值参数表示该调用的返回值
@Override
public void postSend(Message<?> message, MessageChannel channel, boolean sent) {
StompHeaderAccessor accessor = StompHeaderAccessor.wrap(message);
Principal principal = accessor.getUser();
// 忽略心跳消息等非STOMP消息
if(accessor.getCommand() == null)
{
return;
}
switch (accessor.getCommand())
{
// 首次连接
case CONNECT:
break;
// 连接中
case CONNECTED:
break;
// 下线
case DISCONNECT:
if(principal!=null){
System.out.println("终端下线:"+principal.getName());
webSocketManager.deleteUser(accessor);
}
break;
default:
break;
}
}
private String getTokenKey(String token)
{
return CacheConstants.LOGIN_TOKEN_KEY + token;
}
}
- WebSocketManager
@Component
public class WebSocketManager implements IWebSocketManager{
private ThreadPoolTaskScheduler taskScheduler;
private Long onlineCount;
private CopyOnWriteArraySet<String> onlines;
private static final Integer POOL_MIN = 10;
@PostConstruct
public void init() {
taskScheduler = new ThreadPoolTaskScheduler();
taskScheduler.setPoolSize(POOL_MIN);
taskScheduler.initialize();
this.onlines = new CopyOnWriteArraySet<>();
this.onlineCount = 0L;
}
@Override
public boolean isOnline(String username) {
return onlines.contains(username);
}
@Override
public void addUser(StompHeaderAccessor accessor) {
onlines.add(accessor.getUser().getName());
onlineCount = Long.valueOf(onlines.size());
}
@Override
public void deleteUser(StompHeaderAccessor accessor) {
onlines.remove(accessor.getUser().getName());
onlineCount = Long.valueOf(onlines.size());
}
}
- 消息接收和发送
@MessageMapping("/datapoint")
@SendToUser("/queue/datavalue")
public AjaxResult datapoint(Principal principal, @Payload String message) {
return AjaxResult.success(message);
}
@MessageMapping("/dataTopic")
@SendTo("/topic/dataTopic")
public AjaxResult dataTopic(Principal principal, @Payload String message) {
return AjaxResult.success(message);
}
2. VUE前端实现
- websocket.js
import SockJS from 'sockjs-client';
import {Client} from '@stomp/stompjs';
import {getToken} from '@/utils/auth'
const socket = (param) => {
//请求的起始地址,根据开发环境变量确定
let baseUrl = process.env.VUE_APP_BASE_API;
if(param == null){
param = {};
}
param["Authorization"] = 'Bearer ' + getToken()
let stompClient = new Client({
//可以不赋值,因为后面用SockJS来代替
//brokerURL: 'ws://localhost:9527/dev-api/ws/',
//获得客户端token的方法,把token放到请求头中
connectHeaders: param,
debug: function (str) {
//debug日志,调试时候开启
console.log(str);
},
reconnectDelay: 10000,//重连时间
heartbeatIncoming: 4000,
heartbeatOutgoing: 4000,
});
// //用SockJS代替brokenURL
stompClient.webSocketFactory = function () {
//因为服务端监听的是/ws路径下面的请求,所以跟服务端保持一致
return new SockJS(baseUrl + '/bdnj-ws', null, {
timeout: 10000
});
};
return {
stompClient: stompClient,
connect(callback) {
//连接
stompClient.onConnect = (frame) => {
callback(frame);
};
//错误
stompClient.onStompError = function (frame) {
console.log('Broker reported error: ' + frame.headers['message']);
console.log('Additional details: ' + frame.body);
//这里不需要重连了,新版自带重连
};
//启动
stompClient.activate();
},
close() {
if (this.stompClient !== null) {
this.stompClient.deactivate()
}
},
//发送消息
send(addr, msg) {
//添加app的前缀,并发送消息,publish是新版的stomp/stompjs发送api,老版本更改下就可以。
this.stompClient.publish({
destination: '/app'+addr,
body: msg
})
},
//订阅消息
subscribe(addr, callback) {
this.stompClient.subscribe(addr, (res)=>{
//这里进行了JSON类型的转化,因为我的服务端返回的数据都是json,消息本身是string型的,所以进行了转化。
var result = JSON.parse(res.body);
callback(result);
});
}
}
}
export default socket
- 使用
import Websocket from '@/utils/websocket'
this.socket1 = new Websocket({terminalIds:"1000000022462"});
this.socket1.connect((form) => {
this.socket1.subscribe("/user/queue/datavalue", (res) => {
console.log("socket1接收数据:" + res)
this.showResponse("socket1接收数据:" + JSON.stringify(res));
});
this.socket1.subscribe("/topic/dataTopic", (res) => {
console.log("socket1接收数据topic:" + res)
this.showResponse("socket1接收数据topic:" + JSON.stringify(res));
});
})
//发送消息
this.socket1.send("/datapoint", "这是消息");
3. uniapp使用
- websocket-uni.js
//参考 https://blog.csdn.net/W_H_M_S/article/details/121784241
let socketOpen = false;
let socketMsgQueue = [];
export default {
client: null,
baseURL: `ws://127.0.0.1:18080/bdnj-ws-app`, //uni-app使用时不能使用http不然监听不到,需要使用ws
init(headers) {
if (this.client) {
return Promise.resolve(this.client);
}
return new Promise((resolve, reject) => {
const ws = {
send: this.sendMessage,
onopen: null,
onmessage: null,
close: this.closeSocket,
};
uni.connectSocket({
url: this.baseURL,
header: headers,
success: function() {
console.log("WebSocket连接成功");
}
});
uni.onSocketOpen(function(res) {
console.log('WebSocket连接已打开!', res);
socketOpen = true;
for (let i = 0; i < socketMsgQueue.length; i++) {
ws.send(socketMsgQueue[i]);
}
socketMsgQueue = [];
ws.onopen && ws.onopen();
});
uni.onSocketMessage(function(res) {
console.log("回馈")
ws.onmessage && ws.onmessage(res);
});
uni.onSocketError(function(res) {
console.log('WebSocket 错误!', res);
});
uni.onSocketClose((res) => {
this.client.disconnect();
this.client = null;
socketOpen = false;
console.log('WebSocket 已关闭!', res);
});
const Stomp = require('./stomp.js').Stomp;
Stomp.setInterval = function(interval, f) {
return setInterval(f, interval);
};
Stomp.clearInterval = function(id) {
return clearInterval(id);
};
const client = this.client = Stomp.over(ws);
client.connect(headers, function() {
console.log('stomp connected');
resolve(client);
});
});
},
disconnect() {
uni.closeSocket();
},
sendMessage(message) {
if (socketOpen) {
uni.sendSocketMessage({
data: message,
});
} else {
socketMsgQueue.push(message);
}
},
closeSocket() {
console.log('closeSocket');
},
};
- 使用方法
import WebSocket from '@/components/socket/websocket-uni';
WebSocket.init({
"Authorization":res.data.access_token
}).then(client => {
//接收反馈端口,成功方法,错误方法
client.subscribe('/topic/getResponse', function(msg){
console.log(msg)
}, function(msg){
console.log(msg)
});
});