当前位置: 首页 > article >正文

SpringBoot与Vue实现WebSocket心跳机制

思路

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

后端

后端配置

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 {
    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }
}
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;
    // 当前连接的用户ID
    private 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();


    @Autowired
    public void setService(MessageService messageService) {
        WebSocketController.messageService = messageService;
    }

    // 当新的 WebSocket 连接建立时调用此方法
    @OnOpen
    public void onOpen(Session session, @PathParam("userId") String userId) {
        this.session = session;
        this.userId = userId;
        webSockets.add(this);
        sessionPool.put(userId, session);
    }

    // 当 WebSocket 连接关闭时调用此方法
    @OnClose
    public void onClose() {
        webSockets.remove(this);
        sessionPool.remove(this.userId);
    }

    // 当收到消息时调用此方法
    @OnMessage
    public void onMessage(String message) {
        try {
            // 心跳机制
            if (message.startsWith("ping")) {
                // 对心跳消息进行解析,获取用户ID
                String[] 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();
        }
    }


    // 当发生错误时调用此方法
    @OnError
    public void onError(Session session, Throwable error) {
        System.err.println("Error: " + error.getMessage());
    }


    // 处理消息发送的逻辑 messageService已另行实现
    private void handleMessageSend(Message msg, String msgText) {
        // 获取消息中的群组ID
        int groupId = msg.getGroupId();
        // 根据群组ID获取群组信息
        Group group = messageService.getGroupById(groupId);
        // 获取消息中的用户ID
        int 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
            // 发送者的用户ID
            int 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);
        }
    }


}

前端

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;
            }
        },
    },
});

前端使用


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.kler.cn/a/464975.html

相关文章:

  • SCAU期末笔记 - 数据库系统概念往年试卷解析
  • Megatron - LM 怎么进行模型切分的,怎么匹配服务器的
  • Django 中数据库迁移命令
  • Docker安装Prometheus和Grafana
  • 硬件-射频-PCB-常见天线分类-ESP32实例
  • 2025加密风云:行业变革与未来趋势全景透视
  • 华为数通考试模拟真题(附带答案解析)题库领取
  • GAN对抗生成网络(二)——算法及Python实现
  • 多输入多输出 | Matlab实现WOA-CNN鲸鱼算法优化卷积神经网络多输入多输出预测
  • C# 设计模式(行为型模式):责任链模式
  • 分布式微服务项目___某污水处理项目
  • Cornerstone3D:快速搭建可以读取本地文件且四视图显示的Nifti Viewer
  • golang后台框架总结
  • 计算机网络 (19)扩展的以太网
  • Centos 7.6 安装mysql 5.7
  • 静态库封装之ComDir类
  • 数据仓库建设方案和经验总结
  • 【Hackthebox 中英 Write-Up】Web Request | 分析 HTTP 请求和响应
  • C++高级编程技巧:模板元编程与性能优化实践
  • php Yii2 execl表格导出样式定义
  • 【ArcGISPro/GeoScenePro】解决常见的空间参考和投影问题
  • 并发编程系列(三) -synchronized关键字介绍
  • docker 部署nginx
  • 掌握 Dockerfile:格式、解析器指令、环境变量替换
  • uwsgi中指定了uid为nginx,通过subprocess调用conda时候仍尝试读取/root/.config/conda/.condarc
  • Tailwind CSS 实战:响应式布局最佳实践