Spring-boot 集成 SocketIO(看这一篇就够了)

news2024/9/27 5:49:48

1 前言

1.1 什么是 SocketIO ?

      Socket.IO 是一个可以在浏览器与服务器之间实现实时、双向、基于事件的通信的工具库。 Socket.IO 能够在任何平台、浏览器或设备上运行,可靠性和速度同样出色。

1.2 websocket和socket.io区别?

  • websocket
    a:一种让客户端和服务器之间能进行双向实时通信的技术
    b:使用时,虽然主流浏览器都已经支持,但仍然可能有不兼容的情况
    c:适合用于client和基于node搭建的服务端使用
  • socket.io
    a:将WebSocket、AJAX和其它的通信方式全部封装成了统一的通信接口
    b:使用时,不用担心兼容问题,底层会自动选用最佳的通信方式
    c:适合进行服务端和客户端双向数据通信
    d:Socket.IO中文网地址:https://socket.nodejs.cn/docs/v4/

1.3 应用及版本

  • 本文是基于若依前后端分离版本的基础上进行代码编写和演示的
  • spring-boot:2.5.14
  • socketio:2.0.3
  • jdk:java8

2 物料准备(均为后端代码)

2.1 添加 Socket 依赖包

<dependency>
    <groupId>com.corundumstudio.socketio</groupId>
    <artifactId>netty-socketio</artifactId>
    <version>2.0.3</version>
</dependency>

2.2 创建频道常量类:SocketEventContants

  • 我这个常量类是为了统一频道所建,你们不一定需要这个类
package com.mss.common.constant;

/**
 * @Description: Socket 自定义事件名称
 * @Author: zhanleai
 */
public class SocketEventContants {

    /**
     * 用户频道
     **/
    public static final String CHANNEL_USER = "channel_user";

    /**
     * 系统频道
     **/
    public static final String CHANNEL_SYSTEM = "channel_system";

}

2.3 创建 Socket 连接类:SocketHandler

  • 用来监听 socket 客户端上下线,以及服务端自动关闭;
  • 有些博主把这个类的内容跟工具类里监听事件方法放在一起,个人认为需要解耦;
package com.mss.framework.handle;

import com.corundumstudio.socketio.SocketIOServer;
import com.mss.common.utils.socket.SocketUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import org.springframework.stereotype.Component;

/**
 * @Author: zhanleai
 * @Description: 客户端自动连接和断开、服务端关闭
 */
@Component
@Slf4j
public class SocketHandler {

    @Autowired
    private SocketIOServer socketIoServer;

    /**
     *  容器销毁前,自动调用此方法,关闭 socketIo 服务端
     *
     * @Param []
     * @return
     **/
    @PreDestroy
    private void destroy(){
        try {
            log.debug("关闭 socket 服务端");
            socketIoServer.stop();
        }catch (Exception e){
            e.printStackTrace();
        }
    }

    @PostConstruct
    public void init() {
        log.debug("SocketEventListener initialized");

        //添加监听,客户端自动连接到 socket 服务端
        socketIoServer.addConnectListener(client -> {
            String userId = client.getHandshakeData().getSingleUrlParam("userId");
            SocketUtil.connectMap.put(userId, client);
            log.debug("客户端userId: "+ userId+ "已连接,客户端ID为:" + client.getSessionId());
        });

        //添加监听,客户端跟 socket 服务端自动断开
        socketIoServer.addDisconnectListener(client -> {
            String userId = client.getHandshakeData().getSingleUrlParam("userId");
            SocketUtil.connectMap.remove(userId, client);
            log.debug("客户端userId:" + userId + "断开连接,客户端ID为:" + client.getSessionId());
        });
    }

//    // 注释说明:以下 onConnect和 onDisconnect 方法在某些场景下会失效,不建议使用,所以注释掉
//    /**
//     *  客户端自动连接到 socket 服务端
//     *
//     * @Param [client]
//     * @return
//     **/
//    @OnConnect
//    public void onConnect(SocketIOClient client) {
//        String userId = client.getHandshakeData().getSingleUrlParam("userId");
//        SocketUtil.connectMap.put(userId, client);
//        log.debug("客户端userId: "+ userId+ "已连接,客户端ID为:" + client.getSessionId());
//    }
//
//    /**
//     *  客户端跟 socket 服务端自动断开
//     *
//     * @Param [client]
//     * @return
//     **/
//    @OnDisconnect
//    public void onDisconnect(SocketIOClient client) {
//        String userId = client.getHandshakeData().getSingleUrlParam("userId");
//        log.debug("客户端userId:" + userId + "断开连接,客户端ID为:" + client.getSessionId());
//        SocketUtil.connectMap.remove(userId, client);
//    }
}

