SpringBoot与Vue实现WebSocket心跳机制

server/2025/1/6 5:40:20/

思路

前端每隔一段时间向后端发送一次字符串ping-${uid},后端收到后返回pong响应

后端

后端配置

java">package org.example.config;import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;@Configuration
public class WebSocketConfig {@Beanpublic ServerEndpointExporter serverEndpointExporter() {return new ServerEndpointExporter();}
}
java">package org.example.controller;import cn.hutool.json.JSONObject;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArraySet;@Component
@ServerEndpoint("/ws/{userId}")
public class WebSocketController {private Session session;// 当前连接的用户IDprivate String userId;// 存储所有的 WebSocket 连接private static final CopyOnWriteArraySet<WebSocketController> webSockets = new CopyOnWriteArraySet<>();// 存储用户ID和对应的会话,方便查找和管理private static final ConcurrentHashMap<String, Session> sessionPool = new ConcurrentHashMap<>();private static final ObjectMapper objectMapper = new ObjectMapper();@Autowiredpublic void setService(MessageService messageService) {WebSocketController.messageService = messageService;}// 当新的 WebSocket 连接建立时调用此方法@OnOpenpublic void onOpen(Session session, @PathParam("userId") String userId) {this.session = session;this.userId = userId;webSockets.add(this);sessionPool.put(userId, session);}// 当 WebSocket 连接关闭时调用此方法@OnClosepublic void onClose() {webSockets.remove(this);sessionPool.remove(this.userId);}// 当收到消息时调用此方法@OnMessagepublic void onMessage(String message) {try {// 心跳机制if (message.startsWith("ping")) {// 对心跳消息进行解析,获取用户IDString[] parts = message.split("-");String uid = parts[1]; // message格式:ping-uid// 获取对应的会话Session session = sessionPool.get(uid);if (session!= null && session.isOpen()) {// 发送心跳回复session.getBasicRemote().sendText("pong");}return;}// 将接收到的消息反序列化为 Message 对象Message msg = objectMapper.readValue(message, Message.class);// msg 是对象,message 是源文本handleMessageSend(msg, message);} catch (Exception e) {e.printStackTrace();}}// 当发生错误时调用此方法@OnErrorpublic void onError(Session session, Throwable error) {System.err.println("Error: " + error.getMessage());}// 处理消息发送的逻辑 messageService已另行实现private void handleMessageSend(Message msg, String msgText) {// 获取消息中的群组IDint groupId = msg.getGroupId();// 根据群组ID获取群组信息Group group = messageService.getGroupById(groupId);// 获取消息中的用户IDint Uid = msg.getUid();String nickname = messageService.getNicknameByUid(Uid);// 单人-------------if (group == null) {sendOneMessage(String.valueOf(groupId), msgText);return;}// 群聊-------------List<String> userIds = messageService.getUserIdsByGroupId(groupId);if (group.getMulti() == 1) { // Group chat// 发送者的用户IDint uid = msg.getUid();// 根据用户ID获取用户昵称、头像等信息nickname = userService.getUserByUid(uid).getNickname();// 在源文本消息中添加发送者信息JSONObject jsonObject = new JSONObject(msgText);jsonObject.append("nickname", nickname);String updatedMsgText = jsonObject.toString();// 给群组成员发送消息for (String userId : userIds) {if (userId.equals(this.userId)) continue;sendOneMessage(userId, updatedMsgText);}}}// 发送消息给单个用户public void sendOneMessage(String userId, String message) {Session session = sessionPool.get(userId);if (session!= null && session.isOpen()) {// 异步发送消息session.getAsyncRemote().sendText(message);}}}

前端

javascript">import { watch } from 'vue';
import { defineStore, storeToRefs } from 'pinia';
import notificationSound from '@/assets/notification.mp3';const wsurl = import.meta.env.VITE_WS_URL;
const myUid = localStorage.getItem('uid')export const useWsStore = defineStore('ws', {state: () => ({ws: null,}),actions: {async wsConnect(uid) {return new Promise((resolve, reject) => {this.ws = new WebSocket(`${wsurl}/ws/${uid}`);// 提示音const audio = new Audio(notificationSound);audio.volume = 0;audio.play().then(() => {// 静音播放audio.volume = 1; // 恢复音量}).catch((e) => {console.log(e);});this.ws.onopen = () => {this.startHeartBeat(); // 启动心跳resolve();};// ws连接关闭this.ws.onclose = () => {this.ws = new WebSocket(`${wsurl}/ws/${uid}`);};this.ws.onerror = (error) => {reject(error);};this.ws.onmessage = (e) => {let newMsg;try {newMsg = JSON.parse(e.data);} catch {newMsg = e.data; //pong消息,心跳回应return;}audio.play().catch((e) => { console.log(e) });};});},startHeartBeat() {this.heartBeatTimer = setInterval(() => {if (this.ws.readyState === WebSocket.OPEN) {this.ws.send(`ping-${myUid}`); // 发送ping消息} else {this.reconnectWebSocket();}}, 20000); // 每20秒发送一次心跳},stopHeartBeat() {if (this.heartBeatTimer) {clearInterval(this.heartBeatTimer);this.heartBeatTimer = null;}},reconnectWebSocket() {this.stopHeartBeat(); // 停止心跳if (this.ws) {this.ws.close();}this.wsConnect(myUid); // 重新连接WebSocket},wsSend(data) {if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {this.wsConnect(data.uid).then(() => {// 确保发送的信息是字符串this.ws.send(typeof data === "string" ? data : JSON.stringify(data));}).catch(error => {console.error(error);});} else {this.ws.send(typeof data === "string" ? data : JSON.stringify(data));}},disconnectWs() {this.stopHeartBeat();if (this.ws) {this.ws.close();this.ws = null;}},},
});

前端使用

javascript">
import { storeToRefs } from 'pinia';
import { useWsStore } from '@/store/wsStore'const wsStore = useWsStore()
const useWs = storeToRefs(wsStore)onMounted(() => {if (!useWs.ws) wsStore.connectWs(uid)
})await wsStore.wsSend(newMsg);const sendMsg = async (newMsg) => {await wsStore.wsSend(newMsg);
}


http://www.ppmy.cn/server/155787.html

相关文章

STM32 NOR FLASH(SPI FLASH)驱动移植(2)

2&#xff09;FLASH 读取函数 /* * brief 读取 SPI FLASH * note 在指定地址开始读取指定长度的数据 * param pbuf : 数据存储区 * param addr : 开始读取的地址(最大 32bit) * param datalen : 要读取的字节数(最大 65535) * retval 无 */ void norflash_read(uint8_t *pbuf…

面向对象分析与设计Python版 面向对象思维

文章目录 一、面向对象思想的起源二、面向对象的基本概念三、面向对象的思考方式 一、面向对象思想的起源 软件人才 软件人才从低到高4个成长层次&#xff1a;软件蓝领&#xff0c;软件工程师&#xff0c;卓越软件人才&#xff0c;领军人物卓越软件人才要求 系统分析和设计理…

SCAU软件体系结构期末复习-名词解释题

名词解释 软件设计模式&#xff1a; 是对软件设计经验的总结&#xff0c;是对软件设计中反复出现的设计问题的成功解决方案的描述。为了记录这些成功的设计经验并方便以后使用&#xff0c;软件设计模式通常包含4个基本要素&#xff1a;模式名称、问题、解决方案以及效果。它最…

使用Python实现实时视频处理与分析:解锁计算机视觉的无限可能

友友们好! 我的新专栏《Python进阶》正式启动啦!这是一个专为那些渴望提升Python技能的朋友们量身打造的专栏,无论你是已经有一定基础的开发者,还是希望深入挖掘Python潜力的爱好者,这里都将是你不可错过的宝藏。 在这个专栏中,你将会找到: ● 深入解析:每一篇文章都将…

Spring Boot 3 文件上传、多文件上传、大文件分片上传、文件流处理以及批量操作

在 Spring Boot 3 中&#xff0c;可以通过内置的文件处理机制结合 Java 的 IO 流与多线程技术&#xff0c;实现文件上传、多文件上传、大文件分片上传、文件流处理以及批量操作的需求。以下是详细实现步骤&#xff1a; 1. 单文件上传 控制器代码 import org.springframework…

MYSQL在Windows平台上的限制

以下限制适用于在Windows平台上使用MySQL&#xff1a; 程序内存 在windows32位上&#xff0c;一个进程&#xff08;包括MySQL&#xff09;内默认使用超过2GB的内存是不可能的。这是因为windows 32位的物理地址限制是4GB&#xff0c;视窗内的默认设置是在内核&#xff08;2GB&a…

如何利用Java获取SKU详细信息 API 数据

在电子商务领域&#xff0c;SKU&#xff08;Stock Keeping Unit&#xff09;信息对于库存管理、订单处理和客户服务至关重要。通过API获取SKU的详细信息可以帮助商家更有效地管理商品信息。本文将介绍如何利用Java语言获取SKU详细信息的API数据&#xff0c;并提供详细的代码示例…

51单片机——LED模块

LED模块中有8盏灯&#xff0c;从左到右为D1&#xff0c;D2&#xff0c;...&#xff0c;D8 8盏灯以二进制存储在单片机中&#xff0c;它们共同组成一个二进制 0 0 0 0 0 0 0 0 ---------------------------------------------- D8 D7 D6 D5 D…