WebSocket message push implementation based on Ruoyi (SpringBoot front and rear separation version-vue)

introduction

I wrote a small project game quoter by myself. I want to remind the user in the system pop-up window in advance when the system is updated, and pay attention to the system update.
The first thing that comes to mind is WebSocket. Before the update, an announcement is made in advance and pushed to the web client interface through WebSocket.

WebSocket is a communication protocol that enables full-duplex communication over a single TCP connection. 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 established between the two, and two-way data transmission can be performed.

The specific implementation method is as follows:

add dependencies

The version is customized, the latest version I use.

<!-- SpringBoot Websocket -->
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-websocket</artifactId>
	<version>2.2.13.RELEASE</version>
</dependency>

Cancel Websocket authentication

  • If you do not use Zoyi, and the interface access without authentication is only for testing, you can skip this step.
  • If you are using Ruoyi's front and back separation version, you need to modify it in the spring security configuration. The specific file location is the framework module-config-SecurityConfig.java-configure method.

Add the following code:

.antMatchers("/websocket/**").anonymous()

insert image description here

  • If you are using Shiro without separating the front and rear versions, you need to operate in ShiroConfig.java.

Add the following code in the position corresponding to the cancellation of authentication and authentication:

filterChainDefinitionMap.put("/websocket/**", "anon");

add configuration

I created a new websocket package in the framework module.
The code inside the package is as follows:

package com.rdjxx.framework.websocket;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;

/**
 * websocket 配置
 * 
 * @author fm
 */
@Configuration
public class WebSocketConfig
{
    
    
    @Bean
    public ServerEndpointExporter serverEndpointExporter()
    {
    
    
        return new ServerEndpointExporter();
    }
}

package com.rdjxx.framework.websocket;

import java.util.concurrent.Semaphore;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * 信号量相关处理
 * 
 * @author fm
 */
public class SemaphoreUtils
{
    
    
    /**
     * SemaphoreUtils 日志控制器
     */
    private static final Logger LOGGER = LoggerFactory.getLogger(SemaphoreUtils.class);

    /**
     * 获取信号量
     * 
     * @param semaphore
     * @return
     */
    public static boolean tryAcquire(Semaphore semaphore)
    {
    
    
        boolean flag = false;

        try
        {
    
    
            flag = semaphore.tryAcquire();
        }
        catch (Exception e)
        {
    
    
            LOGGER.error("获取信号量异常", e);
        }

        return flag;
    }

    /**
     * 释放信号量
     * 
     * @param semaphore
     */
    public static void release(Semaphore semaphore)
    {
    
    

        try
        {
    
    
            semaphore.release();
        }
        catch (Exception e)
        {
    
    
            LOGGER.error("释放信号量异常", e);
        }
    }
}

package com.rdjxx.framework.websocket;

import java.util.List;
import java.util.concurrent.Semaphore;
import javax.websocket.OnClose;
import javax.websocket.OnError;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.ServerEndpoint;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

/**
 * websocket 消息处理
 *
 * @author fm
 */
@Component
@ServerEndpoint("/websocket/message")
public class WebSocketServer
{
    
    
    /**
     * WebSocketServer 日志控制器
     */
    private static final Logger LOGGER = LoggerFactory.getLogger(WebSocketServer.class);

    /**
     * 默认最多允许同时在线人数100
     */
    public static int socketMaxOnlineCount = 1000;

    private static Semaphore socketSemaphore = new Semaphore(socketMaxOnlineCount);

    /**
     * 连接建立成功调用的方法
     */
    @OnOpen
    public void onOpen(Session session) throws Exception
    {
    
    
        boolean semaphoreFlag = false;
        // 尝试获取信号量
        semaphoreFlag = SemaphoreUtils.tryAcquire(socketSemaphore);
        if (!semaphoreFlag)
        {
    
    
            // 未获取到信号量
            LOGGER.error("\n 当前在线人数超过限制数- {}", socketMaxOnlineCount);
//            WebSocketUsers.sendMessageToUserByText(session, "当前在线人数超过限制数:" + socketMaxOnlineCount);
            session.close();
        }
        else
        {
    
    
            // 添加用户
            WebSocketUsers.put(session.getId(), session);
            //LOGGER.info("\n 建立连接 - {}", session);
            //LOGGER.info("\n 当前人数 - {}", WebSocketUsers.getUsers().size());
            // WebSocketUsers.sendMessageToUserByText(session, "连接成功");
        }
    }

