• 基于若依(SpringBoot前后分离版-vue)的WebSocket消息推送实现


    引言

    自己写了个小项目游戏报价器,想在更新系统的时候可以提前在系统弹窗提示用户,注意系统更新。
    第一想到的就是WebSocket了,在更新前,提前发布公告,通过WebSocket推送到web客户端界面。

    WebSocket是一种通信协议,可在单个TCP连接上进行全双工通信。WebSocket使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在WebSocket API中,浏览器和服务器只需要完成一次握手,两者之间就可以建立持久性的连接,并进行双向数据传输。

    具体实现方法如下:

    添加依赖

    版本自定,我用的最新版。

    
    <dependency>
    	<groupId>org.springframework.bootgroupId>
    	<artifactId>spring-boot-starter-websocketartifactId>
    	<version>2.2.13.RELEASEversion>
    dependency>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    取消Websocket鉴权认证

    • 如果你没有用到若依,并且接口访问没有鉴权仅仅是测试使用,可以跳过这一步。
    • 如果你用的是若依的前后分离版 需要到spring security配置中修改,具体文件位置为framework模块-config-SecurityConfig.java-configure方法。

    添加如下代码:

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

    在这里插入图片描述

    • 如果你是若依前后不分离版本应该用的是Shiro,需要在ShiroConfig.java中操作。

    在对应取消鉴权认证的位置添加如下代码:

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

    添加配置

    我是在framework模块新建了一个websocket包。
    包内的代码如下:

    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();
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    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);
            }
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    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);
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    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[你已离线]");
            }
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139

    前端

    <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>
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142
    • 143
    • 144
    • 145
    • 146
    • 147
    • 148
    • 149
    • 150
    • 151
    • 152
    • 153
    • 154
    • 155
    • 156
    • 157
    • 158
    • 159
    • 160
    • 161
    • 162
    • 163
    • 164
    • 165
    • 166
    • 167
    • 168
    • 169
    • 170
    • 171
    • 172
    • 173
    • 174
    • 175
    • 176
    • 177
    • 178
    • 179
    • 180
    • 181
    • 182
    • 183
    • 184
    • 185
    • 186
    • 187
    • 188
    • 189
    • 190
    • 191
    • 192
    • 193
    • 194
    • 195
    • 196
    • 197
    • 198
    • 199
    • 200
    • 201
    • 202
    • 203
    • 204
    • 205
    • 206
    • 207
    • 208
    • 209
    • 210
    • 211
    • 212
    • 213
    • 214
    • 215
    • 216
    • 217
    • 218
    • 219
    • 220
    • 221
    • 222
    • 223
    • 224
    • 225
    • 226
    • 227
    • 228
    • 229
    • 230
    • 231
    • 232
    • 233
    • 234
    • 235
    • 236
    • 237
    • 238
    • 239
    • 240
    • 241
    • 242
    • 243
    • 244
    • 245
    • 246
    • 247
    • 248
    • 249
    • 250
    • 251
    • 252
    • 253
    • 254
    • 255
    • 256
    • 257
    • 258
    • 259
    • 260
    • 261
    • 262
    • 263
    • 264
    • 265
    • 266
    • 267
    • 268
    • 269
    • 270
    • 271
    • 272
    • 273
    • 274
    • 275
    • 276
    • 277
    • 278
    import request from '@/utils/request'
    
    // 获取前三个通知用于平时展示
    export function getNoticeListTop3(query) {
      return request({
        url: '/system/notice/listTop3/',
        method: 'get',
        params: query
      })
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
  • 相关阅读:
    实现我们mysql的主从复制
    (还在纠结Notability、Goodnotes和Marginnote吗?)iPad、安卓平板、Windows学习软件推荐
    牛客网刷题——JAVA
    C++Primer Plus第十一章类的使用,课后练习1,还是醉汉回家的故事
    字节携港大南大升级 LLaVA-NeXT:借 LLaMA-3 和 Qwen-1.5 脱胎换骨,轻松追平 GPT-4V
    OpenWrt 20.02.2 小米路由器3G配置CP1025网络打印
    交了一个程序员男朋友后,我彻底变了……
    那些舍不得删除的 MP3--批量修改mp3的ID3tag
    vLLM-prefix浅析(System Prompt,大模型推理加速)
    项目部署;流程
  • 原文地址:https://blog.csdn.net/weixin_52799373/article/details/126094965