Websocket+SpringBoot+Vuex实现点对点聊天系统

概述

本篇文章主要记录下我是怎么在项目中实现点对点聊天功能的。关于Websocket和Stomp的概念就不再赘述,直接上代码。

后端代码

Maven

		<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-websocket</artifactId>
        </dependency>

		<dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        
		<!--项目中未使用SpringSecurity的可以忽略-->
		<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>

配置类

configureClientInboundChannel()相当于拦截器,是拦截Websocket连接发送消息前执行的,这里可以用来对每次sendMessage的用户做权限验证。如果项目里无需验证可以忽略重写此方法。

/**
 * WebSocket配置类
 */
@Slf4j
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
// 	以下注入的都是为了SpringSecurity鉴权的
    @Value("${jwt.tokenHead}")
    private String tokenHead;
    @Autowired
    private JwtUtil jwtTokenUtil;
    @Autowired
    private UserDetailsService userDetailsService;

    /**
     * 添加这个Endpoint,这样在网页可以通过websocket连接上服务
     * 也就是我们配置websocket的服务地址,并且可以指定是否使用socketJS
     * @param registry
     */
    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        /**
         * 1.将ws-pets路径注册为stomp的端点,用户连接了这个端点就可以进行websocket通讯,支持
         socketJS
         * 2.setAllowedOrigins("*"):允许跨域
         * 3.withSockJS():支持socketJS访问
         */
        registry.addEndpoint("/ws/pets").setAllowedOriginPatterns("*").withSockJS();
    }

    /**
     * 输入通道参数拦截配置,可以不写
     * @param registration
     */
    @Override
    public void configureClientInboundChannel(ChannelRegistration registration) {
        registration.interceptors(new ChannelInterceptor() {
            @Override
            public Message<?> preSend(Message<?> message, MessageChannel channel) {
                StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
                //判断是否为连接,如果是,需要获取token,并且设置用户对象
                if (StompCommand.CONNECT.equals(accessor.getCommand())){
                    String token = accessor.getFirstNativeHeader("Auth-Token");
                    if (!StringUtils.isEmpty(token)){
                        String authToken = token.substring(tokenHead.length());
                        String username = jwtTokenUtil.getUsernameByToken(authToken);
                        //token中存在用户名
                        if (!StringUtils.isEmpty(username)){
                            //登录
                            UserDetails userDetails = userDetailsService.loadUserByUsername(username);
                            //验证token是否有效
                            if (jwtTokenUtil.validateToken(authToken,userDetails)){
                                UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
                                SecurityContextHolder.getContext().setAuthentication(authenticationToken);
                                accessor.setUser(authenticationToken);
                            }
                        }
                    }
                }
                return message;
            }
        });
    }

    /**
     * 配置消息代理
     * @param registry
     */
    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
        //配置代理域,可以配置多个,配置代理目的地前缀,可以在配置域上向客户端推送消息
        registry.enableSimpleBroker("/broadcast","/message");
//        //设置服务端接收消息的前缀,只有下面注册的前缀的消息才会接收
//        registry.setApplicationDestinationPrefixes("/app");
    }

}

消息实体

定义双方之间发送消息的对象。

/**
 * 聊天消息
 */
@Data
@Accessors(chain = true)
@EqualsAndHashCode(callSuper = false)
public class ChatMsg {
	//发送者唯一标识
    private String from;
    //接收方唯一标识
    private String to;
    //内容
    private String content;

	//发送时间
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss",timezone = "Asia/Shanghai")
    @JsonDeserialize(using = LocalDateTimeDeserializer.class)
    @JsonSerialize(using = LocalDateTimeSerializer.class)
    private LocalDateTime date;
    //发送者用户名
    private String fromNickName;
}

Controller代码

SimpMessagingTemplate是SpringBoot为我们提供发送消息用的统一模板类。
@MessageMapping可以理解为@GetMappring


/**
 * websocket控制器
 */
@Slf4j
@RestController
public class WebSocketController {
    @Autowired
    private SimpMessagingTemplate simpMessagingTemplate;

