RuoYi-Vue(SpringBoot+Vue+Socket.IO) 实现消息推送功能

所需依赖如下:

前端:Vue2 + socket.io-client 4.7.2 + vue-socket.io 3.0.10

后端:SpringBoot + netty-socketio 2.0.3

1.导入socket.io配置

在 ruoyi-admin 模块下的 application.yml 加入socketio配置

读取配置并初始化Socket.IO

在 ruoyi-framework 模块下新建 socket 

 

在socket包下新建配置文件 SocketIoConfig

package com.ruoyi.framework.socket;

import com.corundumstudio.socketio.SocketConfig;
import com.corundumstudio.socketio.SocketIOServer;
import com.corundumstudio.socketio.annotation.SpringAnnotationScanner;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;

import java.util.Arrays;
import java.util.Optional;

/**
 * socket.io配置文件
 */
@Component
public class SocketIoConfig {

    @Value("${socketio.host}")
    private String host;

    @Value("${socketio.port}")
    private Integer port;

    @Value("${socketio.bossCount}")
    private int bossCount;

    @Value("${socketio.workCount}")
    private int workCount;

    @Value("${socketio.allowCustomRequests}")
    private boolean allowCustomRequests;

    @Value("${socketio.upgradeTimeout}")
    private int upgradeTimeout;

    @Value("${socketio.pingTimeout}")
    private int pingTimeout;

    @Value("${socketio.pingInterval}")
    private int pingInterval;

    @Bean
    public SocketIOServer socketIOServer() {
        SocketConfig socketConfig = new SocketConfig();
        socketConfig.setTcpNoDelay(true);
        socketConfig.setSoLinger(0);
        com.corundumstudio.socketio.Configuration config = new com.corundumstudio.socketio.Configuration();
        config.setSocketConfig(socketConfig);
        config.setHostname(host);
        config.setPort(port);
        config.setBossThreads(bossCount);
        config.setWorkerThreads(workCount);
        config.setAllowCustomRequests(allowCustomRequests);
        config.setUpgradeTimeout(upgradeTimeout);
        config.setPingTimeout(pingTimeout);
        config.setPingInterval(pingInterval);

        //服务端
        final SocketIOServer server = new SocketIOServer(config);


        return server;
    }

    //这个对象是用来扫描socketio的注解,比如 @OnConnect、@OnEvent
    @Bean
    public SpringAnnotationScanner springAnnotationScanner() {

        return new SpringAnnotationScanner(socketIOServer());
    }

}

 2.编写消息推送逻辑并启动服务

 在socket包下新建接口 SocketIOService 以及实现类 SocketIOServiceImpl

package com.ruoyi.framework.socket;

public interface SocketIOService {
    // 启动服务
    void start() throws Exception;

    // 停止服务
    void stop();
}

package com.ruoyi.framework.socket;

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;

import com.ruoyi.common.core.domain.entity.SysUser;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.corundumstudio.socketio.SocketIOClient;
import com.corundumstudio.socketio.SocketIOServer;

/**
 * SocketIO 服务实现
 * @author jokerpan
 */
@Service(value = "socketIOService")
public class SocketIOServiceImpl implements SocketIOService {
    private static final Logger log = LoggerFactory.getLogger(SocketIOServiceImpl.class);

    // 用来存已连接的客户端
    private static Map<String, SocketIOClient> clientMap = new ConcurrentHashMap<>();

    // 保存用户自定义事件名称
    private static String SAVE_USER_EVENT = "save"; //保存用户
    private static String CLEAR_USER_EVENT = "clear"; //清除用户

    private static String BROADCAST = "broadcast"; //广播
    private static String SEND_MESSAGE_EVENT = "sendMessage"; //发送消息

    @Autowired
    private SocketIOServer socketIOServer;

    /**
     * Spring IOC容器创建之后,在加载SocketIOServiceImpl Bean之后启动
     * @throws Exception
     */
    @PostConstruct
    private void autoStartup() throws Exception {
        start();
    }

