SpringBoot集成 Websocket 实现服务与客户端进行消息发送和接收

news2024/11/24 7:52:06

介绍

WebSocket是一种在单个TCP连接上进行全双工通信的协议。WebSocket使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。

效果

客户端效果客户端效果图

服务端日志

服务端日志

pom依赖


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

 <!-- lombok -->
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>${lombok.version}</version>
</dependency>

<!-- fastjson2 -->
<dependency>
    <groupId>com.alibaba.fastjson2</groupId>
    <artifactId>fastjson2</artifactId>
    <version>${fastjson2.version}</version>
</dependency>
        

配置类


import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;

/**
 * websocket 配置类
 *
 * @author yunnuo <a href="2552846359@qq.com">Email: 2552846359@qq.com</a>
 * @date 2023-12-29
 */
@Configuration
@EnableWebSocket
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

    /**
     * 注入该bean 自动注册使用 @ServerEndpoint注解声明的Websocket endpoint
     *
     * @return {@link ServerEndpointExporter} bean
     */
    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        // 注册端点
        registry.addEndpoint("/ws/msg")
                //解决跨域问题
                .setAllowedOrigins("*")
                .withSockJS();
    }

    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
        //定义客户端订阅地址的前缀信息
        registry.enableSimpleBroker("/topic");
    }

}

示例

定义端点


import com.alibaba.fastjson2.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Set;

/**
 * websocket 测试 endpoint
 *
 * @author yunnuo <a href="2552846359@qq.com">Email: 2552846359@qq.com</a>
 * @date 2023-12-29
 */
@Slf4j
@Component
@ServerEndpoint(value = "/topic/ws/demo/{uid}")
public class WebSocketDemoEndpoint {

    /**
     * 连接数量池
     */
    private static final Map<Long, Session> SESSION_POOL = new HashMap<>();


    /**
     * 开始连接
     *
     * @param session session
     * @param uid     用户标识
     */
    @OnOpen
    public void onOpen(Session session, @PathParam(value = "uid") Long uid) {
        if (Objects.isNull(uid)) {
            log.warn("onOpen uid is null session:{}", JSONObject.toJSONString(session));
            return;
        }

        SESSION_POOL.put(uid, session);
        log.info("onOpen uid:{}, sessionPool size:{}", uid, SESSION_POOL.size());

    }


    /**
     * 关闭连接
     *
     * @param session session
     * @param uid     用户标识
     */
    @OnClose
    public void onClose(Session session, @PathParam(value = "uid") Long uid) {
        if (Objects.isNull(uid)) {
            log.warn("onClose uid is null session:{}", JSONObject.toJSONString(session));
            return;
        }

        try {
            Session removeSession = SESSION_POOL.remove(uid);
            if (session != null) {
            	session.close();
            }
            if (removeSession!= null && removeSession.isOpen()) {
                removeSession.close();
            }
            log.info("onClose uid:{}, sessionPool size:{}", uid, SESSION_POOL.size());
        } catch (IOException e) {
            log.error("onClose error! uid:{}, e:{}", uid, e.getMessage(), e);
        }

    }

    /**
     * 收到客户端消息
     *
     * @param session session
     * @param uid     用户标识
     * @param message 消息内容
     */
    @OnMessage
    public void onMessage(Session session, @PathParam(value = "uid") Long uid, String message) {
        log.info("onMessage uid:{}, msg:{}", uid, message);
    }


    /**
     * 发送错误
     *
     * @param session session
     * @param uid     uid
     * @param e       异常e
     */
    @OnError
    public void onError(Session session, @PathParam(value = "uid") Long uid, Throwable e) {
        log.error("onError uid:{}, e:{}", uid, e.getMessage(), e);
    }


    /**
     * 广播消息 (SESSION_POOL 池中)
     *
     * @param message message
     */
    public void sendAllMessage(String message) {
        log.info("sendAllMessage:{}", message);

        if (SESSION_POOL.isEmpty()) {
            return;
        }

        SESSION_POOL.forEach((uid, session) -> {
            if (session.isOpen()) {
                session.getAsyncRemote().sendText(message);
                log.info("sendAllMessage for uid:{}, message:{}", uid, message);
            }
        });

    }

