当前位置:   article > 正文

在线聊天室(Vue+Springboot+WebSocket)_springboot和vue如何实现实时在线聊天

springboot和vue如何实现实时在线聊天

实现了一个简单的在线聊天室的前后端。前端用Vue实现,后端用Springboot实现。

一、项目描述

1. 整体功能描述

        在线聊天室的功能包括创建用户和显示在线用户列表、发送消息和显示消息列表、用户和消息列表实时更新这几点。以下是整体功能的活动图

2. 实现思路

用户身份

        进入聊天室的用户需要有一个身份,为了简便,只需要一个唯一的id和一个用户名即可。用户名由用户自定义,id由服务端分配。

        客户端通过将id和用户名记录在sessionStorage来保存用户信息,而服务端通过用户的id及session来区分用户,为此,服务端需要维护一个在线用户列表,来记录用户的信息。

        如活动图所示,当用户第一次进入聊天室时,需要输入用户名,请求服务端分配id,服务端分配id后,客户端进行记录。而用户曾进入过聊天室时,可以直接从sessionStorage中获取id和用户名。

通信方式

        客户端和服务端的通信方式有两种,首先,在线聊天需要两端的全双工通信,因此需要建立websocket连接。通过websocket进行通信。

        但是,如活动图所示,websocket的连接需要用到用户id,而用户在首次进入聊天室时还没有id,此时就需要客户端直接发送http请求到服务端,来获取一个id。

实时更新

        聊天室中的在线用户列表以及消息列表需要实时更新。此功能可以通过websocket实现,每当用户进入、退出聊天室,或发送消息时,客户端会通过websocket向服务端发送消息。服务端会维护一个在线用户列表和消息列表,当收到用户消息时,就会更新这个列表,并向所有用户发送更新消息,以达到实时更新的效果。

二、具体实现

1. 前端整体布局

        因为是一个在线聊天室,所以就参考了微信聊天的页面,左侧显示在线用户列表,右侧显示消息窗口。如下图:

        如图所示,左侧用简单卡片列表的形式展示在线用户列表,右侧是消息列表,通过调整样式,使自己的消息靠右显示,别人的消息靠左显示。以下是部分代码:

  1. <template>
  2. <div class="chatRoom">
  3. <div class="personList">
  4. <div class="title">
  5. <h1>在线聊天室</h1>
  6. </div>
  7. <!-- 在线用户列表 -->
  8. <div class="online-person">
  9. <span class="title">在线用户</span>
  10. <div class="person-cards-wrapper">
  11. <div
  12. class="personList"
  13. v-for="personInfo in personList"
  14. :key="personInfo.id"
  15. >
  16. <PersonCard :personInfo="personInfo"></PersonCard>
  17. </div>
  18. </div>
  19. </div>
  20. </div>
  21. <!-- 聊天窗口 -->
  22. <div class="chatContent">
  23. <ChatWindow></ChatWindow>
  24. </div>
  25. </div>
  26. </template>
  27. <!-- 聊天窗口组件 -->
  28. <template>
  29. <div class="chat-window">
  30. <div class="bottom">
  31. <!-- 聊天信息列表 -->
  32. <div class="chat-content" ref="chatContent">
  33. <div class="chat-wrapper" v-for="item in chatList" :key="item.id">
  34. <!-- 通过uid判断是自己的消息还是别人的 -->
  35. <div class="chat-friend" v-if="item.uid != $store.state.id">
  36. <div class="chat-text">
  37. {{ item.msg }}
  38. </div>
  39. <div class="info-time">
  40. <span>{{ item.name }}</span>
  41. <span>{{ item.time }}</span>
  42. </div>
  43. </div>
  44. <div class="chat-me" v-else>
  45. <!-- 略 -->
  46. </div>
  47. </div>
  48. </div>
  49. <div class="chatInputs">
  50. <input class="inputs" v-model="inputMsg" @keyup.enter="sendText" />
  51. <div class="send box" @click="sendText">
  52. <span class="sendText">发送</span>
  53. </div>
  54. </div>
  55. </div>
  56. </div>
  57. </template>

2. 用户身份

        用户身份的实现方式在实现思路中已经给出,这里不再说明。此外,前端除了在sessionStorage中存储用户信息,还会利用Vuex存储用户信息。