2.4 Socket 配置文件和配置类

(用来定义 socket 的一些配置)

2.4.1 yml 配置

socketio:
    host: 127.0.0.1
    port: 33000
    maxFramePayloadLength: 1048576
    maxHttpContentLength: 1048576
    bossCount: 1
    workCount: 100
    allowCustomRequests: true
    upgradeTimeout: 1000000
    pingTimeout: 6000000
    pingInterval: 25000

2.4.2 配置类:SocketConfig

package com.mss.framework.config;

import com.corundumstudio.socketio.SocketIOServer;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;

@Component
public class SocketConfig {

    @Value("${socketio.host}")
    private String host;

    @Value("${socketio.port}")
    private Integer port;

    @Value("${socketio.bossCount}")
    private int bossCount;

    @Value("${socketio.workCount}")
    private int workCount;

    @Value("${socketio.allowCustomRequests}")
    private boolean allowCustomRequests;

    @Value("${socketio.upgradeTimeout}")
    private int upgradeTimeout;

    @Value("${socketio.pingTimeout}")
    private int pingTimeout;

    @Value("${socketio.pingInterval}")
    private int pingInterval;

    @Bean
    public SocketIOServer socketIOServer() {
        com.corundumstudio.socketio.Configuration configuration = new com.corundumstudio.socketio.Configuration();
        configuration.setPort(port);
        com.corundumstudio.socketio.SocketConfig socketConfig=new com.corundumstudio.socketio.SocketConfig();
        socketConfig.setReuseAddress(true);
        configuration.setSocketConfig(socketConfig);
        configuration.setOrigin(null);
        configuration.setBossThreads(bossCount);
        configuration.setWorkerThreads(workCount);
        configuration.setAllowCustomRequests(allowCustomRequests);
        configuration.setUpgradeTimeout(upgradeTimeout);
        configuration.setPingTimeout(pingTimeout);
        configuration.setPingInterval(pingInterval);
        configuration.setRandomSession(true);

//         configuration.setKeyStorePassword("pi0yo93pqgrs");
//         configuration.setKeyStore(this.getClass().getResourceAsStream("www.ibms.club.jks"));
//         configuration.setAuthorizationListener(data -> {
//             String token = data.getSingleUrlParam("token");
//             return StrUtil.isNotBlank(token);
//         });

        //初始化 Socket 服务端配置
        return new SocketIOServer(configuration);
    }
}

2.5 Socket 服务启动类:ServerRunner

  • 实现 CommandLineRunner 接口,项目启动时自动执行 socketIOServer.start() 方法
package com.mss.framework.run;

import com.corundumstudio.socketio.SocketIOServer;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;

@Slf4j
@Component
@AllArgsConstructor
public class ServerRunner implements CommandLineRunner {

    private final SocketIOServer socketIOServer;

    /**
     *  项目启动时,自动启动 socket 服务,服务端开始工作
     *
     * @Param [args]
     * @return
     **/
    @Override
    public void run(String... args)  {
        socketIOServer.start();
        log.info("socket.io server started !");
    }
}

2.6 Socket 工具类:SocketUtil

  • 下列实例代码中,是使用 userId 来当做客户端唯一标识,这个每个人可以根据自己项目里自行设置;
  • 下列实例代码的应用场景,只有服务端向客户端发送消息的需求,所以实际这个工具类只有 sendToOne() 方法是实际起作用的,其余的代码都是为了本文额外写的方法;
package com.mss.common.utils.socket;

import com.corundumstudio.socketio.SocketIOClient;
import com.corundumstudio.socketio.annotation.OnEvent;
import com.mss.common.constant.SocketEventContants;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