    /**
     * 多个消息发送
     *
     * @param uids    用户标识
     * @param message 消息
     */
    public void sendMessageToUsers(Set<Long> uids, String message) {
        log.info("sendMessageToUsers uids:{}, msg:{}", JSONObject.toJSONString(uids), message);

        uids.forEach(uid -> sendMessageToUser(uid, message));
    }


    /**
     * 单个消息发送
     *
     * @param uid     用户标识
     * @param message 消息
     */
    public void sendMessageToUser(Long uid, String message) {
        Session session = SESSION_POOL.get(uid);

        if (session != null && session.isOpen()) {
            log.info("sendMessageToUser uid:{}, msg:{}", uid, message);
            session.getAsyncRemote().sendText(message);
        }

    }

}

客户端(连接、关闭、发送/接收消息)


<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>websocket测试</title>
    <link rel="stylesheet" href="https://unpkg.com/element-ui/lib/theme-chalk/index.css">
    <script src="https://cdn.jsdelivr.net/npm/vue"></script>
</head>
<body>
<div id="app">
    <el-container>
        <el-header>
            <el-button @click="connect" type="success">连接</el-button>
            <el-button @click="close" type="danger">断开连接</el-button>
        </el-header>
        <el-main>
            <el-row>
                <el-col :span="6">
                    <el-input
                            type="textarea"
                            :rows="2"
                            placeholder="请输入内容"
                            v-model="textarea">
                    </el-input>
                </el-col>
            </el-row>
            <el-row>
                <el-col :span="6">
                    <el-button @click="sendMsg" type="primary" plain>提交消息</el-button>
                </el-col>
            </el-row>

        </el-main>
    </el-container>
</div>
</body>
<!-- import Vue before Element -->
<script src="https://unpkg.com/vue@2/dist/vue.js"></script>
<!-- import JavaScript -->
<script src="https://unpkg.com/element-ui/lib/index.js"></script>


<script>

    const ws_url = "ws://localhost:8088/topic/ws/demo/1";

    new Vue({
        el: '#app',
        data: function () {
            return {
                socket: {},
                textarea: '',
            }
        },

        methods: {

            connect() {
                this.socket = new WebSocket(ws_url);

                this.socket.onopen = function () {
                    ELEMENT.Message({
                        message: '连接服务器成功...',
                        type: 'success'
                    });
                };

                this.socket.onmessage = function (event) {
                    ELEMENT.Message({
                        message: '接收服务器消息: ' + event.data,
                        type: 'success'
                    });
                };

                this.socket.onclose = function () {
                    this.socket = null;
                    ELEMENT.Message({
                        message: '连接服务器断开',
                        type: 'warning'
                    });
                };

                this.socket.onerror = function () {
                    ELEMENT.Message.error('连接服务器失败!');
                    this.socket = null;
                };

            },

            close() {
                this.socket.close();
            },

            sendMsg() {
                if (this.socket != null){
                    this.socket.send(this.textarea);
                }
            }


        }
    })
</script>
</html>

服务端发送消息

定义请求DTO

import com.alibaba.fastjson2.JSONObject;
import lombok.Data;

/**
 * 测试
 *
 * @author yunnuo <a href="2552846359@qq.com">Email: 2552846359@qq.com</a>
 * @date 2024-01-02
 */
@Data
public class SendMsgReq {

    private Long uid;

    private String msg;

    @Override
    public String toString() {
        return JSONObject.toJSONString(this);
    }
}

定义API接口
import com.ukayunnuo.domain.request.SendMsgReq;
import com.ukayunnuo.endpoints.WebSocketDemoEndpoint;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;

/**
 * 发送消息测试
 *
 * @author yunnuo <a href="2552846359@qq.com">Email: 2552846359@qq.com</a>
 * @date 2024-01-02
 */
@Slf4j
@RestController
@RequestMapping("/websocket/demo/sendMsg")
public class SendMsgController {

    @Resource
    private WebSocketDemoEndpoint webSocketDemoEndpoint;

