springboot+webscoket通信功能

news2024/11/24 19:59:37

1. 背景

        项目上需要对某个页面的设计功能(低代码)进行最简单的多人协同,有以下需求点:

(1)第一个进入该设计页面的人给编辑权限,后进入的所有人给在线(可申请编辑)权限

(2)在线人员可申请编辑权限,编辑人员可进行【同意/决绝】操作

(3)申请后,除编辑人和申请人,其余人进入不可申请状态

(4)编辑人同意后,编辑人页面自动保存,并退出编辑,与其他人一起进入在线状态;申请人进入编辑状态

(5)整个过程需要在页面中显示在线人员列表

2. 问题

        按理说,这是一个很简单的websocket功能,创建配置类、操作类及通信方法实现业务,然后运行测试就行。如果不出意外的话,马上就出意外了。因为是结合的springboot,使用了@ServerEndpoint的方式注入,但因为项目中存在aop,导致出现了启动注册websocket失败的异常(至于为什么有aop就不行,请自行某度)。

3. 解决方案

3.1. 引入依赖

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

3.2. 创建拦截器

如有必要,在握手前、握手后方法中进行业务逻辑编码。

package cn.xxx.common.filter;

import java.util.Map;

import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.server.HandshakeInterceptor;

import lombok.extern.slf4j.Slf4j;

/**
 * WebSocket拦截器
 * 
 * @author xxx
 * @date: 2023-07-12 15:27:16
 * @Copyright: Copyright (c) 2006 - 2023
 * @Company: xxx
 * @Version: V1.0
 */
@Component
@Slf4j
public class CustomInterceptor implements HandshakeInterceptor {

    /**
     * 握手前
     *
     * @param request
     * @param response
     * @param wsHandler
     * @param attributes
     * @return
     * @throws Exception
     */
    @Override
    public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler,
        Map<String, Object> attributes) throws Exception {
        return true;
    }

    /**
     * 握手后
     *
     * @param request
     * @param response
     * @param wsHandler
     * @param exception
     */
    @Override
    public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler,
        Exception exception) {
        log.info("握手完成");
    }
}

3.3. 创建推送实体类

package cn.xxx.vo.websocket;

import lombok.Getter;
import lombok.Setter;

/**
 * 多人协同websocket用户对象
 * 
 * @author xxx
 * @date: 2023-07-11 11:29:19
 * @Copyright: Copyright (c) 2006 - 2023
 * @Company: xxx
 * @Version: V1.0
 */
@Getter
@Setter
public class MultiPersonCollaborationVo {
    /** 用户id(ip+用户id) */
    private String id;
    /** 用户状态:1.编辑;2.在线(可申请编辑);3.已申请;4.正在审批;5.不可操作; */
    private Integer status;
    /** 同意状态:1.同意;2.拒绝 */
    private Integer agree;
    /** 接收人id */
    private String recipientId;
}

3.4. 创建操作类

package cn.xxx.websocket;

import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import org.springframework.stereotype.Component;
import org.springframework.util.ObjectUtils;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;

import com.alibaba.fastjson.JSON;

import cn.xxx.vo.websocket.MultiPersonCollaborationVo;
import lombok.extern.slf4j.Slf4j;

/**
 * WebSocket操作类
 * 
 * @author xxx
 * @date: 2023-07-12 15:24:20
 * @Copyright: Copyright (c) 2006 - 2023
 * @Company: xxx
 * @Version: V1.0
 */
@Component
@Slf4j
public class WebSocketHandler extends TextWebSocketHandler {
    /** 会话 */
    private WebSocketSession session;
    /** 页面id */
    private String pageId;
    /** 在线人数 */
    public static int onlineNumber = 0;

    /** 以页面id为key,WebSocketHandler为对象保存起来 */
    private static Map<String, WebSocketHandler> clients = new ConcurrentHashMap<String, WebSocketHandler>();
    /** 在线人员列表 */
    private List<MultiPersonCollaborationVo> mpcList;

