Springboot Websocket一般封装方法

news2025/2/22 22:12:54

此方法不是唯一,只是自己对于Springboot中关于Websocket处理思路比较清晰的一种,在此记录下来。总共不过就四个文件而已。

一、创建Springboot项目,添加不可或缺的pom

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

由于需要跟http请求对接,所以一定要有web的依赖

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

二、配置文件WebsocketConfiguration

package com.chris.modules.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.server.standard.ServerEndpointExporter;

/**
 * @author Chris Chan
 * Create On 2022/11/23 13:22
 * Use for:
 * Explain:
 */
@EnableWebSocket
@Configuration
public class WebsocketConfiguration {
    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }
}

这是最简单的内容了,不过就是创建了一个bean而已。

三、抽象定义文件

package com.chris.modules.ws;

import javax.websocket.Session;
import java.util.concurrent.CopyOnWriteArraySet;

/**
 * @author Chris Chan
 * Create On 2022/11/23 14:37
 * Use for:
 * Explain:
 */
public interface Websocket {
    /**
     * @author Chris Chan
     * Create On 2022/11/23 13:09
     * Use for: 规范连接回调
     * Explain:
     */
    interface LinkPoint {
        //连接时回调
        void onOpen(Session session, String auth);

        //收到消息时回调
        void onMessage(String message);

        //连接关闭时回调
        void onClose();

        //发生错误时回调
        void onError(Session session, Throwable throwable);
    }

    /**
     * @author Chris Chan
     * Create On 2022/11/23 13:15
     * Use for: 客户端
     * Explain:
     */
    interface Client {
        //获取会话
        Session getSession();

        //获取标记
        String getTag();

        //发送文本
        void sendText(String text);

        //发送对象
        void send(Object object);
    }

    /**
     * @author Chris Chan
     * Create On 2022/11/23 13:12
     * Use for: 管理行为
     * Explain:
     */
    interface Manager<T extends Client> {
        //向指定客户端发送文本
        void sendText(String text, T... clients);

        //向所有客户端发送文本
        void sendTextYoAll(String text);

        //向指定客户端发送对象
        void send(Object object, T... clients);

        //向所有客户端发送对象
        void sendToAll(Object object);

        //向其他客户端发送文本
        void sendTextToOther(String text, T... clients);

        //向其他客户端发送对象
        void sendToOther(Object object, T... clients);

        //添加客户端
        void addClients(T... clients);

        //获取所有客户端
        CopyOnWriteArraySet<T> all();

        //移除客户端
        void removeClients(T... clients);

        //根据标记获取客户端
        T getClientByTag(String tag);

        //根据标记获取多个客户端
        T[] getClientsByTags(String... tags);
    }
}

这里本来是三个接口,后来我把他们放到一个文件里面了。其中LinkPoint规范了四个回调方法。Client声明了客户端需要用到的几个行为,Manager为管理类定义了一系列行为。

四、实现客户端

package com.chris.modules.impl;

import com.chris.modules.ws.Websocket.Client;
import com.chris.modules.ws.Websocket.LinkPoint;
import org.springframework.stereotype.Component;

import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;

/**
 * @author Chris Chan
 * Create On 2022/11/23 13:26
 * Use for: 实现一个客户端
 * Explain:
 */
@Component
@ServerEndpoint("/chat/{auth}")
public class DemoClient implements LinkPoint, Client {
    private static DemoManager demoManager = DemoManager.getInstance();
    private String tag;
    private Session session;

    @OnOpen
    @Override
    public void onOpen(Session session, @PathParam("auth") String auth) {
        this.session = session;
        this.tag = auth;

        demoManager.addClients(this);
        String msg = tag + " 上线了" + "目前在线 " + demoManager.all().size() + " 人";
        System.out.println(msg);
        demoManager.sendTextToOther(msg, this);
    }

    @OnMessage
    @Override
    public void onMessage(String message) {
        String msg = tag + ": " + message;
        System.out.println(msg);
        demoManager.sendTextToOther(msg, this);
    }