    /**
     * 发送消息测试
     *
     * @param req {@link SendMsgReq} 请求dto
     * @return 发送结果
     */
    @PostMapping
    public void sendMsg(@RequestBody SendMsgReq req) {
        try {
            webSocketDemoEndpoint.sendMessageToUser(req.getUid(), req.getMsg());
        } catch (Exception e) {
            log.error("send msg error! req:{}, e:{}", req, e.getMessage(), e);
        }
    }
}

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

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

相关文章

【Matlab】LSTM长短期记忆神经网络时序预测算法(附代码)

资源下载&#xff1a; https://download.csdn.net/download/vvoennvv/88688439 一&#xff0c;概述 LSTM&#xff08;Long Short-Term Memory&#xff09;是一种常用的循环神经网络&#xff08;Recurrent Neural Network&#xff0c;RNN&#xff09;结构&#xff0c;由于其对于…

1207. 大臣的旅费(dfs求树的直径/图论)

题目&#xff1a; 1207. 大臣的旅费 - AcWing题库 思路&#xff1a; dfs求树的直径。 代码&#xff1a; #include<iostream> #include<cstdio> #include<vector> using namespace std; const int N100100; struct Edge//边的id以及长度 {int id,w; };ve…

Mysql 下载与安装教程(详细介绍与总结)

一&#xff1a;版本介绍 首先&#xff0c;我们需要先进入官网进行下载&#xff0c;在官网中有好几个版本&#xff0c;那么这里我分别简述一下MySQL各个版本区别&#xff1a; 1&#xff1a;企业版&#xff0c;MySQL Enterprise Edition 需要付费的&#xff0c;可以免费试用30天…

redis的搭建及应用(五)-布隆过滤器插件

redis布隆过滤器 可以把布隆过滤器理解为bitmap结构&#xff0c;判断某个对象是否存在时&#xff0c;它可能会误判。但是布隆过滤器也不是特别不精确&#xff0c;只要参数设置得合理&#xff0c;它的精确度也可以控制得相对足够精确&#xff0c;只会有小小的误判概率。 总得来说…

从 0 到 1 实现 ReentrantLock

虽然本文的标题是从 0 到 1 实现 ReentrantLock &#xff0c;但是为了方便理解&#xff0c;我们先从一个问题出发&#xff1a;既然系统已经有 synchronized 关键字了&#xff0c;那么为什么还会出现 ReentrantLock 这种代码层面的锁? 这就要先回顾一下历史了&#xff1a;在 J…

微服务整合:构建高效灵活的分布式系统

随着软件开发的不断演进和业务的复杂性增加&#xff0c;微服务架构已经成为一种流行的解决方案。然而&#xff0c;当涉及到多个微服务之间的整合时&#xff0c;我们需要谨慎考虑如何实现高效、灵活的分布式系统。 微服务架构的流行使得软件开发变得更加灵活和可扩展。然而&…

layuiadmin新建tabs标签页,点击保存,打开新的标签页并刷新

用的layuiamin前端框架 需求&#xff1a;新增的页面为一个标签页&#xff0c;保存后&#xff0c;需要刷新列表 1、新建customMethod.js文件&#xff0c;自定义自己的方法 layui.define(function (exports) {var $ layui.$var customMethod {// 表单点击保存后&#xff0c;…

【ROS2】MOMO的鱼香ROS2(四)ROS2入门篇——ROS2节点通信之话题与服务

ROS2节点通信之话题与服务点 引言1 理解从通信开始1.1 TCP&#xff08;传输控制协议&#xff09;1.2 UDP&#xff08;用户数据报协议&#xff09;1.3 基于共享内存的IPC方式 2 ROS2话题2.1 ROS2话题指令2.2 话题之RCLPY实现2.2.1 编写发布者2.2 2 编写订阅者2.2.3 运行测试 3 R…

【Unity美术】Unity工程师对3D模型需要达到的了解【二】

&#x1f468;‍&#x1f4bb;个人主页&#xff1a;元宇宙-秩沅 &#x1f468;‍&#x1f4bb; hallo 欢迎 点赞&#x1f44d; 收藏⭐ 留言&#x1f4dd; 加关注✅! &#x1f468;‍&#x1f4bb; 本文由 秩沅 原创 &#x1f468;‍&#x1f4bb; 收录于专栏&#xff1a;Uni…

基于ThinkPHP的云盘系统Cloudreve本地搭建并实现远程访问