	//Authentication是SpringSecurity提供的全局对象,用来获取登录成功的User,若没用到 SpringSecurity可删除此形参
    @MessageMapping("/sendMsg")
    public void handleMsg(Authentication authentication, ChatMsg chatMsg){
        MyUserDetails userDetails = (MyUserDetails)authentication.getPrincipal();
        //获取发送者的用户名
        User user = userDetails.getUser();
        
        chatMsg.setFrom(user.getUsername());
        chatMsg.setFromNickName(user.getNickName());
        /**
         * 点对点发送消息
         * 1.消息接收者
         * 2.消息队列
         * 3.消息对象
         * 消息的类型默认是/user,这个是websocket对单个客户端发送消息特殊的消息类型
         */
        log.info("用户[{}]发送消息=========={}",user.getNickName(), chatMsg);
        simpMessagingTemplate.convertAndSendToUser(chatMsg.getTo(),"/message/chat",chatMsg);
    }

}

写到这里后端的Websocket基础框架就搭好了,接下来写前端代码测试一下能不能实现单聊。

Vue前端代码

Vuex

我把用户聊天模块的数据全部交给Vuex的一个模块来管理,从而实现多个页面之间数据的共享。
目录结构
在这里插入图片描述
chat.js就是我们放聊天数据和聊天用户数据的地方。

chat.js

别忘了npm install stomp,具体版本如下

	"sockjs-client": "^1.5.1",
    "stompjs": "^2.3.3",
import Stomp from 'stompjs'
import SockJS from 'sockjs-client'
import { Notify } from 'vant';
import Vue from "vue";
import {getUsers} from "../../api/chat";
import store from "../index";

const chat = {
  namespaced: true,
  state: {
    sessions: []//会话,是一个map,储存聊天信息
    currentAdmin: JSON.parse(window.localStorage.getItem('user')),//当前用户
    admins: [],//所有聊天对象
    currentSession: null,//当前聊天对象
    sockJs: null,
    stomp: null,
  },
  mutations:{
    //改变当前聊天会话session
    changeCurrentSession(state, currentSession) {
      state.currentSession = currentSession;
      console.log('当前聊天用户:' + state.currentSession.username)
    },
    //添加一条消息进session
    addMessage(state, msg) {
      //会话key的定义 自己的username+’#‘+对方的username
      const sessionKey = state.currentAdmin.username + '#' + msg.to;
      //找到该会话,如果会话从未创建就初始化,然后把message push进去
      let mss = state.sessions[sessionKey];
      if (!mss) {
        Vue.set(state.sessions, sessionKey, []);
      }
      state.sessions[sessionKey].push({
        content: msg.content,
        date: new Date(),
        self: !msg.notSelf//是否是自己发的消息
      })
      console.log(state.sessions)
    },
    //设置所有聊天用户
    INIT_ADMIN(state, data) {
      state.admins = data;
    }
  },
  actions: {
    //连接websocket
    connect(context) {
      context.dispatch('initChatUsers')//获取所有聊天用户
      const { state } = context
      //连接wbsocket
      let socket = new SockJS('/ws/pets')
      state.stomp = Stomp.over(socket);
      const token = store.state.user.token;
      //连接携带鉴权token
      state.stomp.connect({'Auth-Token': token}, success => {
        //订阅聊天消息,注意加上默认前缀/user,这点在后端代码已经指出,点对点通信的默认前缀
        state.stomp.subscribe('/user/message/chat', msg => {
          let receiveMsg = JSON.parse(msg.body);
          //当前不在消息页面或者正在和另一个人聊天,消息提示
          if (!state.currentSession || receiveMsg.from != state.currentSession.username){
            Notify({type: 'primary',message: receiveMsg.fromNickName+'发来了信息'})
          }
          //接收到的消息设为不是自己发的
          receiveMsg.notSelf = true;
          receiveMsg.to = receiveMsg.from;
          //收到的别人的消息放进session
          context.commit('addMessage', receiveMsg);
        })
      }, error => {
      })

      //监听窗口关闭
      window.onbeforeunload = function (event) {
        socket.close()
      }
    },

    //自己发送消息
    sendMessage({ commit, state}, msgObj){
      state.stomp.send('/sendMsg', {}, JSON.stringify(msgObj));
      //自己发送的消息添加进session
      commit('addMessage',msgObj);
    },
    //初始化所有聊天用户,向后端请求数据
    initChatUsers(context) {
      getUsers().then(res=>{
        if (res.data){
          context.commit('INIT_ADMIN', res.data.users);
        }
      })
    }
  }
}