    /**
     * 连接关闭时处理
     */
    @OnClose
    public void onClose(Session session)
    {
    
    
        //LOGGER.info("\n 关闭连接 - {}", session);
        // 移除用户
        WebSocketUsers.remove(session.getId());
        // 获取到信号量则需释放
        SemaphoreUtils.release(socketSemaphore);
    }

    /**
     * 抛出异常时处理
     */
    @OnError
    public void onError(Session session, Throwable exception) throws Exception
    {
    
    
        if (session.isOpen())
        {
    
    
            // 关闭连接
            session.close();
        }
        String sessionId = session.getId();
        //LOGGER.info("\n 连接异常 - {}", sessionId);
        //LOGGER.info("\n 异常信息 - {}", exception);
        // 移出用户
        WebSocketUsers.remove(sessionId);
        // 获取到信号量则需释放
        SemaphoreUtils.release(socketSemaphore);
    }

    /**
     * 服务器接收到客户端消息时调用的方法
     */
    @OnMessage
    public void onMessage(String message, Session session)
    {
    
    
        String msg = message.replace("你", "我").replace("吗", "");
        WebSocketUsers.sendMessageToUserByText(session, msg);
    }
}

package com.rdjxx.framework.websocket;

import java.io.IOException;
import java.util.Collection;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import javax.websocket.Session;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * websocket 客户端用户集
 * 
 * @author fm
 */
public class WebSocketUsers
{
    
    
    /**
     * WebSocketUsers 日志控制器
     */
    private static final Logger LOGGER = LoggerFactory.getLogger(WebSocketUsers.class);

    /**
     * 用户集
     */
    private static Map<String, Session> USERS = new ConcurrentHashMap<String, Session>();

    /**
     * 存储用户
     *
     * @param key 唯一键
     * @param session 用户信息
     */
    public static void put(String key, Session session)
    {
    
    
        USERS.put(key, session);
    }

    /**
     * 移除用户
     *
     * @param session 用户信息
     *
     * @return 移除结果
     */
    public static boolean remove(Session session)
    {
    
    
        String key = null;
        boolean flag = USERS.containsValue(session);
        if (flag)
        {
    
    
            Set<Map.Entry<String, Session>> entries = USERS.entrySet();
            for (Map.Entry<String, Session> entry : entries)
            {
    
    
                Session value = entry.getValue();
                if (value.equals(session))
                {
    
    
                    key = entry.getKey();
                    break;
                }
            }
        }
        else
        {
    
    
            return true;
        }
        return remove(key);
    }

    /**
     * 移出用户
     *
     * @param key 键
     */
    public static boolean remove(String key)
    {
    
    
        //LOGGER.info("\n 正在移出用户 - {}", key);
        Session remove = USERS.remove(key);
        if (remove != null)
        {
    
    
            boolean containsValue = USERS.containsValue(remove);
            //LOGGER.info("\n 移出结果 - {}", containsValue ? "失败" : "成功");
            return containsValue;
        }
        else
        {
    
    
            return true;
        }
    }

    /**
     * 获取在线用户列表
     *
     * @return 返回用户集合
     */
    public static Map<String, Session> getUsers()
    {
    
    
        return USERS;
    }

    /**
     * 群发消息文本消息
     *
     * @param message 消息内容
     */
    public static void sendMessageToUsersByText(String message)
    {
    
    
        Collection<Session> values = USERS.values();
        for (Session value : values)
        {
    
    
            sendMessageToUserByText(value, message);
        }
    }