    @OnClose
    @Override
    public void onClose() {
        demoManager.removeClients(this);
        String msg = tag + " 下线了,目前在线 " + demoManager.all().size() + " 人";
        System.out.println(msg);
        demoManager.sendTextToOther(msg);
    }

    @OnError
    @Override
    public void onError(Session session, Throwable throwable) {
        System.out.println("出错");
        try {
            session.close();
            demoManager.removeClients(this);
            String msg = tag + " 离线了,目前在线 " + demoManager.all().size() + " 人";
            System.out.println(msg);
            demoManager.sendTextToOther(msg);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    @Override
    public Session getSession() {
        return this.session;
    }

    @Override
    public String getTag() {
        return this.tag;
    }

    @Override
    public void sendText(String text) {
        try {
            session.getBasicRemote().sendText(text);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void send(Object object) {
        try {
            session.getBasicRemote().sendObject(object);
        } catch (IOException | EncodeException e) {
            e.printStackTrace();
        }
    }
}

其中的auth原本的设计是token,使用来识别客户端身份的,可以增加校验逻辑。目前整体逻辑是按照聊天的业务来实现的。

五、实现管理类

package com.chris.modules.impl;

import com.chris.modules.ws.Websocket.Manager;

import java.util.Arrays;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.stream.Collectors;

/**
 * @author Chris Chan
 * Create On 2022/11/23 13:32
 * Use for:
 * Explain:
 */
public class DemoManager implements Manager<DemoClient> {
    private static CopyOnWriteArraySet<DemoClient> linkSet = new CopyOnWriteArraySet<>();

    //单例
    private static DemoManager instance = new DemoManager();

    private DemoManager() {
    }

    public static DemoManager getInstance() {
        return instance;
    }

    @Override
    public void sendText(String text, DemoClient... clients) {
        for (DemoClient client : clients) {
            client.sendText(text);
        }
    }

    @Override
    public void sendTextYoAll(String text) {
        for (DemoClient demoClient : linkSet) {
            demoClient.sendText(text);
        }
    }

    @Override
    public void send(Object object, DemoClient... clients) {
        for (DemoClient client : clients) {
            client.send(object);
        }
    }

    @Override
    public void sendToAll(Object object) {
        for (DemoClient demoClient : linkSet) {
            demoClient.send(object);
        }
    }

    @Override
    public void sendTextToOther(String text, DemoClient... clients) {
        Set<String> tagSet = Arrays.stream(clients).map(DemoClient::getTag).collect(Collectors.toSet());
        for (DemoClient demoClient : linkSet) {
            if (tagSet.contains(demoClient.getTag())) {
                continue;
            }
            demoClient.sendText(text);
        }
    }

    @Override
    public void sendToOther(Object object, DemoClient... clients) {
        Set<String> tagSet = Arrays.stream(clients).map(DemoClient::getTag).collect(Collectors.toSet());
        for (DemoClient demoClient : linkSet) {
            if (tagSet.contains(demoClient.getTag())) {
                continue;
            }
            demoClient.send(object);
        }
    }

    @Override
    public void addClients(DemoClient... clients) {
        linkSet.addAll(Arrays.asList(clients));
    }

    @Override
    public CopyOnWriteArraySet<DemoClient> all() {
        return linkSet;
    }

    @Override
    public void removeClients(DemoClient... clients) {
        for (DemoClient client : clients) {
            linkSet.remove(client);
        }
    }

    @Override
    public DemoClient getClientByTag(String tag) {
        for (DemoClient demoClient : linkSet) {
            if (demoClient.getTag().equals(tag)) {
                return demoClient;
            }
        }
        return null;
    }

    @Override
    public DemoClient[] getClientsByTags(String... tags) {
        if (null == tags || tags.length == 0) {
            return null;
        }
        Set<String> tagSet = Arrays.stream(tags).collect(Collectors.toSet());
        List<DemoClient> clientList = linkSet.stream().filter(c -> tagSet.contains(c.getTag())).collect(Collectors.toList());
        DemoClient[] clients = new DemoClient[clientList.size()];
        clientList.toArray(clients);
        return clients;
    }
}

本来管理类可以使用一个静态类就够了,但是为了用一个接口来规范行为,所以设计成了单例模式。其中对客户端的管理使用了并发包下面的set集合。

到现在,一个拥有实时聊天的逻辑就具备了。运行起来,用一个网络客户端就可以模拟聊天了。

 一般在设计使用长连接时,主要负责下行数据传输,上行需求一般还是倚靠http接口来解决。我们写两个接口。

package com.chris.modules.web;

import com.chris.modules.impl.DemoClient;
import com.chris.modules.impl.DemoManager;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author Chris Chan
 * Create On 2022/11/23 14:21
 * Use for:
 * Explain:
 */
@RestController
@RequestMapping("api/ws")
public class WsController {
    private static DemoManager demoManager = DemoManager.getInstance();

    /**
     * 向指定客户端发送消息
     *
     * @param auth
     * @param tags
     * @param msg
     * @return
     */
    @GetMapping("send")
    public String send(String auth, String tags, String msg) {
        //模拟auth发送者,这里可根据http请求携带的token来分析
        DemoClient authCli = demoManager.getClientByTag(auth);
        //获取目标客户端
        DemoClient[] clients = demoManager.getClientsByTags(tags.split(","));
        //发送消息
        demoManager.sendText(authCli.getTag() + ":" + msg, clients);
        return "send success.";
    }

    /**
     * 向指定客户端发送消息
     *
     * @param auth
     * @param tags
     * @param msg
     * @return
     */
    @GetMapping("sendToOther")
    public String sendToOther(String auth, String tags, String msg) {
        DemoClient authCli = demoManager.getClientByTag(auth);
        DemoClient[] clients = demoManager.getClientsByTags(tags.split(","));
        demoManager.sendTextToOther(authCli.getTag() + ":" + msg, clients);
        return "send success.";
    }
}

尝试调用:

http://localhost:8088/api/ws/send?auth=chris&tags=chris,bill&msg=hello,chris and bill

 正式使用的时候,还需要为下行数据封装一个数据协议,以适应不同的也无需求。

补充:

上述确定上行用http,websocket尽在下行的时候使用。所以有Springboot的服务调用Websocket客户端,没有反向的调用。

加入我们的业务允许上行,比如实时聊天,需要将数据写入数据库,这是就需要调用Springboot的服务。但是直接在ServerEndpoint中装载服务是不行的。我们需要做一点修改。

首先创建一个service,模拟一个简单的逻辑。

package com.chris.modules.service;

import org.springframework.stereotype.Service;

/**
 * @author Chris Chan
 * Create On 2022/11/23 15:20
 * Use for:
 * Explain:
 */
@Service
public class DemoService {
    public void show(String msg){
        System.out.println(msg+" --Springboot服务调用");
    }
}

修改DemoClient,把服务作为一个静态成员加入。

 创建一个监听器,在spring上下文装载完成之后,把服务的bean传给client。

package com.chris.modules.config;

import com.chris.modules.impl.DemoClient;
import com.chris.modules.service.DemoService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationListener;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.event.ContextRefreshedEvent;

/**
 * @author Chris Chan
 * Create On 2022/11/23 15:22
 * Use for:
 * Explain:
 */
@Configuration
public class AppStartedListener implements ApplicationListener<ContextRefreshedEvent> {
    @Autowired
    DemoService demoService;
    @Override
    public void onApplicationEvent(ContextRefreshedEvent event) {
        DemoClient.setDemoService(demoService);
    }
}

这样就可以在有上行数据之后,调用Springboot的服务了。

 

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

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

相关文章

内存模块 --- 基础扫盲

SDRAM synchronous dynamic random-access memory 同步动态随机存储器 同步&#xff1a;需要同步时钟支持工作&#xff0c;内部命令的发送与数据的传输都是以这个时钟为基准 动态&#xff1a;内部的存储阵列需要不断地刷新来保证数据不丢失 随机&#xff1a;可以自由地在指定地…

MyBatis进阶版

本文有点难 目录 1.一些区分 1.1参数占位符#{}和${} 1.1.1SQL注入 1.1.2like查询 1.2resultType和resultMap 2.映射查询 2.1一对一表映射 2.2一对多表映射 3.动态SQL 3.1标签 3.2标签 3.3标签 3.4标签 3.5标签 1.一些区分 1.1参数占位符#{}和${} ①什么是#{}&a…

【CSS】表布局,浮动布局

❤️ Author&#xff1a; 老九 ☕️ 个人博客&#xff1a;老九的CSDN博客 &#x1f64f; 个人名言&#xff1a;不可控之事 乐观面对 &#x1f60d; 系列专栏&#xff1a; 文章目录表布局表层表标题(caption)border-collapse边框隐藏表大小table-layout(表宽度)vertical-align例…

你安全吗?丨虎云系统“后门”

作者&#xff1a;黑蛋 在电视剧《你安全吗&#xff1f;》中&#xff0c;马平川这个人物已经慢慢浮出水面&#xff0c;算是此部电视剧幕后的最大反派&#xff0c;他明面上是虎迫集团的技术总监&#xff0c;是虎云系统的负责人&#xff0c;开发者。背后却在泰曼达有着诈骗基地&a…

2022年11月21日13:32:00——T5——JS对象与Date日期函数

1、JavaScrip对象的使用&#xff1a; /** * 1、对象的声明赋值使用的是{}花括号&#xff0c;大括号 * 2、对象中的值以key:value的格式进行赋值&#xff0c;多个值中间使用【,】区分 * 3、获取对象中的值的方法需要通过[]并给与key名的方式获取&…

电脑怎么设置开机密码?简单几步给你的电脑“上锁”

在我们日常生活中&#xff0c;最常见的就是开机密码了&#xff0c;而电脑作为我们使用频率最高也是最复杂的设备&#xff0c;其安全性也是非常重要的&#xff0c;那么电脑怎么设置开机密码&#xff1f;那么今天我们就来说一说开机密码的设置方法&#xff01;为了更安全&#xf…

Essay写作字数怎么正确进行删减?

对于留学生来说&#xff0c;Essay写作伴随着整个留学生活。正因为留学生大量的Essay作业&#xff0c;不少留学生为了规定的Essay字数而发愁&#xff0c;毕竟既不能写得太少&#xff0c;又不能写得太多&#xff0c;所以很纠结。那么如何根据老师规定的字数进行删减呢&#xff1f…

Talk | 微软亚洲研究院宋恺涛南大余博涛:面向文本/音乐序列任务的Transformer注意力机制设计

本期为TechBeat人工智能社区第456期线上Talk&#xff01; 北京时间11月22日(周二)20:00&#xff0c;微软亚洲研究院研究员——宋恺涛与南京大学硕士研究生——余博涛的Talk将准时在TechBeat人工智能社区开播&#xff01; 他们与大家分享的主题是: “面向文本/音乐序列任务的Tra…

String类_Java(一)

作者&#xff1a;爱塔居的博客_CSDN博客-JavaSE领域博主 专栏&#xff1a;JavaSE &#x1f33c;作者简介&#xff1a;大三学生&#xff0c;希望跟大家一起进步&#xff01; 文章目录 目录 文章目录 前言 一、构造字符串 二、Sring对象的比较 2.1 比较是否引用同一对象 2.2 比较…

跨平台编译工具--CMake上手教程

文章目录一、引入二、基本关键字1.PROJECT2.SET3.MESSAGE4.ADD_EXECUTABLE5.ADD_SUBDIRECTORY(1)使用(2)CMakeLists执行顺序(3)输出文件的位置6.INSTALL(1)安装文件(2)安装非目标文件可执行文件(3)安装目录(4)安装指令7.ADD_LIBRARY8.SET_TARGET_PROPERTIES三、语法的基本规则四…

知识引擎藏经阁天花板——高性能Java架构核心原理手册

开场 本书是按照程序设计与架构的顺序编写的&#xff0c;共13章。 第1章介绍学习高性能Java应了解的核心知识&#xff0c;为前置内容。 第2章和第3章讲解在编写代码之前&#xff0c;如何高效地为My SQL填充亿级数据&#xff0c;并对My SQL进行基准测试&#xff0c;以便在之后…

Linux-awk和printf

printf printf ‘输出类型输出格式’ 输内容 输出类型&#xff1a; %ns 输出字符串&#xff0c;n是数字指代输出几个字符 %ni 输出整数&#xff0c;n是数字&#xff0c;指代输出几个数字 %m.nf 输出浮点数&#xff0c;m和n是数字&#xff0c;指代输出总位数和小数位数&#xf…

YOLO V1学习总结

图片大小&#xff1a;448 * 448 —> 7 * 7 *&#xff08;5 * B C&#xff09; 5&#xff1a;每个框的x,y,w,h,confidence; B2&#xff1a;在7*7的feature上&#xff0c;每个cell会生成2个预测框&#xff1b; C&#xff1a;类别数。 损失函数 坐标中心误差和位置宽高的误差…

卷积神经网络基本概念

卷积神经网络基本概念1. 感受野2. 卷积核3. 特征图【feature map】4. 通道【channel】5. 填充【padding】6. 步长【stride】7. 池化【pooling】8. dropout数字1处&#xff1a;一个圈表示一个神经元数字2处&#xff1a;一个圈表示一个神经元&#xff0c;圈的大小表示感受野的大小…

基于matlab的最小支配集CDS仿真

目录 1.算法描述 2.仿真效果预览 3.MATLAB部分代码预览 4.完整MATLAB程序 1.算法描述 支配集的定义如下&#xff1a;给定无向图G &#xff08;V , E&#xff09;,其中V是点集&#xff0c; E是边集&#xff0c; 称V的一个子集S称为支配集当且仅当对于V-S中任何一个点v, 都有…

一、FFmpeg 的初尝试《FFmpeg 音视频开发基础入门到实战》

学习目标 了解 FFmpeg学习 FFmpeg 工具的下载及环境配置了解 FFmpeg 工具的使用方式了解 FFmpeg play 的使用方法了解 FFmpeg paly 的音量设置、窗口设置、音量设置等设置方法 一、了解 FFmpeg FFmpeg 是一个音视频处理的工具&#xff0c;通过 FFmpeg 可以对视频进行旋转、缩…

新零售SaaS架构:多租户系统架构设计

什么是多租户&#xff1f; 多租户是SaaS领域的特有产物&#xff0c;在SaaS服务中&#xff0c;租户是指使用SaaS系统的客户&#xff0c;租户不同于用户&#xff0c;例如&#xff0c;B端SaaS产品&#xff0c;用户可能是某个组织下的员工&#xff0c;但整个企业组织是SaaS系统的租…

得数据者得天下!作为后端开发必备技能之一的MySQL,这份十多年经验总结的应用实战与性能调优我想你肯定是需要的!

MySQL对于很多Linux从业者而言&#xff0c;是一个非常棘手的问题&#xff0c;多数情况都是因为对数据库出现问题的情况和处理思路不清晰。在进行MySQL的优化之前必须要了解的就是MySQL的查询过程&#xff0c;很多的查询优化工作实际上就是遵循一些原则让MySQL的优化器能够按照预…

跑步戴什么耳机比较好、精挑五款最佳跑步耳机推荐

运动蓝牙耳机近几年受到市场的欢迎&#xff0c;种类越来越多&#xff0c;各类功能也日益五花八门&#xff0c;消费者很难准确的进行分辨&#xff0c;一不小心可能买到华而不实的产品。现在了解一下值得入手的运动蓝牙耳机&#xff0c;从多个角度对蓝牙耳机进行评估后&#xff0…

大数据项目之电商数仓、实时数仓同步数据、离线数仓同步数据、用户行为数据同步、日志消费Flume配置实操、日志消费Flume测试、日志消费Flume启停脚本

文章目录8. 实时数仓同步数据9. 离线数仓同步数据9.1 用户行为数据同步9.1.1 数据通道9.1.1.1 用户行为数据通道9.1.2 日志消费Flume配置概述9.1.2.1 日志消费Flume关键配置9.1.3 日志消费Flume配置实操9.1.3.1 创建Flume配置文件9.1.3.2 配置文件内容如下9.1.3.2.1 配置优化9.…