/**
 * @Author: zhanleai
 * @Description:
 */
@Component
@Slf4j
public class SocketUtil {

    //暂且把用户&客户端信息存在缓存
    public static ConcurrentMap<String, SocketIOClient> connectMap = new ConcurrentHashMap<>();

    /**
     *  单发消息(以 userId 为标识符,给用户发送消息)
     *
     * @Param [userId, message]
     * @return
     **/
    public static void sendToOne(String userId, Object message) {
        //拿出某个客户端信息
        SocketIOClient socketClient = getSocketClient(userId);
        if (Objects.nonNull(socketClient) ){
            //单独给他发消息
            socketClient.sendEvent(SocketEventContants.CHANNEL_USER,message);
        }else{
            log.info(userId + "已下线,暂不发送消息。");
        }
    }

    /**
     *  群发消息
     *
     * @Param
     * @return
     **/
    public static void sendToAll(Object message) {
        if (connectMap.isEmpty()){
            return;
        }
        //给在这个频道的每个客户端发消息
        for (Map.Entry<String, SocketIOClient> entry : connectMap.entrySet()) {
            entry.getValue().sendEvent(SocketEventContants.CHANNEL_SYSTEM, message);
        }
    }

    /**
     * 根据 userId 识别出 socket 客户端
     * @param userId
     * @return
     */
    public static SocketIOClient getSocketClient(String userId){
        SocketIOClient client = null;
        if (StringUtils.hasLength(userId) &&  !connectMap.isEmpty()){
            for (String key : connectMap.keySet()) {
                if (userId.equals(key)){
                    client = connectMap.get(key);
                }
            }
        }
        return client;
    }

    /**
     *  1)使用事件注解,服务端监听获取客户端消息;
     *  2)拿到客户端发过来的消息之后,可以再根据业务逻辑发送给想要得到这个消息的人;
     *  3)channel_system 之所以会向全体客户端发消息,是因为我跟前端约定好了,你们也可以自定定义;
     *
     * @Param message
     * @return
     **/
    @OnEvent(value = SocketEventContants.CHANNEL_SYSTEM)
    public void channelSystemListener(String message) {
        if (!StringUtils.hasLength(message)){
            return;
        }

        this.sendToAll(message);
    }
}

3 Socket 调用

3.1 实际项目的应用场景:在需要发送消息通知的业务代码中调用

  • 这个方法里有几个类:Message、DateUtils、IMessageService、MessageMapper,均为根据自身业务场景自定义的类,你们自己建吧。有需要再私信我要;
  • 后端代码写到这里,实际上已经写完了。从 3.2 开始均为测试代码;
package com.mss.message.service.impl;

import com.mss.common.utils.DateUtils;
import com.mss.common.utils.socket.SocketUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.mss.message.mapper.MessageMapper;
import com.mss.message.domain.entity.Message;
import com.mss.message.service.IMessageService;

/**
 * 消息Service业务层处理
 *
 * @author zhanleai
 */
@Service
@Slf4j
public class MessageServiceImpl implements IMessageService {

    @Autowired
    private MessageMapper messageMapper;

    /**
     * 新增消息
     *
     * @param message 消息
     * @return 结果
     */
    @Override
    public int insertMessage(Message message) {
        message.setSendTime(DateUtils.getNowDate());
        // 消息入库,消息持久化
        int i = messageMapper.insertMessage(message);
        // 新增消息之后,再向前端推送 Socket 消息
        SocketUtil.sendToOne(message.getSendUserId().toString(),message);
        return i;
    }

}

3.2 测试Controller

  • 下文均为测试的代码
package com.mss.message.controller;

import com.mss.common.utils.socket.SocketUtil;
import io.swagger.annotations.Api;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.mss.common.core.controller.BaseController;
import com.mss.common.core.domain.AjaxResult;

/**
 * 消息Controller
 *
 * @author zhanleai
 */
@RestController
@Api(tags="消息")
@RequestMapping("/message")
public class MessageController extends BaseController {

    /**
     *  给指定客户端发送消息
     * 
     * @Param [userId, message]
     * @return 
     **/
    @GetMapping("/sendToOne")
    public AjaxResult sendToOne(String userId , String message){
        SocketUtil.sendToOne(userId,message);
        return AjaxResult.success("单独发送消息成功。");
    }
}

