• SpringBoot+Vue 整合websocket实现简单聊天窗口


    效果图

    1 输入临时名字充当账号使用
    image-1694448636449

    2 进入聊天窗口
    image-1694448674599

    3 发送消息 (复制一个页面,输入其他名字,方便展示效果)
    image-1694448766333

    4 其他窗口效果
    image-1694448783698

    代码实现

    后端SpringBoot项目,自行创建

    pom依赖

    		<dependency>
                <groupId>org.springframework.bootgroupId>
                <artifactId>spring-boot-starter-webartifactId>
            dependency>
    
            <dependency>
                <groupId>org.projectlombokgroupId>
                <artifactId>lombokartifactId>
                <optional>trueoptional>
            dependency>
            <dependency>
                <groupId>org.springframework.bootgroupId>
                <artifactId>spring-boot-starter-testartifactId>
                <scope>testscope>
            dependency>
    
            <dependency>
                <groupId>org.springframework.bootgroupId>
                <artifactId>spring-boot-starter-websocketartifactId>
                <version>2.7.12version>
            dependency>
            <dependency>
                <groupId>com.alibaba.fastjson2groupId>
                <artifactId>fastjson2artifactId>
                <version>2.0.23version>
            dependency>
    
    • 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

    WebSocketConfig.java

    package com.dark.wsdemo.config;
    
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.web.socket.server.standard.ServerEndpointExporter;
    
    /**
     * WebSocket配置类。开启WebSocket的支持
     */
    @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

    WebSocketServer.java

    package com.dark.wsdemo.service;
    
    import com.alibaba.fastjson2.JSON;
    import com.alibaba.fastjson2.JSONObject;
    import com.dark.wsdemo.vo.MessageVo;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.stereotype.Component;
    
    import javax.websocket.*;
    import javax.websocket.server.PathParam;
    import javax.websocket.server.ServerEndpoint;
    import java.util.Date;
    import java.util.Map;
    import java.util.concurrent.ConcurrentHashMap;
    import java.util.concurrent.atomic.AtomicInteger;
    
    /**
     * WebSocket的操作类
     */
    @Component
    @Slf4j
    @ServerEndpoint("/websocket/{name}")
    public class WebSocketServer {
    
        /**
         * 静态变量,用来记录当前在线连接数,线程安全的类。
         */
        private static final AtomicInteger onlineSessionClientCount = new AtomicInteger(0);
    
        /**
         * 存放所有在线的客户端
         */
        private static final Map<String, Session> onlineSessionClientMap = new ConcurrentHashMap<>();
    
        /**
         * 连接 name 和连接会话
         */
        private String name;
    
        @OnOpen
        public void onOpen(@PathParam("name") String name, Session session) {
            /**
             * session.getId():当前session会话会自动生成一个id,从0开始累加的。
             */
            Session beforeSession = onlineSessionClientMap.get(name);
            if (beforeSession != null) {
                //在线数减1
                onlineSessionClientCount.decrementAndGet();
                log.info("连接已存在,关闭之前的连接 ==> session_id = {}, name = {}。", beforeSession.getId(), name);
                //通知之前其他地方连接被挤掉
                sendToOne(name, "您的账号在其他地方登录,您被迫下线。");
                // 从 Map中移除
                onlineSessionClientMap.remove(name);
                //关闭之前的连接
                try {
                    beforeSession.close();
                } catch (Exception e) {
                    log.error("关闭之前的连接异常,异常信息为:{}", e.getMessage());
                }
            }
            log.info("连接建立中 ==> session_id = {}, name = {}", session.getId(), name);
            onlineSessionClientMap.put(name, session);
    
            //在线数加1
            onlineSessionClientCount.incrementAndGet();
            this.name = name;
            sendToOne(name, "连接成功");
            log.info("连接建立成功,当前在线数为:{} ==> 开始监听新连接:session_id = {}, name = {}。", onlineSessionClientCount, session.getId(), name);
        }
    
    
        @OnClose
        public void onClose(@PathParam("name") String name, Session session) {
            if (name == null || name.equals("")) {
                name = this.name;
            }
            // 从 Map中移除
            onlineSessionClientMap.remove(name);
    
            //在线数减1
            onlineSessionClientCount.decrementAndGet();
            log.info("连接关闭成功,当前在线数为:{} ==> 关闭该连接信息:session_id = {}, name = {}。", onlineSessionClientCount, session.getId(), name);
        }
    
        @OnMessage
        public void onMessage(String message, Session session) {
            JSONObject jsonObject = JSON.parseObject(message);
            String toname = jsonObject.getString("name");
            String msg = jsonObject.getString("message");
            log.info("服务端收到客户端消息 ==> fromname = {}, toname = {}, message = {}", name, toname, message);
    
            /**
             * 模拟约定:如果未指定name信息,则群发,否则就单独发送
             */
            if (toname == null || toname == "" || "".equalsIgnoreCase(toname)) {
                sendToAll(msg);
            } else {
                sendToOne(toname, msg);
            }
        }
    
        /**
         * 发生错误调用的方法
         *
         * @param session
         * @param error
         */
        @OnError
        public void onError(Session session, Throwable error) {
            log.error("WebSocket发生错误,错误信息为:" + error.getMessage());
            error.printStackTrace();
        }
    
        /**
         * 群发消息
         *
         * @param message 消息
         */
        private void sendToAll(String message) {
            // 遍历在线map集合
            onlineSessionClientMap.forEach((onlineName, toSession) -> {
                // 排除掉自己
                if (!name.equalsIgnoreCase(onlineName)) {
                    log.info("服务端给客户端群发消息 ==> name = {}, toname = {}, message = {}", name, onlineName, message);
                    MessageVo messageVo = new MessageVo();
                    messageVo.setFrom(name);
                    messageVo.setDate(new Date());
                    messageVo.setMessage(message);
                    toSession.getAsyncRemote().sendText(JSON.toJSONString(messageVo));
                }
            });
        }
    
        /**
         * 指定发送消息
         *
         * @param toName
         * @param message
         */
        private void sendToOne(String toName, String message) {
            // 通过name查询map中是否存在
            Session toSession = onlineSessionClientMap.get(toName);
            if (toSession == null) {
                log.error("服务端给客户端发送消息 ==> toname = {} 不存在, message = {}", toName, message);
                return;
            }
            // 异步发送
            log.info("服务端给客户端发送消息 ==> toname = {}, message = {}", toName, message);
            MessageVo messageVo = new MessageVo();
            messageVo.setFrom(name);
            messageVo.setDate(new Date());
            messageVo.setMessage(message);
            toSession.getAsyncRemote().sendText(JSON.toJSONString(messageVo));
    
        }
    
    }
    
    
    • 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

    MessageVo.java

    package com.dark.wsdemo.vo;
    
    import com.fasterxml.jackson.annotation.JsonFormat;
    import lombok.Data;
    
    import java.util.Date;
    
    @Data
    public class MessageVo {
        private String from;
        //json时候格式化为时间格式
        @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
        private Date date;
        private String message;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    Vue代码实现

    App.vue

    <template>
      <div id="app">
        <!-- Modal Dialog -->
        <div class="modal" v-if="!username">
          <div class="modal-content">
            <h2>请输入你的名字</h2>
            <input type="text" v-model="inputUsername" />
            <button @click="setUsername">确定</button>
          </div>
        </div>
    
        <!-- Chat Box -->
        <div class="chat-box" v-if="username">
          <div class="chat-history">
            <div v-for="msg in messages" :key="msg.id" :class="[msg.type, 'message']">
              <div class="info">
                <span class="from">{{ msg.from }}</span>
                <span class="date">{{ msg.date }}</span>
              </div>
              <div class="bubble">
                {{ msg.message }}
              </div>
            </div>
          </div>
          <div class="chat-input">
            <input type="text" v-model="inputMessage" @keyup.enter="sendMessage" placeholder="请输入消息..."/>
            <button @click="sendMessage">发送</button>
          </div>
        </div>
      </div>
    </template>
    
    <script>
    export default {
      data() {
        return {
          inputMessage: '',
          inputUsername: '',
          messages: [],
          username: '',
          ws: null,
        };
      },
      methods: {
        setUsername() {
          if (this.inputUsername.trim() === '') return;
          this.username = this.inputUsername.trim();
          this.ws = new WebSocket(`ws://localhost:8081/websocket/${this.username}`);
    
          this.ws.addEventListener('message', (event) => {
            const data = JSON.parse(event.data);
            this.messages.push({ ...data, type: 'left', id: this.messages.length });
          });
        },
        sendMessage() {
          if (this.inputMessage.trim() === '') return;
          const message = {
            from: this.username,
            date: new Date().toLocaleString(),
            message: this.inputMessage.trim(),
          };
          this.ws.send(JSON.stringify(message));
          this.messages.push({ ...message, type: 'right', id: this.messages.length });
          this.inputMessage = '';
        },
      },
    };
    </script>
    
    <style>
    /* Modal Styles */
    .modal {
      display: flex;
      justify-content: center;
      align-items: center;
      position: fixed;
      left: 0;
      top: 0;
      width: 100%;
      height: 100%;
      background-color: rgba(0, 0, 0, 0.5);
      z-index: 9999;
    }
    
    .modal-content {
      background-color: #fff;
      padding: 20px;
      width: 300px;
      text-align: center;
      border-radius: 10px;
      box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
    }
    
    /* Chat Box Styles */
    #app {
      background-color: #f2f2f2;
      display: flex;
      justify-content: center;
      align-items: center;
      height: 100vh;
      margin: 0;
      font-family: Arial, sans-serif;
    }
    
    .chat-box {
      box-shadow: 0 8px 16px 0 rgba(0,0,0,0.2);
      width: 300px;
      height: 400px;
      border-radius: 8px;
      overflow: hidden;
      display: flex;
      flex-direction: column;
    }
    
    .chat-history {
      flex: 1;
      overflow-y: auto;
      padding: 10px;
      background-color: #fff;
    }
    
    .message {
      padding: 5px 0;
    }
    
    .info {
      font-size: 12px;
      color: gray;
      margin-bottom: 4px;
    }
    
    .left .bubble {
      background-color: #e6e6e6;
      border-radius: 15px;
      padding: 12px;
      display: inline-block;
    }
    
    .right .bubble {
      background-color: #007bff;
      color: white;
      border-radius: 15px;
      padding: 12px;
      display: inline-block;
      margin-left: auto;
    }
    
    .chat-input {
      display: flex;
      padding: 10px;
      background-color: #f7f7f7;
      border-top: 1px solid #ccc;
    }
    
    input {
      flex: 1;
      padding: 8px;
      border: 1px solid #ccc;
      border-radius: 4px;
      margin-right: 10px;
    }
    
    button {
      padding: 10px 20px;
      background-color: #007bff;
      color: white;
      border: none;
      border-radius: 4px;
      cursor: pointer;
    }
    
    button:hover {
      background-color: #0056b3;
    }
    </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
  • 相关阅读:
    leetcode每天5题-Day54(贪心3)
    DGIOT实战教程-监控摄像头接入(v4.6.0)
    mqant启动流程
    seaweedfs分布式文件系统
    手机厂商“卷”到了手腕上
    从Cookie到设备ID,从算法到云+端!全面盘点设备指纹技术的五代发展
    Beautiful Soup的使用
    如何实现IM即时通讯“消息”列表卡顿优化
    【vue五分钟】五分钟让你了解vue组件
    Elsevier出版社 | 优质好刊合集
  • 原文地址:https://blog.csdn.net/qq_49619863/article/details/132820032