    /**
     * socket 建立成功事件
     *
     * @param session
     * @throws Exception
     */
    @Override
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
        Map<String, Object> paramMap = this.getUriParams(session.getUri().getQuery());
        String id = paramMap.get("loginip").toString() + "@" + paramMap.get("id").toString();
        onlineNumber++;
        log.info("已连接【会话id=" + session.getId() + ",用户id=" + id + "】");
        this.session = session;
        this.pageId = paramMap.get("pageId").toString();
        MultiPersonCollaborationVo mpc = new MultiPersonCollaborationVo();
        mpc.setId(id);
        // 如果是第一个用户,则设置为编辑者
        if (ObjectUtils.isEmpty(this.mpcList)) {
            this.mpcList = new ArrayList<>();
            mpc.setStatus(1);
        } else {
            // 后面进来的设置为在线
            mpc.setStatus(2);
        }
        this.mpcList.add(mpc);

        try {
            clients.put(this.pageId, this);
            // 推送结果
            Map<String, Object> result = new HashMap<String, Object>();
            result.put("status", 200);
            result.put("senderId", null);
            result.put("msg", null);
            result.put("object", this.mpcList);
            this.sendMessageTo(JSON.toJSONString(result), this.pageId);
        } catch (Exception e) {
            this.mpcList.remove(mpc);
            log.error("推送用户列表失败:" + e.toString());
        }

    }

    /**
     * 接收消息事件
     *
     * @param session
     * @param message
     * @throws Exception
     */
    @Override
    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
        try {
            log.info("来自客户端消息:" + message + "客户端的id是:" + session.getId());
            MultiPersonCollaborationVo param =
                JSON.parseObject(message.getPayload().toString(), MultiPersonCollaborationVo.class);
            // 找到发送者、接收者
            MultiPersonCollaborationVo sender = null;
            MultiPersonCollaborationVo recipient = null;
            for (MultiPersonCollaborationVo mpc : this.mpcList) {
                if (mpc.getId().equals(param.getId())) {
                    sender = mpc;
                    continue;
                }
                if (mpc.getId().equals(param.getRecipientId())) {
                    recipient = mpc;
                    continue;
                }
                if (null != sender && null != recipient) {
                    break;
                }
            }

            // 推送结果
            Map<String, Object> result = new HashMap<String, Object>();
            int status = 500;
            String senderId = null;
            String msg = null;
            if (null == sender) {
                msg = "发送者为空!";
            } else if (null != sender && null == recipient) {
                senderId = sender.getId();
                msg = "接收者不在线!";
            } else if (null != sender && null != recipient && sender.getId().equals(recipient.getId())) {
                senderId = sender.getId();
                msg = "不能推送消息给自己!";
            } else {
                // 判断接收状态
                // 发送人为编辑人
                if (1 == param.getStatus().intValue()) {
                    // 将所有用户状态设置为在线
                    for (MultiPersonCollaborationVo mpc : this.mpcList) {
                        mpc.setStatus(2);
                    }
                    // 判断审批是否同意
                    if (1 == param.getAgree().intValue()) {
                        // 同意时,设置接收人为编辑
                        recipient.setStatus(1);
                    } else if (2 == param.getAgree().intValue()) {
                        // 拒绝时,设置接收人为申请被拒绝
                        recipient.setStatus(2);
                        msg = "您的申请被拒绝了!";
                    }
                    senderId = recipient.getId();
                } else if (3 == param.getStatus().intValue()) {
                    // 发送人为申请编辑
                    // 将所有用户状态设置为不可编辑
                    for (MultiPersonCollaborationVo mpc : this.mpcList) {
                        mpc.setStatus(5);
                    }
                    // 设置发送人为已申请
                    sender.setStatus(3);
                    // 设置接收人为正在审批
                    recipient.setStatus(4);
                    senderId = sender.getId();
                }
                status = 200;
            }
            result.put("status", status);
            result.put("senderId", senderId);
            result.put("msg", msg);
            result.put("object", this.mpcList);
            sendMessageTo(JSON.toJSONString(result), this.pageId);
        } catch (Exception e) {
            log.info("发生了错误了");
        }
    }

    /**
     * socket 断开连接时
     *
     * @param session
     * @param status
     * @throws Exception
     */
    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
        Map<String, Object> paramMap = this.getUriParams(session.getUri().getQuery());
        String id = paramMap.get("loginip").toString() + "@" + paramMap.get("id").toString();
        onlineNumber--;
        if (!ObjectUtils.isEmpty(this.mpcList)) {
            Iterator<MultiPersonCollaborationVo> mpcIt = this.mpcList.iterator();
            while (mpcIt.hasNext()) {
                if (mpcIt.next().getId().equals(id)) {
                    mpcIt.remove();
                }
            }
        }

        try {
            // 如果用户列表为空,则删除
            if (ObjectUtils.isEmpty(this.mpcList)) {
                clients.remove(this.pageId);
                log.info(this.pageId + "所有人员已退出");
            } else {
                // 推送结果
                Map<String, Object> result = new HashMap<String, Object>();
                result.put("status", 200);
                result.put("senderId", null);
                result.put("msg", null);
                result.put("object", this.mpcList);
                sendMessageTo(JSON.toJSONString(result), this.pageId);
                log.info("用户【" + id + "】退出,当前在线人数" + onlineNumber);
            }
        } catch (IOException e) {
            log.info("推送在线列表异常:" + e.toString());
        }
    }

    /**
     * 获取uri的参数
     * 
     * @author: caip
     * @date: 2023-07-12 16:18:26
     * @param param
     * @return
     */
    private Map<String, Object> getUriParams(String param) {
        Map<String, Object> result = new HashMap<>();
        String[] array = param.split("&");
        for (String s : array) {
            result.put(s.split("=")[0], s.split("=")[1]);
        }
        return result;
    }

    /**
     * 推送消息到
     * 
     * @author: caip
     * @date: 2023-07-11 10:23:42
     * @param message
     * @param ToPageId
     * @throws IOException
     */
    public void sendMessageTo(String message, String toPageId) throws IOException {
        for (WebSocketHandler item : clients.values()) {
            if (item.pageId.equals(toPageId)) {
                item.session.sendMessage(new TextMessage(message));
                break;
            }
        }
    }
}