3. 前后端通信

        前后端的通信有两种方式,首先是直接发送http请求,比如用户请求分配id时,这里是采用axios的方式像后端接口发送请求。

        然后是websocket连接,前端需要始终与后端保持websocket连接。需要注意的是,建立连接的时机需要保证在用户得到id后。以下是部分代码:

 前端:

  1. <!-- 输入用户名的对话框 -->
  2. <div v-if="showDialog" class="dialog-wrapper">
  3. <div class="dialog">
  4. <h2 class="dialog-title">请输入您的昵称</h2>
  5. <el-input
  6. v-model="inputName"
  7. class="dialog-content"
  8. placeholder="请输入内容"
  9. maxlength="10"
  10. ></el-input>
  11. <button @click="closeDialog" class="dialog-button">确定</button>
  12. </div>
  13. </div>
  14. <script>
  15. mounted() {
  16. // 如果sessionStorage中无用户id,则需输入用户名,分配id
  17. if (!sessionStorage.hasOwnProperty("id")) {
  18. this.openDialog();
  19. } else {
  20. // 如果sessionStorage中有,则无需分配,直接用vuex存储
  21. this.$store.dispatch("setUserId", sessionStorage.getItem("id"));
  22. this.$store.dispatch("setUserName", sessionStorage.getItem("name"));
  23. console.log("增加已有用户:" + this.$store.state.name);
  24. addUser({ id: this.$store.state.id, name: this.$store.state.name }).then(
  25. (res) => {
  26. // 建立websocket连接,要保证addUser后服务端才发送更新消息
  27. this.$store.dispatch("connect");
  28. }
  29. );
  30. }
  31. },
  32. methods: {
  33. openDialog() {
  34. this.showDialog = true;
  35. },
  36. closeDialog() {
  37. if (this.inputName.length == 0) {
  38. this.$message({
  39. message: "用户名不能为空",
  40. type: "warning",
  41. });
  42. } else {
  43. newUser(this.inputName).then((res) => {
  44. // 将获取到的id用vuex存储
  45. this.$store.dispatch("setUserId", res);
  46. this.$store.dispatch("setUserName", this.inputName);
  47. // 将数据存储到sessionStorage
  48. sessionStorage.setItem("id", res);
  49. sessionStorage.setItem("name", this.inputName);
  50. // 建立websocket连接,需要用户id, 因此在分配id后进行
  51. this.$store.dispatch("connect");
  52. });
  53. this.showDialog = false;
  54. }
  55. },
  56. },
  57. </script>
  1. export const newUser = (param) => {
  2. return axios({
  3. method: 'post',
  4. baseURL: `${baseUrl}/user/new/${param}`,
  5. }).then(res => res.data)
  6. }
  7. export const addUser = (param) =>{
  8. return axios({
  9. method: 'post',
  10. baseURL: `${baseUrl}/user/add`,
  11. data: param,
  12. }).then(res => res.data)
  13. }

后端:

  1. @RestController
  2. @RequestMapping("user")
  3. public class UserController {
  4. // 用户第一次进入,分配id
  5. @PostMapping("new/{name}")
  6. public int newUser(@PathVariable String name) {
  7. System.out.println(name);
  8. User user = new User(++Storage.cur_id, name);
  9. Storage.userList.put(user.getId(), user);
  10. return Storage.cur_id;
  11. }
  12. // 用户再次进入,无需分配id
  13. @PostMapping("add")
  14. public void addUser(@RequestBody User user) {
  15. System.out.println(user);
  16. Storage.userList.put(user.getId(), user);
  17. }
  18. }
  1. public class MyWebSocket {
  2. private Session session;
  3. private String userId;
  4. //concurrent包的线程安全Set,用来存放每个客户端对应的MyWebSocket对象。
  5. private static final CopyOnWriteArraySet<MyWebSocket> webSockets = new CopyOnWriteArraySet<>();
  6. // 用来存在线连接用户信息
  7. private static final ConcurrentHashMap<String, Session> sessionPool = new ConcurrentHashMap<String, Session>();
  8. // 连接成功调用的方法
  9. @OnOpen
  10. public void onOpen(Session session, @PathParam(value = "userId") String userId) {
  11. this.session = session;
  12. this.userId = userId;
  13. webSockets.add(this);
  14. sessionPool.put(userId, session); // 存储用户的session
  15. // 有新用户连接后,需要通知所有用户更新在线用户列表
  16. sendAllMessage(JSON.toJSONString(new SocketMessage(1, Storage.getUserList(), null)));
  17. sendOneMessage(userId, JSON.toJSONString(new SocketMessage(2, null, Storage.getMsgList())));
  18. }
  19. // 连接关闭调用的方法
  20. @OnClose
  21. public void onClose() {
  22. webSockets.remove(this);
  23. sessionPool.remove(this.userId);
  24. // 将用户从在线列表中删掉,并通知所有用户
  25. Storage.userList.remove(Integer.parseInt(this.userId));
  26. sendAllMessage(JSON.toJSONString(new SocketMessage(1, Storage.getUserList(), null)));
  27. }
  28. // 收到客户端消息后调用的方法
  29. @OnMessage
  30. public void onMessage(String message) {
  31. Message msg = JSON.parseObject(message, Message.class);
  32. Storage.msgList.add(msg);
  33. sendAllMessage(JSON.toJSONString(new SocketMessage(2, null, Storage.getMsgList())));
  34. }
  35. // 单点消息
  36. public void sendOneMessage(String userId, String message) {
  37. Session session = sessionPool.get(userId);
  38. if (session != null && session.isOpen()) {
  39. log.info("【websocket消息】 单点消息:" + message);
  40. session.getAsyncRemote().sendText(message);
  41. }
  42. }
  43. // 广播消息
  44. public void sendAllMessage(String message) {
  45. for (MyWebSocket webSocket : webSockets) {
  46. try {
  47. if (webSocket.session.isOpen()) {
  48. webSocket.session.getAsyncRemote().sendText(message);
  49. }
  50. } catch (Exception e) {
  51. e.printStackTrace();
  52. }
  53. }
  54. }
  55. }