    /**
     * Spring IOC容器在销毁SocketIOServiceImpl Bean之前关闭,避免重启项目服务端口占用问题
     * @throws Exception
     */
    @PreDestroy
    private void autoStop() throws Exception  {
        stop();
    }

    @Override
    public void start() {

        // 监听客户端连接
        socketIOServer.addConnectListener(client -> {
            log.info("======socket连接成功!======");
        });

        // 监听客户端断开连接
        socketIOServer.addDisconnectListener(client -> {
            log.info("======socket连接关闭!======");
        });

        /**
         * 监听客户端自定义事件
         */
        //保存用户
        socketIOServer.addEventListener(SAVE_USER_EVENT, String.class, (client, data, ackSender) -> {
            // 客户端推送`自定义`事件时,onData接受数据,这里是string类型的json数据,还可以为Byte[],object其他类型
            String userId = data;
            clientMap.put(userId, client);
            log.debug("------ID为:【" + userId + "】的用户加入连接------");
        });

        //清除用户
        socketIOServer.addEventListener(CLEAR_USER_EVENT, String.class, (client, data, ackSender) -> {
            String userId = data;
            if (clientMap.get(userId) != null) clientMap.remove(userId);
            log.debug("------ID为:【" + userId + "】的用户断开连接------");
        });

        //广播消息
//        socketIOServer.addEventListener(BROADCAST, String.class, (client, data, ackSender) -> {
//            String msg = data;
//            //全体在线用户推送
//            socketIOServer.getBroadcastOperations().sendEvent("message", msg);
//        });

        socketIOServer.addEventListener(SEND_MESSAGE_EVENT, SysUser.class, (client, data, ackSender) -> {
            SysUser user = data;
            SocketIOClient socketIOClient = clientMap.get(user.getUserId().toString());
            if (socketIOClient != null) {
                socketIOClient.sendEvent("message", user.getMsg());
                log.debug("触发消息推送");
            }else {
                log.debug("推送用户不在线!");
            }
        });

        socketIOServer.start();
    }

    @Override
    public void stop() {
        if (socketIOServer != null) {
            socketIOServer.stop();
            socketIOServer = null;
        }
    }

}

这里我写了三个自定义的事件

save(在前端用户登录后用来保存在线用户,保存在 clintMap 中, key值为userId) 

clear(在用户退出登录后清除其在线信息)

sendMessage(前端触发此事件之后通过传回的userId去判断用户是否在线,然后获取其中的socketIOClient, 通过 socketIOClient 发送到对应的在线用户)

3.前端逻辑实现

main.js 文件中注册socket并全局监听事件

import Vue from 'vue'

import Cookies from 'js-cookie'

import Element from 'element-ui'
import './assets/styles/element-variables.scss'

import KrPrintDesigner from "kr-print-designer"
import "kr-print-designer/lib/kr-print-designer.css"

import '@/assets/styles/index.scss' // global css
import '@/assets/styles/ruoyi.scss' // ruoyi css
import App from './App'
import store from './store'
import router from './router'
import directive from './directive' // directive
import plugins from './plugins' // plugins
import { download } from '@/utils/request'

import './assets/icons' // icon
import './permission' // permission control
import { getDicts } from "@/api/system/dict/data";
import { getConfigKey } from "@/api/system/config";
import { parseTime, resetForm, addDateRange, selectDictLabel, selectDictLabels, handleTree } from "@/utils/ruoyi";

import VueSocketIO from 'vue-socket.io';
import SocketIO from "socket.io-client";

// 分页组件
import Pagination from "@/components/Pagination";
// 自定义表格工具组件
import RightToolbar from "@/components/RightToolbar"
// 富文本组件
import Editor from "@/components/Editor"
// 文件上传组件
import FileUpload from "@/components/FileUpload"
// 图片上传组件
import ImageUpload from "@/components/ImageUpload"
// 图片预览组件
import ImagePreview from "@/components/ImagePreview"
// 字典标签组件
import DictTag from '@/components/DictTag'
// 头部标签组件
import VueMeta from 'vue-meta'
// 字典数据组件
import DictData from '@/components/DictData'