4 前端调用代码

  • 前端代码监听了 channel_user 和 channel_system 两个频道,一个做了三个动作:
    1)连接上服务端;
    2)监听并接收 channel_user 频道的消息;
    3)给服务端发送一条消息,并广播到所有客户端;
  • postman 只做了一个动作,给后端指定的 userId 发送一条 channel_user 频道的消息,并被指定客户端捕获;

4.1 html 测试代码以及说明

  • 此处不做赘述,详细的 html 测试代码见我另一篇文章《SocketIO 的 html 代码示例》:链接跳转

4.2 浏览器打开 html 文件,然后查看后端服务日志

  • (socket 服务端启动,端口号为 33000,客户端 zhanleai 连接上来了)

  • 浏览器截图
    在这里插入图片描述

  • 后端服务日志截图
    在这里插入图片描述

4.3 postman 工具测试

  • postman 截图
    在这里插入图片描述
  • 浏览器收到消息截图
    在这里插入图片描述

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

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

相关文章

video标签,去除上下默认边距

不知道为什么&#xff0c;video标签上下会有空白 清除方法 style"width 100%; height100%; object-fit: fill"

校园二手物品交易网站/校园闲置物品交易系统

摘 要 本文论述了校园二手物品交易网站的设计和实现&#xff0c;该网站从实际运用的角度出发&#xff0c;运用了计算机网站设计、数据库等相关知识&#xff0c;网络和JSP技术、SSM框架Mysql数据库设计来实现的&#xff0c;网站主要包括学生注册、学生登录、浏览商品、搜索商品…

Vue+Element Plus后台管理主界面搭建实现

​ 续接Django REST Framework&#xff0c;使用Vite构建Vue3的前端项目 1. 后台管理系统主界面框架搭建 后台系统主界面搭建 新建后台管理文件目录 完成后台整体布局 // 1.主界面 index.vue<script setup lang"ts"></script><template><el-…

应用层HTTP协议

文章目录 应用层HTTP协议1、HTTP协议概念2、URL&#xff08;统一资源定位符&#xff09;2.1、URL的encode&#xff08;编码&#xff09;和decode&#xff08;解码&#xff09; 3、HTTP请求和响应报头格式3.1、请求报头3.2、响应报头 4、HTTP的方法4.1、GET方法4.2、POST方法4.3…

生成模型VAE

VAE likelihood-basedELBOVAESGVB估计器和AEVB算法重参数化 likelihood-based likelihood-based generative models是生成模型的一类范式&#xff0c;通过最大化所有观测数据的似然函数来学习模型参数。 该怎么去理解likelihood-based&#xff0c;基于似然的生成模型&#xf…

互联网重构“规则制定权”,周期性谋咒开始轮转!

周期“魔咒”又开始轮转了。 产业趋势叠加资本周期&#xff0c;使得任何产业都有其周期性规律&#xff0c;传统资源产业是如此&#xff0c;科技产业亦非例外。 刚刚迎来30周年庆的中国互联网赛道就正处于新一轮小周期的节点。随着移动用户量逐渐被开发利用至阶段性顶峰&#…

学习c语言第24天(练习)

编程题 第一题 最大公约数最小公倍数求和 //求最大公约数和最小公倍数之和 //暴力求解 //int main() //{ // int n 0; // int m 0; // while (scanf("%d %d", &n, &m)2) // { // int min n < m ? n : m; // int max n > m ? n : m; //…

原神4.8版本角色数据

<!DOCTYPE html> <html lang"zh-CN"><head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.0"><title>原神4.8版本角色数据</title><style>…

【浏览器插件】Chrome扩展V3版本

前言&#xff1a;Chrome从2022年6月开始&#xff0c;新发布插件只接受V3版。2024年V2版已从应用商店下架。 浏览器扩展插件开发API文档 chrome官网&#xff08;要翻墙&#xff09;&#xff1a; https://developer.chrome.com/docs/extensions/mv3 MDN中文&#xff1a;https:/…

gitee拉取项目,提交代码