    /**
     * 发送文本消息
     *
     */
    public static void sendMessageToUserByText(Session session, String message)
    {
    
    
        if (session != null)
        {
    
    
            try
            {
    
    
                session.getBasicRemote().sendText(message);
            }
            catch (IOException e)
            {
    
    
                LOGGER.error("\n[发送消息异常]", e);
            }
        }
        else
        {
    
    
            //LOGGER.info("\n[你已离线]");
        }
    }
}

front end

<template>
  <div class="navbar">
    <hamburger id="hamburger-container" :is-active="sidebar.opened" class="hamburger-container"
               @toggleClick="toggleSideBar"/>

    <breadcrumb id="breadcrumb-container" class="breadcrumb-container" v-if="!topNav"/>
    <top-nav id="topmenu-container" class="topmenu-container" v-if="topNav"/>

    <div class="right-menu">
      <template v-if="device!=='mobile'">
        <screenfull id="screenfull" class="right-menu-item hover-effect"/>

        <el-tooltip content="布局大小" effect="dark" placement="bottom">
          <size-select id="size-select" class="right-menu-item hover-effect"/>
        </el-tooltip>

      </template>

      <el-dropdown class="avatar-container right-menu-item hover-effect" trigger="click">
        <div class="avatar-wrapper">
          <img :src="avatar" class="user-avatar">
          <i class="el-icon-caret-bottom"/>
        </div>
        <el-dropdown-menu slot="dropdown">
          <router-link to="/user/profile">
            <el-dropdown-item>个人中心</el-dropdown-item>
          </router-link>
          <el-dropdown-item @click.native="setting = true">
            <span>布局设置</span>
          </el-dropdown-item>
          <el-dropdown-item divided @click.native="logout">
            <span>退出登录</span>
          </el-dropdown-item>
        </el-dropdown-menu>
      </el-dropdown>
    </div>
  </div>
</template>

<script>
import {
      
      mapGetters} from 'vuex'
import Breadcrumb from '@/components/Breadcrumb'
import TopNav from '@/components/TopNav'
import Hamburger from '@/components/Hamburger'
import Screenfull from '@/components/Screenfull'
import SizeSelect from '@/components/SizeSelect'
import Search from '@/components/HeaderSearch'
import RuoYiGit from '@/components/RuoYi/Git'
import RuoYiDoc from '@/components/RuoYi/Doc'
import {
      
      Notification} from "element-ui";
import {
      
      getNoticeListTop3} from "@/api/system/navbar";