// 全局方法挂载
Vue.prototype.getDicts = getDicts
Vue.prototype.getConfigKey = getConfigKey
Vue.prototype.parseTime = parseTime
Vue.prototype.resetForm = resetForm
Vue.prototype.addDateRange = addDateRange
Vue.prototype.selectDictLabel = selectDictLabel
Vue.prototype.selectDictLabels = selectDictLabels
Vue.prototype.download = download
Vue.prototype.handleTree = handleTree
Vue.prototype.sLoading = sLoading

// 全局组件挂载
Vue.component('DictTag', DictTag)
Vue.component('Pagination', Pagination)
Vue.component('RightToolbar', RightToolbar)
Vue.component('Editor', Editor)
Vue.component('FileUpload', FileUpload)
Vue.component('ImageUpload', ImageUpload)
Vue.component('ImagePreview', ImagePreview)

Vue.use(directive)
Vue.use(plugins)
Vue.use(VueMeta)
Vue.use(KrPrintDesigner)

DictData.install()

Vue.use(Element, {
  size: Cookies.get('size') || 'medium' // set element-ui default size
})

Vue.config.productionTip = false


// socket 连接参数
const socketOptions = {
  autoConnect: false,       // 自动连接 = true
}

// 注册SocketIO
Vue.use(
  new VueSocketIO({
    debug: true ,   // debug调试,生产建议关闭
    connection: SocketIO("192.168.2.68:9999", socketOptions),
    store,
  })
)


let vue = new Vue({
  el: '#app',
  //这里为全局监听socket事件消息
  sockets: {
    connecting() {
      console.log('正在连接')
    },
    disconnect() {
      console.log("Socket 断开");
    },
    connect_failed() {
      console.log('连接失败')
    },
    error() {
      console.log("Socket 连接错误!!!")
    },
    connect() {
      console.log('====== Socket 连接成功!======')
    }
  },
  router,
  store,
  render: h => h(App)
})

export default vue

这里记得最后 export default vue 要把vue暴露出去,后面要用

src/store/modules/user.js 文件中的获取用户信息及登出方法中处理连接逻辑

import { login, logout, getInfo } from '@/api/login'
import { getToken, setToken, removeToken } from '@/utils/auth'
import vue from "@/main";
import store from '@/store/index';

const user = {
  state: {
    token: getToken(),
    id: '',
    name: '',
    avatar: '',
    roles: [],
    permissions: []
  },

  mutations: {
    SET_TOKEN: (state, token) => {
      state.token = token
    },
    SET_ID: (state, id) => {
      state.id = id
    },
    SET_NAME: (state, name) => {
      state.name = name
    },
    SET_AVATAR: (state, avatar) => {
      state.avatar = avatar
    },
    SET_ROLES: (state, roles) => {
      state.roles = roles
    },
    SET_PERMISSIONS: (state, permissions) => {
      state.permissions = permissions
    }
  },

  actions: {
    // 登录
    Login({ commit }, userInfo) {
      const username = userInfo.username.trim()
      const password = userInfo.password
      const code = userInfo.code
      const uuid = userInfo.uuid
      return new Promise((resolve, reject) => {
        login(username, password, code, uuid).then(res => {
          setToken(res.token)
          commit('SET_TOKEN', res.token)
          resolve()
        }).catch(error => {
          reject(error)
        })
      })
    },

    // 获取用户信息
    GetInfo({ commit, state }) {
      return new Promise((resolve, reject) => {
        getInfo().then(res => {
          const user = res.user
          const avatar = (user.avatar == "" || user.avatar == null) ? require("@/assets/images/profile.jpg") : process.env.VUE_APP_BASE_API + user.avatar;
          if (res.roles && res.roles.length > 0) { // 验证返回的roles是否是一个非空数组
            commit('SET_ROLES', res.roles)
            commit('SET_PERMISSIONS', res.permissions)
          } else {
            commit('SET_ROLES', ['ROLE_DEFAULT'])
          }
          commit('SET_ID', user.userId)
          commit('SET_NAME', user.userName)
          commit('SET_AVATAR', avatar)
          //TODO 获取用户信息时检查socket连接状态并进行连接
          if (!vue.$socket.connected) {
            vue.$socket.connect();
            vue.$socket.emit('save', user.userId);
          }
          resolve(res)
        }).catch(error => {
          reject(error)
        })
      })
    },

    // 退出系统
    LogOut({ commit, state }) {
      return new Promise((resolve, reject) => {
        logout(state.token).then(() => {
          commit('SET_TOKEN', '')
          commit('SET_ROLES', [])
          commit('SET_PERMISSIONS', [])
          //TODO 用户退出登录后清除用户并关闭连接
          vue.$socket.emit('clear', store.state.user.id);
          vue.$socket.close();
          removeToken()
          resolve()
        }).catch(error => {
          reject(error)
        })
      })
    },

    // 前端 登出
    FedLogOut({ commit }) {
      return new Promise(resolve => {
        commit('SET_TOKEN', '')
        removeToken()
        resolve()
      })
    }
  }
}