export default chat

getters.js

getter的chatMessages是取出某一个会话的聊天记录的

const getters = {
  token: state => state.user.token,
  user: state => state.user.userInfo,
  chatMessages: state => {
    return state.chat.sessions[state.chat.currentAdmin.username+'#'+state.chat.currentSession.username]
  }
}
export default getters

main.js

main.js中加入导函守卫这段代码,这是websocket连接的入口,如果用户已经具有token说明处于登录状态,若此时stomp未初始化的话,就去连接后端。

import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'

router.beforeEach(async (to, from, next) => {

  if (store.getters.token){
    //用户已登录且未连接websocket
    if (store.state.chat.stomp == null){
      await store.dispatch('chat/connect')
    }
    next()
  }else {
    await next({name:'Login'})
  }

})


new Vue({
  router,
  store,
  render: h => h(App)
}).$mount('#app')

HTML

页面部分html、样式什么的太多了,这里只给出重要的代码。

联系人列表页面

在这里插入图片描述

<script>
  import {getAllUsers, searchUsers} from "../../api/chat";
  import {mapState} from 'vuex'
  export default {
    name: "Chat",
    data(){
      return{
        searchStr: '',
        displayUsers: [],
        self: this.$store.getters.user
      }
    },
    mounted() {
      this.displayUsers = this.admins
    },
    computed: {
      ...mapState('chat',['currentSession', 'admins']),
    },
    watch:{
      searchStr(val, oldVal){
        if (!val){
          this.displayUsers = this.admins
        }
      }
    },
    methods:{
    //改变当前会话
      changeCurrentSession(session) {
        this.$store.commit('chat/changeCurrentSession',session)
        this.$router.push({
          name: 'ChatDetail'
        })
      },
      //搜索用户
      search(){
        if (this.searchStr){
          searchUsers(this.searchStr).then(res => {
            this.displayUsers = res.data.users
            console.log(this.displayUsers);
          })
        }
      }
    }
  }
</script>

聊天页面

在这里插入图片描述
聊天页面就一个方法,就是发送聊天消息的方法

<script>
  import {mapState, mapMutations} from 'vuex'
  import MessageList from "./components/MessageList";
  import MessageHeader from "./components/MessageHeader";
  export default {
    //......................
    methods:{
    //发送聊天消息
      addMsg(e) {
        if (this.content.length) {
          let msgObj = new Object();
          msgObj.to = this.currentSession.username;
          msgObj.content = this.content;
          msgObj.self = true;
          this.$store.dispatch('chat/sendMessage', msgObj)
          this.content = '';
        }
      }
    }
    //............
  }
</script>

测试

在这里插入图片描述
控制台出现这些log就说明连接websocket并订阅成功了。
在这里插入图片描述
在这里插入图片描述

总结