export default {
      
      
  data() {
      
      
    return {
      
      
      url: "ws://localhost:8087/websocket/message",
      message: "",
      text_content: "",
      ws: null,
    };
  },
  components: {
      
      
    Breadcrumb,
    TopNav,
    Hamburger,
    Screenfull,
    SizeSelect,
    Search,
    RuoYiGit,
    RuoYiDoc
  },
  computed: {
      
      
    ...mapGetters([
      'sidebar',
      'avatar',
      'device'
    ]),
    setting: {
      
      
      get() {
      
      
        return this.$store.state.settings.showSettings
      },
      set(val) {
      
      
        this.$store.dispatch('settings/changeSetting', {
      
      
          key: 'showSettings',
          value: val
        })
      }
    },
    topNav: {
      
      
      get() {
      
      
        return this.$store.state.settings.topNav
      }
    }
  },
  mounted() {
      
      
    this.notice();
    const wsuri = this.url;
    this.ws = new WebSocket(wsuri);
    const self = this;
    this.ws.onopen = function (event) {
      
      
      //self.text_content = self.text_content + "已经打开连接!" + "\n";
    };
    this.ws.onmessage = function (event) {
      
      
      //self.text_content = event.data + "\n";
      var messageBody = JSON.parse(event.data);
      Notification.info({
      
      
        title: "通知",
        dangerouslyUseHTMLString: true,
        message: messageBody.noticeContent,
        duration: 0,
        offset: 40,
        onClick: function () {
      
      
          //self.warnDetailByWarnid(messageBody.warnId); //自定义回调,message为传的参数
          // 点击跳转的页面
        },
      });
    };
    this.ws.onclose = function (event) {
      
      
      //self.text_content = self.text_content + "已经关闭连接!" + "\n";
    };
  },
  methods: {
      
      
    notice() {
      
      
      getNoticeListTop3().then(response => {
      
      
        for (let i = 0; i < response.length; i++) {
      
      
          let messageBody = response[i];
          setTimeout(() => {
      
      
            this.notificationInfo(messageBody);
          }, 100);
        }
      })
    },
    notificationInfo(messageBody) {
      
      
      Notification.info({
      
      
        title: "通知",
        dangerouslyUseHTMLString: true,
        message: messageBody.noticeContent,
        duration: 0,
        offset: 40,
        onClick: function () {
      
      
          //self.warnDetailByWarnid(messageBody.warnId); //自定义回调,message为传的参数
          // 点击跳转的页面
        },
      });
    },
    // join() {
      
      
    //
    // },
    exit() {
      
      
      if (this.ws) {
      
      
        this.ws.close();
        this.ws = null;
      }
    },
    send() {
      
      
      if (this.ws) {
      
      
        this.ws.send(this.message);
      } else {
      
      
        alert("未连接到服务器");
      }
    },
    warnDetailByWarnid(warnid) {
      
      
      // 跳转预警详情页面
      this.$router.push({
      
      
        path: "/XXX/XXX",
        query: {
      
      
          warnid: warnid,
        },
      });
    },
    toggleSideBar() {
      
      
      this.$store.dispatch('app/toggleSideBar')
    },
    async logout() {
      
      
      this.$confirm('确定注销并退出系统吗?', '提示', {
      
      
        confirmButtonText: '确定',
        cancelButtonText: '取消',
        type: 'warning'
      }).then(() => {
      
      
        this.$store.dispatch('LogOut').then(() => {
      
      
          location.href = '/index';
        })
      }).catch(() => {
      
      
      });
    }
  }
}
</script>

<style lang="scss" scoped>
.navbar {
      
      
  height: 50px;
  overflow: hidden;
  position: relative;
  background: #fff;
  box-shadow: 0 1px 4px rgba(0, 21, 41, .08);

  .hamburger-container {
      
      
    line-height: 46px;
    height: 100%;
    float: left;
    cursor: pointer;
    transition: background .3s;
    -webkit-tap-highlight-color: transparent;

    &:hover {
      
      
      background: rgba(0, 0, 0, .025)
    }
  }

  .breadcrumb-container {
      
      
    float: left;
  }

  .topmenu-container {
      
      
    position: absolute;
    left: 50px;
  }

  .errLog-container {
      
      
    display: inline-block;
    vertical-align: top;
  }

  .right-menu {
      
      
    float: right;
    height: 100%;
    line-height: 50px;

    &:focus {
      
      
      outline: none;
    }

    .right-menu-item {
      
      
      display: inline-block;
      padding: 0 8px;
      height: 100%;
      font-size: 18px;
      color: #5a5e66;
      vertical-align: text-bottom;

      &.hover-effect {
      
      
        cursor: pointer;
        transition: background .3s;

        &:hover {
      
      
          background: rgba(0, 0, 0, .025)
        }
      }
    }

    .avatar-container {
      
      
      margin-right: 30px;

      .avatar-wrapper {
      
      
        margin-top: 5px;
        position: relative;

        .user-avatar {
      
      
          cursor: pointer;
          width: 40px;
          height: 40px;
          border-radius: 10px;
        }

        .el-icon-caret-bottom {
      
      
          cursor: pointer;
          position: absolute;
          right: -20px;
          top: 25px;
          font-size: 12px;
        }
      }
    }
  }
}
</style>

import request from '@/utils/request'

// 获取前三个通知用于平时展示
export function getNoticeListTop3(query) {
    
    
  return request({
    
    
    url: '/system/notice/listTop3/',
    method: 'get',
    params: query
  })
}

Guess you like

Origin blog.csdn.net/weixin_52799373/article/details/126094965