4. 发送消息与实时更新

        聊天室需要保证用户列表与消息列表的实时更新,这里利用了Vue的响应式数据与Vuex,通过Vuex存储用户列表与消息列表,然后在Vue组件中监听它们的变化,每当有变化时,就更新自身的数据,从而使页面实时更新。

        而Vuex中数据的变化,是通过WebSocket通信实现的,实现方式在实现思路部分已经给出,这里不再说明。以下是部分代码:

  1. import Vue from 'vue';
  2. import Vuex from 'vuex';
  3. Vue.use(Vuex);
  4. const state = {
  5. socket: null,
  6. id: "",
  7. name: "",
  8. userList: [],
  9. msgList: [],
  10. };
  11. const mutations = {
  12. setSocket(state, socket) {
  13. state.socket = socket;
  14. },
  15. setId(state, id) {
  16. state.id = id;
  17. },
  18. setName(state, name) {
  19. state.name = name;
  20. },
  21. setUserList(state, userList) {
  22. state.userList = userList;
  23. },
  24. setMsgList(state, msgList) {
  25. state.msgList = msgList;
  26. },
  27. }
  28. const actions = {
  29. connect(context) {
  30. const socket = new WebSocket('ws://localhost:8089/chat/' + context.state.id)
  31. socket.onopen = () => {
  32. console.log("建立websocket连接")
  33. }
  34. socket.onmessage = (event) => {
  35. // 接收消息的情况有两种:更新用户列表和更新消息列表
  36. console.log("收到服务端的消息:" + event.data);
  37. let msg = JSON.parse(event.data);
  38. if (msg.type == '1') {
  39. console.log("更新用户列表: ");
  40. context.commit("setUserList", msg.userList);
  41. } else if (msg.type == '2') {
  42. console.log("更新消息列表");
  43. context.commit("setMsgList", msg.msgList);
  44. }
  45. }
  46. context.commit("setSocket", socket)
  47. },
  48. setUserId(context, id) {
  49. console.log("设置用户id为:" + id);
  50. context.commit("setId", id);
  51. },
  52. setUserName(context, name) {
  53. console.log("设置用户name为:" + name);
  54. context.commit("setName", name);
  55. },
  56. sendMsg(context, msg) {
  57. context.state.socket.send(msg);
  58. }
  59. };
  60. export default new Vuex.Store({
  61. actions,
  62. mutations,
  63. state,
  64. });
  1. export default {
  2. data() {
  3. return {
  4. chatList: [],
  5. inputMsg: "",
  6. };
  7. },
  8. methods: {
  9. //获取窗口高度并滚动至最底层
  10. scrollBottom() {
  11. this.$nextTick(() => {
  12. const scrollDom = this.$refs.chatContent;
  13. scrollDom.scrollTop = scrollDom.scrollHeight;
  14. });
  15. },
  16. //发送文字信息
  17. sendText() {
  18. if (this.inputMsg) {
  19. const now = new Date();
  20. const year = now.getFullYear();
  21. const month = ("0" + (now.getMonth() + 1)).slice(-2);
  22. const day = ("0" + now.getDate()).slice(-2);
  23. const hours = ("0" + now.getHours()).slice(-2);
  24. const minutes = ("0" + now.getMinutes()).slice(-2);
  25. const seconds = ("0" + now.getSeconds()).slice(-2);
  26. const formattedTime =
  27. year +
  28. "-" +
  29. month +
  30. "-" +
  31. day +
  32. " " +
  33. hours +
  34. ":" +
  35. minutes +
  36. ":" +
  37. seconds;
  38. let msg = {
  39. name: this.$store.state.name,
  40. time: formattedTime,
  41. msg: this.inputMsg,
  42. uid: this.$store.state.id,
  43. };
  44. // 向websocket服务端发送消息
  45. this.$store.dispatch("sendMsg", JSON.stringify(msg));
  46. this.inputMsg = "";
  47. } else {
  48. this.$message({
  49. message: "消息不能为空",
  50. type: "warning",
  51. });
  52. }
  53. },
  54. },
  55. computed: {
  56. getChatMsgList() {
  57. return this.$store.state.msgList;
  58. },
  59. },
  60. watch: {
  61. // 更新消息列表
  62. getChatMsgList: {
  63. handler: function (newVal, oldVal) {
  64. console.log("更新chatList");
  65. this.chatList = this.getChatMsgList;
  66. this.scrollBottom();
  67. },
  68. deep: true,
  69. },
  70. },
  71. };

三、代码仓库

ChatRoom: 简单的在线聊天网站的前后端

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/从前慢现在也慢/article/detail/576589
推荐阅读
  

闽ICP备14008679号