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)

  • 19
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
以下是一个简单的使用Spring BootVue.jsWebSocket实现的聊天室的代码示例: Spring Boot后端代码: ```java @Configuration @EnableWebSocket public class WebSocketConfig implements WebSocketConfigurer { @Override public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { registry.addHandler(new WebSocketHandler(), "/chat").setAllowedOrigins("*"); } @Bean public ObjectMapper objectMapper() { return new ObjectMapper(); } } class WebSocketHandler extends TextWebSocketHandler { private static final Map<WebSocketSession, String> users = new ConcurrentHashMap<>(); @Override public void afterConnectionEstablished(WebSocketSession session) { users.put(session, "Anonymous"); } @Override protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception { ChatMessage chatMessage = new ObjectMapper().readValue(message.getPayload(), ChatMessage.class); if (chatMessage.getType() == ChatMessage.MessageType.JOIN) { users.put(session, chatMessage.getSender()); } for (WebSocketSession user : users.keySet()) { user.sendMessage(new TextMessage(new ObjectMapper().writeValueAsString(chatMessage))); } } @Override public void afterConnectionClosed(WebSocketSession session, CloseStatus status) { users.remove(session); } } @Data @AllArgsConstructor @NoArgsConstructor class ChatMessage { public enum MessageType { CHAT, JOIN, LEAVE } private String sender; private String content; private MessageType type; public static ChatMessage joinMessage(String sender) { return new ChatMessage(sender, "", MessageType.JOIN); } public static ChatMessage leaveMessage(String sender) { return new ChatMessage(sender, "", MessageType.LEAVE); } } @RestController public class ChatController { @GetMapping("/users") public List<String> users() { return new ArrayList<>(WebSocketHandler.users.values()); } } ``` Vue.js前端代码: ```html <template> <div> <h2>Chat Room</h2> <div> <label>Your name:</label> <input v-model="name" @keyup.enter="join" /> <button @click="join">Join</button> </div> <div v-if="joined"> <div> <label>Message:</label> <input v-model="message" @keyup.enter="send" /> <button @click="send">Send</button> </div> <div> <h3>Users:</h3> <ul> <li v-for="user in users" :key="user">{{ user }}</li> </ul> </div> <div> <h3>Chat:</h3> <ul> <li v-for="chat in chats" :key="chat.id"> <strong>{{ chat.sender }}:</strong> {{ chat.content }} </li> </ul> </div> </div> </div> </template> <script> import SockJS from "sockjs-client"; import Stomp from "stompjs"; export default { data() { return { name: "", message: "", joined: false, chats: [], users: [], stompClient: null, }; }, methods: { join() { const socket = new SockJS("/chat"); this.stompClient = Stomp.over(socket); this.stompClient.connect({}, () => { this.stompClient.subscribe("/topic/chat", (message) => { const chat = JSON.parse(message.body); if (chat.type === "JOIN") { this.users.push(chat.sender); } else if (chat.type === "LEAVE") { this.users.splice(this.users.indexOf(chat.sender), 1); } this.chats.push(chat); }); this.stompClient.send( "/app/chat", JSON.stringify(ChatMessage.joinMessage(this.name)) ); this.joined = true; }); }, send() { this.stompClient.send( "/app/chat", JSON.stringify( new ChatMessage(this.name, this.message, ChatMessage.MessageType.CHAT) ) ); this.message = ""; }, }, }; class ChatMessage { static MessageType = { CHAT: "CHAT", JOIN: "JOIN", LEAVE: "LEAVE", }; constructor(sender, content, type) { this.sender = sender; this.content = content; this.type = type; } static joinMessage(sender) { return new ChatMessage(sender, "", ChatMessage.MessageType.JOIN); } static leaveMessage(sender) { return new ChatMessage(sender, "", ChatMessage.MessageType.LEAVE); } } </script> ``` 在这个示例中,我们使用了Spring BootWebSocket支持来处理来自客户端的事件。我们创建了一个WebSocket处理程序,它维护了一个用户会话列表,并在用户加入、离开或发送聊天消息时广播消息到所有连接的客户端。我们还为WebSocket处理程序创建了一个控制器,以便在客户端请求所有当前连接的用户列表时返回它们。 在Vue.js应用程序中,我们使用SockJS和Stomp.js来建立与服务器的WebSocket连接,并处理来自服务器的事件。我们使用Vue.js的数据绑定来更新聊天消息、用户列表和用户输入框中的数据,并在加入聊天室、发送聊天消息或断开连接时发送相关的WebSocket事件。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值