3.5. 创建配置类

package cn.xxx.common.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
import org.springframework.web.socket.server.standard.ServletServerContainerFactoryBean;

import cn.xxx.common.filter.CustomInterceptor;
import cn.xxx.websocket.WebSocketHandler;
import lombok.RequiredArgsConstructor;

/**
 * WebSocket配置类
 * 
 * @author xxx
 * @date: 2023-07-11 08:51:14
 * @Copyright: Copyright (c) 2006 - 2023
 * @Company: xxx
 * @Version: V1.0
 */
@Configuration
@EnableWebSocket
@RequiredArgsConstructor
public class WebSocketConfig implements WebSocketConfigurer {
    private final WebSocketHandler httpAuthHandler;
    private final CustomInterceptor customInterceptor;

    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry.addHandler(httpAuthHandler, "ws").addInterceptors(customInterceptor).setAllowedOrigins("*");
    }

    @Bean
    public ServletServerContainerFactoryBean createWebSocketContainer() {
        ServletServerContainerFactoryBean container = new ServletServerContainerFactoryBean();
        container.setMaxSessionIdleTimeout(600000L);
        return container;
    }
}

4. 测试结果

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/752359.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

使用MQTTX和前端vue进行通讯

需求&#xff1a;根据后端给的接口&#xff0c;前端实现消息订阅和消息加密连接操作&#xff0c;不走后端直接和硬件设备进行操作 1.下载mqttx 官网链接&#xff1a;MQTTX: Your All-in-one MQTT Client Toolbox 根据自己电脑选择不同的操作系统&#xff0c;默认下载后是英文…

金鸣表格识别中何时应勾选“手写”选项?