export default user

 这里就可以直接引入之前暴露出去的 vue 来访问其中 $socket

最后在 App.vue 中监听自定义事件获取到消息就搞定了

<template>
  <div id="app">
    <router-view />
    <theme-picker />
  </div>
</template>

<script>
import ThemePicker from "@/components/ThemePicker";

export default {
  name: "App",
  components: { ThemePicker },
  metaInfo() {
      return {
          title: this.$store.state.settings.dynamicTitle && this.$store.state.settings.title,
          titleTemplate: title => {
              return title ? `${title} - ${process.env.VUE_APP_TITLE}` : process.env.VUE_APP_TITLE
          }
      }
  },
  sockets: {
    //监控接收消息自定义事件
    message: data => {
      console.log("APP接收到消息:" + data);
    }
  }
};
</script>
<style scoped>
#app .theme-picker {
  display: none;
}
</style>

 

  • 12
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
"ruoyi-基于springboot vue的前后端分离权限管理系统.zip" 是一个基于SpringBootVue的前后端分离的权限管理系统。以下是对该系统的详细说明: 这个系统是一个完全分离前后端的架构,使用了现代化的前端技术Vue.js和后端框架SpringBoot。它的设计目标是实现一个可靠、安全、易用的权限管理系统。 系统的前端部分使用Vue.js库进行开发,它充分利用了Vue.js的组件化和响应式特性,从而提供了一个良好的用户界面和交互体验。前端页面可以动态地响应用户的操作,并与后端进行数据交互。通过Vue-router插件,系统实现了页面的路由功能,使用户能够方便地在不同的页面之间进行切换和导航。此外,系统还使用了Element UI库,该库提供了丰富的组件和样式,可以大大提高开发效率。 系统的后端部分使用了SpringBoot框架,它是一种快速开发Java应用程序的框架。SpringBoot具有自动配置、快速启动、约定大于配置等特点,可以让开发人员更专注于业务逻辑的实现。后端部分负责处理前端发送的请求,并进行权限验证、数据查询和操作等后端逻辑。同时,系统还使用了MyBatis框架来操作数据库,它是一种简化了数据库访问的框架,能够有效地提高数据库操作的效率。 此外,系统还具有权限管理的功能。它能够根据不同的角色对用户进行权限控制,实现用户的分级管理和权限的授权。系统管理员可以在后台管理界面对用户进行管理,并配置他们的角色和权限。通过这种方式,可以保护系统的安全性,并限制用户对敏感数据的访问。 总之,ruoyi-基于springboot vue的前后端分离权限管理系统.zip 是一个功能强大且易于使用的权限管理系统,它综合运用了SpringBootVue.js的优势,提供了一个完整的前后端分离架构,可以满足不同应用场景下的权限管理需求。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值