SpringCloud+gateway+uniapp+stomp的websocket实现

1,126 阅读3分钟

1. 后台实现

  • 后台架构基于若依源码搭建。
  • 网关配置,路由spring.cloud.gateway.routes,以及白名单

image.png

image.png

  • 添加全局过滤器
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)
    });
});