在金鸣表格文字识别系统的表格识别模块中&#xff0c;有个“手写”的复选框可供用户选择性使用。这里的“手写”是手写识别的简称&#xff0c;设置此项的目的是为了让用户更准确地识别手写的表格图片中的文字。为何要单独设置这个选项而不是由程序全自动地进行处理呢&#xff1…

【GitOps系列】K8s极简实战

文章目录 示例应用介绍部署应用到k8s 如何使用命名空间隔离团队及应用环境&#xff1f;如何为业务选择最适合的工作负载类型&#xff1f;如何解决服务发现问题&#xff1f;如何迁移应用配置&#xff1f;如何将集群的业务服务暴露外网访问&#xff1f;如何保障业务资源需求和自动…

JavaWeb(3)——HTML、CSS、JS 快速入门

一、JavaScript 运算符 • 赋值运算符&#xff08; &#xff09; 赋值运算符执行过程&#xff1f; 将等号右边的值赋予给左边, 要求左边必须是一个容器 出现是为了简化代码, 比如让 let age 18 &#xff0c;age 加 2 怎么写呢 let age 18age 2console.log(age)age * 2con…

html+JavaScript实现一个好看的颜色码查询器,支持查询、转换、颜色选择器和颜色码对照表

前言 相信大家平时工作的时候应该会经常用到颜色码吧&#xff0c;比如说想找个好看的颜色&#xff0c;或者有个颜色码但是不知道这个码是什么颜色的&#xff0c;这个时候我们就可以用颜色码对照表或者颜色码查询来查看了。 当然也可以用截图软件或者取色器或者PS来查看&#…

如何有效检测、识别和管理 Terraform 配置漂移?

作者&#xff5c;Krishnadutt Panchagnula 翻译&#xff5c;Seal软件 链接&#xff5c;https://betterprogramming.pub/detecting-identifying-and-managing-terraform-state-drift-997366a74537 在理想的 IaC 世界中&#xff0c;我们所有的基础设施实现和更新都是通过将更新的…

【高并发】高并发架构实战:从需求分析到系统设计

Yan-英杰的主页 悟已往之不谏 知来者之可追 C程序员&#xff0c;2024届电子信息研究生 很多软件工程师的职业规划是成为架构师&#xff0c;但是要成为架构师很多时候要求先有架构设计经验&#xff0c;而不做架构师又怎么会有架构设计经验呢&#xff1f;那么要如何获得架构设…

Cesium 测距、测面功能实现

参考博主 功能代码参考 新需求&#xff1a;点击测距&#xff0c;此时画线逻辑已生成到运行缓存中&#xff0c;如果 用户误触测距&#xff0c;想撤销&#xff0c;如何操作&#xff1f; 代码&#xff1a; // 重置画图resetDraw(){// 清除可能会用到的监听事件if (this.handle…

操作系统17:外存组织方式和文件存储管理

目录 1、外存的组织方式 &#xff08;1&#xff09;连续组织方式 &#xff08;2&#xff09;链接组织方式 2.1 - 隐式链接 2.2 - 显式链接 &#xff08;3&#xff09;索引组织方式 3.1 - 单级索引组织方式 3.2 - 多级索引组织方式 3.3 - 增量式索引组织方式 2、文件存…

【操作系统】几种基本页面置换算法的基本思想和流程图

目录 一、概述二、最佳置换算法&#xff08;OPT&#xff09;三、先进先出置换算法&#xff08;FIFO&#xff09;四、最近最久未使用置换算法&#xff08;LRU&#xff09;五、三种页面置换算法优缺点对比六、运行结果七、总结 一、概述 在地址映射过程中&#xff0c;若在页面中发…

Linux 发行版 Gentoo 存在重大漏洞

导读网络安全公司 SonarSource 在日前研究中发现&#xff0c;Gentoo Linux 发行版中存在漏洞 CVE-2023-28424&#xff0c;黑客可以利用该漏洞进行 SQL 注入攻击。 研究人员从 GentooLinux 的 Soko 搜索组件中找到了这个漏洞。该漏洞的 CVSS 风险评分为 9.1&#xff0c;属于特别…