文章目录 1、前言2、本地网站搭建2.1 环境使用2.2 支持组件选择2.3 网页安装2.4 测试和使用2.5 问题解决 3、本地网页发布3.1 cpolar云端设置3.2 cpolar本地设置 4、公网访问测试5、结语 1、前言 自云存储概念兴起已经有段时间了&#xff0c;各互联网大厂也纷纷加入战局&#…

扫码看图时,多图如何用轮播排列展示?

在扫描二维码看图时&#xff0c;一般图片大多会通过上下排列的方式来展示&#xff0c;如果图片的数量太多&#xff0c;就需要在手机上不断地下滑才能看到所有内容&#xff0c;这种方式会导致在查看图片时感觉疲劳或者眼花的情况。那么想要解决这个问题&#xff0c;我们可以在生…

flutter接入扫码枪的扫描结果,其实就是监听键盘输入,从测试到页面显示出来

检测设备是否正常 首先一定要测试一下你的硬件设备是否正常&#xff0c;虽然有的设备看着插入usb后指示灯什么都亮了&#xff0c;但是不一定就说明设备没问题&#xff0c;这就需要先验证一下&#xff0c;比如打开记事本或者doc文档&#xff0c;然后扫描一下条形码&#xff0c;…

electron 主进程对预加载脚本和渲染进程通信

知识整理 主进程main.js node环境可以使用node的方法预加载脚本可以使用部分node方法,可以理解为是主进程和渲染进程之间的一个桥梁渲染进程属于浏览器环境,不可以使用node方法,可以操作dom等js方法 主进程对渲染进程通信 上一篇文章实现了自定义菜单栏功能,上上篇实现了预加…

程序员30而立的北京之路

作为一名程序员&#xff0c;职业规划和心灵成长是我工作和生活中不可或缺的部分。30岁是一个人生中的重要节点&#xff0c;也是所谓的“而立之年”&#xff0c;在这个阶段&#xff0c;我开始更加关注自己的职业发展和内心成长。在这篇文章中&#xff0c;我将分享我在北京这座城…

YOLOv8改进 | 细节创新篇 | iAFF迭代注意力特征融合助力多目标细节涨点

一、本文介绍 本文给大家带来的改进机制是iAFF&#xff08;迭代注意力特征融合&#xff09;&#xff0c;其主要思想是通过改善特征融合过程来提高检测精度。传统的特征融合方法如加法或串联简单&#xff0c;未考虑到特定对象的融合适用性。iAFF通过引入多尺度通道注意力模块(我…

花几分钟整点jmeter花活,轻松超越90%软件测试

jmeter 可以做性能测试&#xff0c;这个很多人都知道&#xff0c;那你知道&#xff0c;jmeter 可以在启动运行时&#xff0c;指定线程数和运行时间&#xff0c;自定义性能场景吗&#xff1f; 前言 jmeter 性能测试&#xff0c;动态设定性能场景 平时&#xff0c;我们使用 jmet…

使用.Net nanoFramework 驱动ESP32的OLED显示屏

本文介绍如何使用.Net nanoFramework 驱动ESP32的OLED显示屏。我们将会从最基础的部分开始&#xff0c;逐步深入&#xff0c;让你能够理解并实现整个过程。无论你是初学者还是有一定经验的开发者&#xff0c;这篇文章都会对你有所帮助。 1. 硬件准备 1.1 ESP32开发板 这里我们…

安装中望CAD2023 SP2

1.下载中望CAD2023 SP2&#xff0c;并安装&#xff1b; 2.把“flxNetCommon.dll”拷贝到安装目录&#xff08;与“ZWCAD.exe”同一个目录&#xff09;&#xff1b; 3.运行“ZwLicenseManager.exe” 4.点击“激活许可证”&#xff1b; 5.点击“浮动许可” ->“仅配置不查询…

Hotspot源码解析-第十一章

第十一章 11.1 线程 11.1.1 线程的概念 说起线程&#xff0c;首先得提起进程&#xff0c;相信很面试者在回答进程与线程的区别时都会用一句话&#xff1a;“进程是操作系统资源分配的基本单位&#xff0c;而线程是任务调度和执行的基本单位”&#xff0c;只能说这句话部分正…