初步搭建已经完成了,但还有很多问题有待解决。
(1)消息持久化,聊天记录放在vuex一刷新页面就丢失了,应该在后端落库。
(2)用户在线状态,以及用户上下线的实时性提示
(3)消息的“已读”,“未读”功能怎么实现
(4)群聊怎么实现

  • 2
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
【资源介绍】 毕设项目:基于netty+websocket+springboot的实时聊天系统.zip毕设项目:基于netty+websocket+springboot的实时聊天系统.zip毕设项目:基于netty+websocket+springboot的实时聊天系统.zip毕设项目:基于netty+websocket+springboot的实时聊天系统.zip毕设项目:基于netty+websocket+springboot的实时聊天系统.zip毕设项目:基于netty+websocket+springboot的实时聊天系统.zip毕设项目:基于netty+websocket+springboot的实时聊天系统.zip毕设项目:基于netty+websocket+springboot的实时聊天系统.zip毕设项目:基于netty+websocket+springboot的实时聊天系统.zip毕设项目:基于netty+websocket+springboot的实时聊天系统.zip毕设项目:基于netty+websocket+springboot的实时聊天系统.zip毕设项目:基于netty+websocket+springboot的实时聊天系统.zip毕设项目:基于netty+websocket+springboot的实时聊天系统.zip毕设项目:基于netty+websocket+springboot的实时聊天系统.zip毕设项目:基于netty+websocket+springboot的实时聊天系统.zip 该项目是个人毕设项目,答辩评审分达到95分,代码都经过调试测试,确保可以运行!欢迎下载使用,可用于小白学习、进阶。 该资源主要针对计算机、通信、人工智能、自动化等相关专业的学生、老师或从业者下载使用,亦可作为期末课程设计、课程大作业、毕业设计等。 项目整体具有较高的学习借鉴价值!基础能力强的可以在此基础上修改调整,以实现不同的功能。
好的,下面是一个简单的示例代码,演示了如何在Spring Boot应用程序中使用STOMPWebSocket实现简单的消息传递。 首先,需要添加以下依赖项: ```xml <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-websocket</artifactId> </dependency> <dependency> <groupId>org.webjars</groupId> <artifactId>webjars-locator-core</artifactId> </dependency> <dependency> <groupId>org.webjars</groupId> <artifactId>sockjs-client</artifactId> <version>1.1.2</version> </dependency> <dependency> <groupId>org.webjars</groupId> <artifactId>stomp-websocket</artifactId> <version>2.3.3</version> </dependency> ``` 然后,在Spring Boot应用程序的主类上添加`@EnableWebSocketMessageBroker`注释,以启用WebSocket消息代理: ```java @SpringBootApplication @EnableWebSocketMessageBroker public class DemoApplication { public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); } } ``` 接下来,创建一个WebSocket配置类,用于配置STOMP消息代理: ```java @Configuration public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { @Override public void configureMessageBroker(MessageBrokerRegistry config) { config.enableSimpleBroker("/topic"); config.setApplicationDestinationPrefixes("/app"); } @Override public void registerStompEndpoints(StompEndpointRegistry registry) { registry.addEndpoint("/chat").withSockJS(); } } ``` 这里配置了一个简单的消息代理,用于处理来自`/app`前缀的应用程序消息和来自`/topic`前缀的广播消息。同时,配置了一个STOMP端点,允许使用SockJS协议进行WebSocket通信。 接下来,在控制器类中添加以下代码,处理来自客户端的消息: ```java @Controller public class ChatController { @MessageMapping("/chat") @SendTo("/topic/messages") public ChatMessage sendMessage(ChatMessage message) { return new ChatMessage(message.getFrom(), message.getText()); } } ``` 这里定义了一个处理`/chat`端点的方法,当接收到客户端发送的消息时,会将消息转发到`/topic/messages`频道上。`ChatMessage`是一个简单的POJO类,用于表示聊天消息。 最后,在前端页面中添加以下代码,用于与WebSocket服务器建立连接和发送消息: ```javascript var stompClient = null; function connect() { var socket = new SockJS('/chat'); stompClient = Stomp.over(socket); stompClient.connect({}, function (frame) { console.log('Connected: ' + frame); stompClient.subscribe('/topic/messages', function (chatMessage) { showMessage(JSON.parse(chatMessage.body)); }); }); } function sendMessage() { var from = document.getElementById('from').value; var text = document.getElementById('text').value; stompClient.send("/app/chat", {}, JSON.stringify({'from': from, 'text': text})); } function showMessage(chatMessage) { var messageArea = document.getElementById('messageArea'); messageArea.innerHTML += chatMessage.from + ': ' + chatMessage.text + '\n'; } ``` 这里使用SockJSSTOMP.js客户端库来与WebSocket服务器建立连接和发送消息。`connect()`方法用于建立连接,`sendMessage()`方法用于发送消息,`showMessage()`方法用于显示接收到的消息。 以上就是一个简单的示例,演示了如何在Spring Boot应用程序中使用STOMPWebSocket实现简单的消息传递。希望能帮到你。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值