6款开源中文OCR使用介绍(亲测效果)

文章目录 前言开源ocr项目1. Paddle OCR&#xff08;推荐指数&#xff1a;★★★★★&#xff09;1.1 简介1.2 使用1.3 优缺点 2. CnOCR&#xff08;推荐指数&#xff1a;★★★★★&#xff09;2.1 简介2.2 使用2.3 优缺点 3. chinese_lite OCR&#xff08;推荐指数&#xff1…

保障AI时代的图像安全:揭示解决虚假图片危机的三种策略

写在前面从 P 图到假图批量生成&#xff0c;AI 图像安全成可信 AI 重点关注方向三大技术&#xff1a;提前布局&#xff0c;合合信息 AI 图像安全技术助力行业健康发展✔ AI 图像篡改检测技术✔ 生成式图像鉴别技术✔ OCR 对抗攻击技术 一项标准&#xff1a;与中国信通院等权威机…

在本机搭建自己的ftp服务器--最简单的方法(详细教程)

在本机搭建自己的ftp服务器–最简单的方法 FTP服务器可以在局域网中快速传输文件&#xff0c;是在互联网上提供文件存储和访问服务的计算机&#xff0c;它们依照FTP协议提供服务。 FTP是File Transfer Protocol(文件传输协议)。顾名思义&#xff0c;就是专门用来传输文件的协议…

vue-next-admin跨域配置

vue-next-admin&#xff0c;这是基于 vue3.x CompositionAPI typescript vite element plus vue-router-next next.vuex&#xff0c;适配手机、平板、pc 的后台开源免费模板库 这是个开源免费的后台管理系统&#xff0c;从v2到v3&#xff0c;变化比较大&#xff0c;但是…

Windows系统安装配置Oracle数据库连接工具PLSQL

1.解压连接工具所需轻桌面压缩包 直接将轻桌面压缩包解压到一个自定义路径下&#xff08;三个里面选择其中一个&#xff0c;推荐选择第一个轻桌面包&#xff09;&#xff0c;后面的环境变量会用到。 2.配置Windows环境变量 NLS_LANG AMERICAN_AMERICA.AL32UTF8 ORACLE_HOME …

VMware安装Ubuntu(VMware版本17-Ubuntu版本16.0)

VMware安装Ubuntu&#xff08;VMware版本17-Ubuntu版本16.0&#xff09; 一&#xff0c;VMware虚拟机下载官网点击https://customerconnect.vmware.com/cn/downloads/info/slug/desktop_end_user_computing/vmware_workstation_pro/17_0 二&#xff0c;Ubuntu乌班图下载官网点…

解决Vue项目打包后dist中的index.html用浏览器直接打开显示空白页的问题

目录 场景描述 问题分析 解决方案 vue-cli2项目通过修改index.html引用路径或添加配置信息 方案一&#xff1a;将index.html中引用的绝对路径改为相对路径 方案二&#xff1a;修改项目的assetsPublicPath或添加publicPath配置信息 vue-cli3项目通过修改index.html引用路…

Mars3d采用ellipsoid球实现模拟地球旋转效果

1.Mars3d采用ellipsoid球实现模拟地球旋转效果 2.开始自选装之后&#xff0c;模型一直闪烁 http://mars3d.cn/editor-vue.html?idgraphic/entity/ellipsoid 3.相关代码&#xff1a; import * as mars3d from "mars3d"export let map // mars3d.Map三维地图对象 …

Linux基础服务10——虚拟化kvm

文章目录 一、基本了解二、安装kvm2.1 部署准备2.2 安装基础服务2.3 安装web管理服务 三、web界面管理3.1 添加kvm主机3.2 存储管理3.2.1 上传镜像3.2.2 扩容存储池 3.3 网络管理3.4 创建虚拟机3.5 报错处理3.5.1 Server disconnected3.5.1 文件句柄问题 一、基本了解 什么是虚…