1&#xff0c;安装git 2&#xff0c;gitee配置成员仓库权限 3&#xff0c;克隆项目 git clone gitgitee.com:sky474775788/Python_API_AUTO.git 4&#xff0c;配置用户信息 git config --global user.name 林俊xxx git config --global user.email ohnxxxsee1xxx.com 5&…

机器学习·L2W4-决策树

决策树 从根节点的所有示例开始计算所有可能特征的分割信息增益&#xff0c;并选择信息增益最高的特征根据所选特征分割数据集&#xff0c;并创建树的左分支和右分支不断重复分割过程&#xff0c;直到满足停止条件 信息增益 也可以理解为信息熵的减少 p p p是结果为positive…

住宅代理和数据中心代理:指纹浏览器用哪个更安全?

在当今的数字时代&#xff0c;确保您的在线安全至关重要。这就是住宅和数据中心代理发挥作用的地方&#xff0c;它们可以保护您的身份和个人信息。指纹浏览器解决了账号所在环境指纹参数隔离的安全问题&#xff0c;而IP解决环境的定位与隔离问题&#xff0c;就像Maskfog中提供的…

洛伦兹微分方程与混沌理论

前言 这一段时间在看书中关于深度学习与神经网络的内容&#xff0c;其中有一节介绍神经网络用于预测洛伦兹微分方程的数值解&#xff0c;还提到了“吸引子”这一概念&#xff0c;当时也没太理解是什么&#xff0c;下午搜集了一本书上关于混沌理论的介绍——《混沌的本质》。 这…

【线性回归】——解决运筹优化类问题

目录 文章目录 前言 一、模型原理 1.线性规划模型的三要素 &#x1f60f;&#x1f60f;&#x1f60f; 2.模型特点 3.线性规划的表现形式 二、模型建立步骤 1.找决策变量 2.确定目标函数 3.找到约束条件 4.运用Matlab中的Linprog函数 总结 前言 在实际生活应用中&#xff0c;我…

Unity入门5——材质

创建材质 点击Assets → Create → Material&#xff0c;得到一个默认材质球的副本。 使用材质 直接把材质球拖拽到物体上&#xff0c;或设置mesh renderer组件下的Materials 数组中第一个元素

etcd高可用集群部署

文章目录 一、环境准备二、安装部署2.1 下载安装包2.2 将etcd和etcdctl复制到/usr/local/bin中2.3 创建目录并赋予权限2.4 修改节点配置2.4.1 配置etcd.conf文件2.4.2 配置/etc/systemd/system/etcd.service文件 2.5 启动ectd服务2.6 查看集群成员信息2.7 查看集群状态 在生产环…

【PyQt5】PyQt5 信号和槽

基于GUI的应用程序是事件驱动的。函数或方法按照用户的操作&#xff08;例如点击按钮、从集合中选择项目或点击鼠标等&#xff09;来执行&#xff0c;这些操作被称为 事件 。用于构建GUI界面的小部件充当这些事件的来源。每个PyQt小部件都是从QObject类派生而来&#xff0c;设计…

《Redis设计与实现》读书笔记-复制

目录 1.概述 2.复制命令 3.部分重同步过程 4.部分重同步实现 4.1复制偏移量 4.2复制积压缓冲区 4.3服务器运行ID 5.总结 1.概述 在redis 通过向从服务器发送命令&#xff1a;SLAVE OF&#xff0c;让从服务器复制主服务器&#xff0c;成为复制。 复制的目的 让从服务器…

等保测评 linux设置三权分立

1、首先浅谈一下目录结构 drwxr-xr-x意思如下&#xff1a;第一位表示文件类型。 d是目录文件&#xff0c;l是链接文件&#xff0c;-是普通文件&#xff0c;p是管道。后面的分三个三个来看&#xff0c;即 rwx 、r-x 、r-x。 第一个&#xff1a; root &#xff1a;r 是可读&#…

在Linux中认识pthread库

int *pnullptr; pnullptr; *pnullptr; 指针变量做右值也是变量拥有空间。去承装数据。 *p代表指针所指向的空间&#xff0c;及0号地址&#xff0c;及往虚拟地址的0号地址处写8个字节的数据&#xff0c;全部写为0. &#xff08;此操作不允许&#xff09; 进程和线程的关系如…