• springboot+websocket+vue聊天室


    一、项目实现内容

    1. http://localhost:8080/websocket?uid=1

    在这里插入图片描述

    1. http://localhost:8080/websocket?uid=2

    在这里插入图片描述

    1. http://localhost:8080/websocket?uid=3

    在这里插入图片描述

    二、websocket

    websocket api介绍
    再看这里,这个是我看介绍比较好的websocket使用

    1. websocket方法定义
      WebSocket.onclose
      用于指定连接关闭后的回调函数。
      WebSocket.onerror
      用于指定连接失败后的回调函数。
      WebSocket.onmessage
      用于指定当从服务器接受到信息时的回调函数。
      WebSocket.onopen
      用于指定连接成功后的回调函数。

    2. 先是定义websocket的处理逻辑
      在这里插入图片描述

    3. 消息流转过程
      在这里插入图片描述

    三、实现过程

    前提:这只是一个小demo没用到数据库,只是简单的在后端直接返回准备好的用户,但是逻辑是没有问题的,只要你的用户信息换成查数据库和将发到服务器的消息数据保存一份到数据库就行了。(CURD比较简单,逻辑明白就行)

    java后端

    1. @Component注册到spring容器,交由spring控制
    2. @ServerEndpoint("/path")是和@RequestMapping("/path")差不多类似的,若是有ws协议的路上path匹配则交由该对象处理(主要是将目前的类定义成一个websocket服务器端, 注解的值将被用于监听用户连接的终端访问URL地址,客户端可以通过这个URL来连接到WebSocket服务器端
    3. WebSocketServer的加载spring容器之前,后面有客户端连接服务器,则将WebSocketServersession、uid替换成客户端对应的存储在Map中记录起来,发送消息还得用到对应的session
      在这里插入图片描述
    4. 之后接收到客户端的消息,onMessage内可以通过webSocketMap记录的
      WebSocketServer使用session.getBasicRemote().sendText(message);发送消息message
    @Component
    @ServerEndpoint("/wechat/{uid}")
    public class WebSocketServer {
        /**
         * 记录在线的用户数
         */
        private static AtomicInteger onlineUserNum=new AtomicInteger(0);
    
        /**
         * 存储连接该服务的用户(客户端)对应的WebSocketServer (uid,WebSocketServer)
         */
        private static Map<Integer,WebSocketServer> webSocketMap=new ConcurrentHashMap<>();
    
        /**
         * 与某个客户端的连接会话,需要通过它来给客户端发送数据
         */
        private Session session;
    
        /**
         * 当前连接进行用户的uid
         */
        private int uid;
    
        /**
         * 连接成功后的回调函数
         * @param uid
         * @param session
         */
        @OnOpen
        public void onOpen(@PathParam("uid")int uid,Session session){
            //获取当前的session、uid
            this.session=session;
            this.uid=uid;
            //存储客户端对应的websocket
            if (!webSocketMap.containsKey(uid)){
                //判断这里还应该查一下数据库,但是我这里比较潦草就没做
                //还未连接过
                webSocketMap.put(uid,this);
                //在线人数+1
                onlineUserNum.incrementAndGet();
            }else{
                //已经连接过,记录新的websocket
                webSocketMap.replace(uid,this);
            }
            System.out.println("用户id:"+uid+"建立连接!");
        }
    
        /**
         * 连接失败后的回调函数
         * @param session
         * @param error
         */
        @OnError
        public void onError(Session session, Throwable error) {
            System.out.println("用户:"+this.uid+"连接失败,原因:"+error.getMessage());
            error.printStackTrace();
        }
    
        /**
         * 前提:成功建立连接
         *      发送过来的消息按如下的机制推送到接收的客户端
         * @param message
         * @param session
         */
        @OnMessage
        public void onMessage(String message,Session session){
            System.out.println(message);
            if(message.isEmpty()||message==null){
                //消息不正常,不处理
                return;
            }
            //初始化消息的格式 json->自己定义的消息体
            Message fromMessage = JSON.parseObject(message,Message.class);
            if(!webSocketMap.containsKey(fromMessage.getToUid())){
                System.out.println("要接收的用户不在线,暂存数据库,等该用户上线在获取!");
                return;
            }
            //在线则直接推送数据到接收端客户端
            WebSocketServer webSocketServer = webSocketMap.get(fromMessage.getToUid());
            webSocketServer.sendMessage(message);
        }
    
        /**
         * 推送消息到客户端
         * @param message
         */
        public void sendMessage(String message) {
            try {
                this.session.getBasicRemote().sendText(message);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    
        /**
         * 连接关闭后的回调函数
         */
        @OnClose
        public void onClose(){
            if (webSocketMap.containsKey(uid)){
                webSocketMap.remove(uid);
                //在线人数-1
                onlineUserNum.decrementAndGet();
            }
        }
    
    }
    

    vue前端

    下面介绍进入页面的逻辑(可以和前面的图多结合理解)

    1. localhost:8080/?uid=i 获取用户i的信息进入页面(因为没登陆注册就这样用于测试)
    2. 获取在线用户,除去本身
    3. 创建WebSocket对象连接服务器(此时服务器记录了和客户端连接时的WebSocketServer
    4. 就可以通过全局的WebSocket对象发送消息了
    <template>
      <div class="bg">
        <el-container class="wechat">
          <el-aside width="35%" style="border-right: 1px solid #fff">
            
            <div class="item">
              <el-avatar
                :size="46"
                :src="user.avatarUrl"
                style="float: left; margin-left: 2px"
              >el-avatar>
              <div class="name">
                {{ user.nickname
                }}<el-tag style="margin-left: 5px" type="success">本人el-tag>
              div>
            div>
            
            <div
              class="item"
              v-for="(item1, index) in userlist"
              :key="item1.uid"
              @click="selectUser(index)"
            >
              
              <el-badge
                :value="new_message_num[index]"
                :max="99"
                :hidden="!new_message_num[index] > 0"
                style="float: left; margin-left: 2px"
              >
                <el-avatar :size="46" :src="item1.avatarUrl">el-avatar>
              el-badge>
              <div class="name">{{ item1.nickname }}div>
            div>
          el-aside>
          <el-main>
            <el-container class="wechat_right">
              
              <el-header class="header">{{
                anotherUser != null && anotherUser.uid > 0
                  ? anotherUser.nickname
                  : "未选择聊天对象"
              }}el-header>
              
              <el-main class="showChat">
                <div v-for="item2 in messageList[index]" :key="item2.msg">
                  
                  <div class="leftBox" v-if="item2.FromUid == anotherUser.uid">
                    <span style="font-size: 4px">{{ item2.time }}span
                    >{{ item2.msg }}
                  div>
                  <div class="myBr" v-if="item2.FromUid == anotherUser.uid">div>
                  
                  <div class="rightBox" v-if="item2.FromUid == user.uid">
                    <span style="font-size: 4px">{{ item2.time }}span
                    >{{ item2.msg }}
                  div>
                  <div class="myBr" v-if="item2.FromUid == user.uid">div>
                div>
              el-main>
              
              <el-main class="inputValue">
                <textarea v-model="inputValue" id="chat" cols="26" rows="5">
                textarea>
                
                <el-button
                  v-if="
                    anotherUser != null && anotherUser.uid > 0 && inputValue != ''
                  "
                  type="success"
                  size="mini"
                  round
                  id="send"
                  @click="senMessage"
                  >发送el-button
                >
              el-main>
            el-container>
          el-main>
        el-container>
      div>
    template>
    
    <script>
    export default {
      data() {
        return {
          //自己
          user: {},
          //要私信的人
          anotherUser: {},
          //在线的用户
          userlist: [],
          //要私信的人在userlist的索引位置
          index: 0,
          //消息队列集合 [本人和第一个人之间的消息集合、本人和第二个人之间的消息集合、...]
          messageList: [],
          //新消息个数集合
          new_message_num: [],
          //将要发送的内容
          inputValue: "",
          //websocket
          websocket: null,
        };
      },
      methods: {
        //获取自己被分配的信息
        getYourInfo(uid) {
          let params = new URLSearchParams();
          this.$axios
            .post("/user/getYourInfo/" + uid, params)
            .then((res) => {
              this.user = res.data.data;
              if (res.data.code == 200) {
                //获取在线用户
                this.getUserList();
              }
            })
            .catch((err) => {
              console.error(err);
            });
        },
        //获取在线用户
        getUserList() {
          let params = new URLSearchParams();
          this.$axios
            .post("/user/getUserList", params)
            .then((res) => {
              this.userlist = res.data.data.filter(
                //去掉自己
                (user) => user.uid !== this.user.uid
              );
              //填充消息数据 messagelist:[[]、[]...]  并且将新消息队列置为0
              for (let i = 0; i < this.userlist.length; i++) {
                this.messageList.push([]);
                this.new_message_num.push(0);
              }
              //将当前的客户端和服务端进行连接,并定义接收到消息的处理逻辑
              this.init(this.user.uid);
            })
            .catch((err) => {
              console.error(err);
            });
        },
        //选择聊天对象
        selectUser(index) {
          this.anotherUser = this.userlist[index];
          this.index = index;
          //将新消息置为0
          this.new_message_num[index] = 0;
        },
        //将当前的客户端和服务端进行连接,并定义接收到消息的处理逻辑
        init(uid) {
          var self = this;
          if (typeof WebSocket == "undefined") {
            console.log("您的浏览器不支持WebSocket");
            return;
          }
          //清除之前的记录
          if (this.websocket != null) {
            this.websocket.close();
            this.websocket = null;
          }
          //-----------------------连接服务器-----------------------
          let socketUrl = "ws://localhost:8088/wechat/" + this.user.uid;
          //开启WebSocket 连接
          this.websocket = new WebSocket(socketUrl);
    
          //指定连接成功后的回调函数
          this.websocket.onopen = function () {
            console.log("websocket已打开");
          };
          //指定连接失败后的回调函数
          this.websocket.onerror = function () {
            console.log("websocket发生了错误");
          };
          //指定当从服务器接受到信息时的回调函数
          this.websocket.onmessage = function (msg) {
            //消息体例如{"FromUid":1,"ToUid":2,"msg":"你好","time":"00:07:03"} => message对象
            let data = JSON.parse(msg.data);
            //添加到对应的消息集合中
            let index = data.FromUid > uid ? data.FromUid - 2 : data.FromUid - 1;
            self.messageList[index].push(data);
            //新消息数+1
            self.new_message_num[index]++;
          };
          //指定连接关闭后的回调函数
          this.websocket.onclose = function () {
            console.log("websocket已关闭");
          };
        },
        //发送信息
        senMessage() {
          //消息体例如{"FromUid":1,"ToUid":2,"msg":"你好","time":"00:07:03"}
          let message = {
            FromUid: this.user.uid,
            ToUid: this.anotherUser.uid,
            msg: this.inputValue,
            time: new Date().toLocaleTimeString(),
          };
          //将消息插进消息队列,显示在前端
          this.messageList[this.index].push(message);
          //将消息发送至服务器端再转发到对应的用户
          this.websocket.send(JSON.stringify(message));
          //清空一下输入框内容
          this.inputValue = "";
        },
      },
      created() {
        let uid = this.$route.query.uid;
        if (uid != undefined) {
          //获取被分配的用户信息
          this.getYourInfo(uid);
        }
      },
    };
    script>
    
    <style>
    /*改变滚动条 */
    ::-webkit-scrollbar {
      width: 3px;
      border-radius: 4px;
    }
    
    ::-webkit-scrollbar-track {
      background-color: inherit;
      -webkit-border-radius: 4px;
      -moz-border-radius: 4px;
      border-radius: 4px;
    }
    
    ::-webkit-scrollbar-thumb {
      background-color: #c3c9cd;
      -webkit-border-radius: 4px;
      -moz-border-radius: 4px;
      border-radius: 4px;
    }
    .bg {
      background: url("https://s1.ax1x.com/2022/06/12/Xgr9u6.jpg") no-repeat top;
      background-size: cover;
      background-attachment: fixed;
      width: 100%;
      height: 100%;
      position: fixed;
      top: 0;
      left: 0;
      right: 0;
      bottom: 0;
    }
    .wechat {
      width: 60%;
      height: 88%;
      margin: 3% auto;
      border-radius: 20px;
      background-color: rgba(245, 237, 237, 0.3);
    }
    /*聊天框左侧 */
    .item {
      position: relative;
      width: 94%;
      height: 50px;
      margin-bottom: 3%;
      border-bottom: 1px solid #fff;
    }
    .item .name {
      line-height: 50px;
      float: left;
      margin-left: 10px;
    }
    /*聊天框右侧 */
    
    .wechat_right {
      position: relative;
      width: 100%;
      height: 100%;
    }
    .header {
      text-align: left;
      height: 50px !important;
    }
    .showChat {
      width: 100%;
      height: 65%;
    }
    .inputValue {
      position: relative;
      margin: 0;
      padding: 0;
      width: 100%;
      height: 50%;
    }
    .inputValue #chat {
      font-size: 18px;
      width: 96%;
      height: 94%;
      border-radius: 20px;
      resize: none;
      background-color: rgba(245, 237, 237, 0.3);
    }
    #send {
      position: absolute;
      bottom: 12%;
      right: 6%;
    }
    /*展示区 */
    .leftBox {
      float: left;
      max-width: 60%;
      padding: 8px;
      position: relative;
      font-size: 18px;
      border-radius: 12px;
      background-color: rgba(40, 208, 250, 0.76);
    }
    .rightBox {
      float: right;
      max-width: 60%;
      padding: 8px;
      font-size: 18px;
      border-radius: 12px;
      position: relative;
      background-color: rgba(101, 240, 21, 0.945);
    }
    .myBr {
      float: left;
      width: 100%;
      height: 20px;
    }
    .leftBox > span {
      left: 3px;
      width: 120px;
      position: absolute;
      top: -16px;
    }
    .rightBox > span {
      width: 120px;
      position: absolute;
      right: 3px;
      top: -16px;
    }
    style>
    

    源代码

    源代码

    WebSocketServer调用spring容器注意事项

    WebSocket启动的时候优先于spring容器,从而导致在WebSocketServer中调用业务Service会报空指针异常
    解决方法,静态初始化并提前加载bean

    1. 静态初始化
    //如需要 MessageService
    private static MessageService messageService;
    
    1. 提前加载bean
    @Configuration
    public class WebSocketConfig {
    
        /**
         * 注入ServerEndpointExporter,该Bean会自动注册使用@ServerEndpoint注解声明的websocket endpoint
         */
        @Bean
        public ServerEndpointExporter serverEndpointExporter() {
            return new ServerEndpointExporter();
        }
    
        //通过get方法注入bean
        @Autowired
        protected void getMessageService(MessageService ms){
            WebSocketServer.messageService=ms;
        }
    }
    

    扩展

    1. 群发实现
    2. 多人聊天实现
    3. 语音、视屏聊天实现

    相信前两个大家如果看明白上面的demo应该能做

    • 给消息设置一个状态,后端服务器接收到消息是群发之后,就将消息发送给所有的在线用户,不在线的先存数据库(或者维护一个uid数组,这样更灵活)
    • 定义一个群ID,将消息发送至群内的所有人
    • 这个我建议自己查查看

    使用netty实现了上面一模一样的功能
    netty+springboot+vue聊天室(需要了解netty)

  • 相关阅读:
    [Kettle] Excel输入
    Docker 部署 nacos 服务
    在Sora引爆视频生成时,Meta开始用Agent自动剪视频了
    mysql 客户端SSL错误2026 (HY000)
    车载软件架构 —— AUTOSAR Vector SIP包(二)
    2023-2024-1 for循环-1
    gbase 8a 基础语法概念问题
    MultipartFile 上传文件的踩坑点
    多线程JUC 篇 1.1juc的基本知识
    MacBook快捷键使用
  • 原文地址:https://blog.csdn.net/weixin_48557496/article/details/125246048