Spring Boot 整合 socket 实现简单聊天

news2024/12/23 14:52:18

来看一下实现的界面效果
在这里插入图片描述
pom.xml的maven依赖

 <!-- 引入 socket -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-websocket</artifactId>
        </dependency>
        <!-- 引入 Fastjson ,实现序列化使用  -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.62</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>

配置类

@Configuration
public class WebSocketConfiguration {
    /**
     * 给 spring 容器注入这个 ServerEndpointExporter对象
     * <p>
     * 这个bean会检测所有带有 @ServerEndpoint 注解的 bean 并注册他们。
     * ps:
     * 如果使用的是外置的 Tomcat 容器,则不需要自己提供 ServerEndpointExporter,因为它将由 Tomcat 容器自己提供和管理。
     *
     * @return ServerEndpointExporter
     */
    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }
}

消息接口

public interface Message {
}

WebSocket 会话上下文工具

@Slf4j
public class WebSocketContext {

    /**
     * Session 与用户的映射
     */
    private static final Map<Session, String> SESSION_USER_MAP = new ConcurrentHashMap<>();
    /**
     * 用户与 Session 的映射
     */
    private static final Map<String, Session> USER_SESSION_MAP = new ConcurrentHashMap<>();

    /**
     * 添加 Session 在这个方法中,会绑定用户和 Session 之间的映射
     *
     * @param session Session
     * @param user    用户
     */
    public static void add(Session session, String user) {
        // 更新 USER_SESSION_MAP , 这里的 user 正常来讲应该是具体的用户(id),而不是单纯的 session.getId()
        USER_SESSION_MAP.put(user, session);
        // 更新 SESSION_USER_MAP
        SESSION_USER_MAP.put(session, user);
    }

    /**
     * 移除 Session
     *
     * @param session Session
     */
    public static void remove(Session session) {
        // 从 SESSION_USER_MAP 中移除
        String user = SESSION_USER_MAP.remove(session);
        // 从 USER_SESSION_MAP 中移除
        if (user != null && user.length() > 0) {
            USER_SESSION_MAP.remove(user);
        }
    }


    /**
     * 广播发送消息给所有在线用户
     *
     * @param type    消息类型
     * @param message 消息体
     * @param <T>     消息类型
     * @param me      当前消息的发送者,不会将消息发送给自己
     */
    public static <T extends Message> void broadcast(String type, T message, Session me) {
        // 创建消息
        String messageText = buildTextMessage(type, message);
        // 遍历 SESSION_USER_MAP ,进行逐个发送
        for (Session session : SESSION_USER_MAP.keySet()) {
            if (!session.equals(me)) {
                sendTextMessage(session, messageText);
            }
        }
    }

    /**
     * 发送消息给单个用户的 Session
     *
     * @param session Session
     * @param type    消息类型
     * @param message 消息体
     * @param <T>     消息类型
     */
    public static <T extends Message> void send(Session session, String type, T message) {
        // 创建消息
        String messageText = buildTextMessage(type, message);
        // 遍历给单个 Session ,进行逐个发送
        sendTextMessage(session, messageText);
    }

    /**
     * 发送消息给指定用户
     *
     * @param user    指定用户
     * @param type    消息类型
     * @param message 消息体
     * @param <T>     消息类型
     * @return 发送是否成功
     */
    public static <T extends Message> boolean send(String user, String type, T message) {
        // 获得用户对应的 Session
        Session session = USER_SESSION_MAP.get(user);
        if (session == null) {
            log.error("==> user({}) 不存在对应的 session", user);
            return false;
        }
        // 发送消息
        send(session, type, message);
        return true;
    }

    /**
     * 构建完整的消息
     *
     * @param type    消息类型
     * @param message 消息体
     * @param <T>     消息类型
     * @return 消息
     */
    private static <T extends Message> String buildTextMessage(String type, T message) {
        JSONObject messageObject = new JSONObject();
        messageObject.put("type", type);
        messageObject.put("body", message);
        return messageObject.toString();
    }

    /**
     * 真正发送消息
     *
     * @param session     Session
     * @param messageText 消息
     */
    private static void sendTextMessage(Session session, String messageText) {
        if (session == null) {
            log.error("===> session 为 null");
            return;
        }
        RemoteEndpoint.Basic basic = session.getBasicRemote();
        if (basic == null) {
            log.error("===> session.basic 为 null");
            return;
        }
        try {
            basic.sendText(messageText);
        } catch (IOException e) {
            log.error("===> session: {} 发送消息: {} 发生异常", session, messageText, e);
        }
    }

    /**
     * 在线人数通知
     */
    public static void countNotice() {
        Integer count = SESSION_USER_MAP.size();
        ChatCountMessage message = new ChatCountMessage();
        message.setCount(count);
        broadcast(MsgTypeEnum.CHAT_COUNT.getCode(), message, null);
    }
}

消息类型枚举

@Getter
@AllArgsConstructor
public enum MsgTypeEnum {
    /**
     * 同于标识 当前消息是 聊天消息
     */
    CHAT_MSG("1", "聊天消息"),
    /**
     * 用于标识 当前消息是 人数消息
     */
    CHAT_COUNT("2", "聊天室人数");

    private final String code;
    private final String desc;
}

配置接入点
ServerEndpoint 注解是一个类层次的注解,它的功能主要是将目前的类定义成一个websocket服务器端,
注解的值将被用于监听用户连接的终端访问URL地址,客户端可以通过这个URL来连接到WebSocket服务器端

@Component
@ServerEndpoint("/chat")
@Slf4j
public class WebSocketServer {

    /**
     * 连接建立成功调用的方法
     */
    @OnOpen
    public void onOpen(Session session) {
        log.info("===> onOpen:{}", session.getId());
        // 上线,并且通知到其他人
        WebSocketContext.add(session, session.getId());
        WebSocketContext.countNotice();
    }

    /**
     * 连接关闭调用的方法
     */
    @OnClose
    public void onClose(Session session) {
        log.info("===> onClose:{}", session.getId());
        // 下线,并且通知到其他人
        WebSocketContext.remove(session);
        WebSocketContext.countNotice();
    }

    /**
     * 收到客户端消息后调用的方法
     *
     * @param message 客户端发送过来的消息
     */
    @OnMessage
    public void onMessage(String message, Session session) {
        log.info("===> onMessage:{},message:{}", session.getId(), message);
        // 进行消息的转发,同步到其他的客户端上
        ChatMsgMessage msg = JSON.parseObject(message, ChatMsgMessage.class);
        WebSocketContext.broadcast(MsgTypeEnum.CHAT_MSG.getCode(), msg, session);
    }

    /**
     * 监听错误
     *
     * @param session session
     * @param error   错误
     */
    @OnError
    public void onError(Session session, Throwable error) {
        log.error("SessionId:{},出现异常:{}", session.getId(), error.getMessage());
        error.printStackTrace();
    }

}

在线人数消息实体

@Data
@Accessors(chain = true)
public class ChatCountMessage implements Message {

    public static final String TYPE = MsgTypeEnum.CHAT_COUNT.getCode();

    /**
     * 消息编号
     */
    private String msgId;
    /**
     * 内容
     */
    private Integer count;

}

消息发送实体

@Data
@Accessors(chain = true)
public class ChatMsgMessage implements Message {

    public static final String TYPE = MsgTypeEnum.CHAT_MSG.getCode();

    /**
     * 消息编号
     */
    private String msgId;
    /**
     * 内容
     */
    private String msg;

}

在resources下新建static静态文件夹
在这里插入图片描述
index.css文件

@font-face {
    font-family: "pix";
    src: url("../DottedSongtiSquareRegular.otf");
}

html, body, pre, code, kbd, samp {
    font-family: "pix", serif;
    font-weight: bold;
    font-size: 35px;
}

.all-div {
    display: flex;
    flex-direction: column;
    width: 800px;
    margin: 20px auto;
    overflow-scrolling: auto;
}

/*.message {*/
/*    overflow: auto;*/
/*    width: 800px;*/
/*    height: 400px;*/
/*    margin-top: 20px;*/
/*}*/

.send-btns {
    display: flex;
}

/*自己发送聊天的样式*/
.message-me {
    color: red;
    text-align: right
}

.message-list {
    display: flex;
    flex-direction: column;
}

.message-left {
    display: flex;
    margin-top: 2rem;
    align-self: flex-start;
}
.message-right {
    display: flex;
    margin-top: 2rem;
    align-self: flex-end;
}

index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>在线聊天</title>
    <link href="NES.css" rel="stylesheet"/>
    <link href="/css/index.css" rel="stylesheet">
</head>
<style>
</style>
<body>
<div class="all-div">
    <!--  头部区域  -->
    <div style="margin-left: 10px">
        Spring Boot 集成 WebSocket 示例;
        <span class="nes-text is-primary">在线人数:</span>
        <span class="nes-text is-error" id="count">0</span>
    </div>
    <!--  内容显示区域  -->
    <div class="nes-container is-rounded is-dark message-list" id="message">

    </div>
    <!--  操作区域  -->
    <div class="nes-field is-inline">
        <br/>
        <input id="text" type="text" class="nes-input" style="padding: .2rem 1rem !important;"/>
        <button onclick="send()" class="nes-btn is-success">发送</button>
        <button onclick="closeWebSocket()" class="nes-btn is-error">关闭WebSocket连接</button>
    </div>
</div>
</body>

<script type="text/javascript">
    let websocket = null;
    //判断当前浏览器是否支持WebSocket
    if ('WebSocket' in window) {
        //改成你的地址
        websocket = new WebSocket("ws://127.0.0.1:8080/chat");
    } else {
        alert('当前浏览器不支持 websocket')
        throw "当前浏览器不支持 websocket"
    }

    //连接发生错误的回调方法
    websocket.onerror = function () {
        setMessageInnerHTML("WebSocket连接发生错误" + "&#13;");
    };

    //连接成功建立的回调方法
    websocket.onopen = function () {
        setMessageInnerHTML("WebSocket连接成功" + "&#13;");
    }
    //接收到消息的回调方法
    websocket.onmessage = function (event) {
        let jsonData = event.data;
        let data = JSON.parse(jsonData);
        console.log("收到消息==", event);
        if (data.type === "1") {
            let msg = otherPersonShowMsg(data.body.msg)
            setMessageInnerHTML(msg);
        }
        if (data.type === "2") {
            setChatCountInnerHTML(data.body.count)
        }
    }

    //连接关闭的回调方法
    websocket.onclose = function () {
        setMessageInnerHTML("WebSocket连接关闭" + "&#13;");
    }

    //监听窗口关闭事件,当窗口关闭时,主动去关闭websocket连接,防止连接还没断开就关闭窗口,server端会抛异常。
    window.onbeforeunload = function () {
        closeWebSocket();
    }

    //将消息显示在网页上
    function setMessageInnerHTML(innerHTML) {
        document.getElementById('message').innerHTML += innerHTML;
    }

    //将消息显示在网页上
    function setChatCountInnerHTML(innerHTML) {
        document.getElementById('count').innerHTML = innerHTML;
    }

    //关闭WebSocket连接
    function closeWebSocket() {
        websocket.close();
        setChatCountInnerHTML(0)
    }

    //发送消息
    function send() {
        var message = document.getElementById('text').value;
        websocket.send('{"msg":"' + message + '"}');
        document.getElementById('text').value = '';
        message = this.meShowMsg(message);
        setMessageInnerHTML(message);
    }

    // 显示别人发送的消息
    function otherPersonShowMsg(str) {
        return ` <section class="message-left">
                  <i class="nes-bcrikko"></i>
                  <div class="nes-balloon from-left is-dark" style="padding: .2rem 1rem !important;">
                    <p>${str}</p>
                  </div>
                </section>`
    }

    // 显示自己发送的消息
    function meShowMsg(str) {
        return ` <section class="message-right">
                    <div class="nes-balloon from-right is-dark" style="padding: .2rem 1rem !important;">
                      <p>${str}</p>
                    </div>
                    <i class="nes-bcrikko"></i>
              </section>`
    }
</script>
</html>

以上的是Spring Boot 整合 socket 实现简单聊天 若需完整代码 可识别二维码后 给您发代码。
在这里插入图片描述

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

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

相关文章

安卓动态加载view

目录 前言一、addview1.1 addView 的重载方法1.2 在 LinearLayout 中的使用1.2.1 addView(View child)方法的分析&#xff1a;1.2.2 addView(View child, int index)方法的分析&#xff1a;1.2.3 小结 1.3 在 RelativeLayout 中的使用 二、addContentview2.1 测试 12.2 测试 22…

如何用画图处理截图【攻略】

如何用画图处理截图【攻略】 前言版权推荐如何用画图处理截图用画图打开图片简单使用操作&#xff1a;重设图片大小操作&#xff1a;简单覆盖 最后 前言 2024-5-9 22:29:27 以下内容源自《【攻略】》 仅供学习交流使用 版权 禁止其他平台发布时删除以下此话 本文首次发布于…

安卓开发--新建工程,新建虚拟手机,按键事件响应

安卓开发--新建工程&#xff0c;新建虚拟手机&#xff0c;按键事件响应 1.前言2.运行一个工程2.1布局一个Button2.2 button一般点击事件2.2 button属性点击事件2.2 button推荐点击事件 本篇博客介绍安卓开发的入门工程&#xff0c;通过使用按钮Buton来了解一个工程的运作机制。…

Java 语法 (杂七杂八的知识)

面向对象三大特性 封装, 多态, 继承 基本数据类型 一字节 (Byte) 占八位 (bit) JDK, JRE, JVM JDK (Java Development Kit) : Java 开发工具包, 包括了 JRE, 编译器 javac, 和调试工具 Jconsole, jstack 等 JRE (Java Runtime Environment) : Java 运行时环境, 包括了 JVM , …

代码随想录第四十三天|最后一块石头的重量 II 、目标和

题目链接&#xff1a;. - 力扣&#xff08;LeetCode&#xff09; 代码如下&#xff1a; 题目链接&#xff1a;. - 力扣&#xff08;LeetCode&#xff09; 代码如下&#xff1a;

实战教程:个性化生鲜超市小程序制作与运营全解析

生鲜电商行业一直以来都备受关注&#xff0c;而如今&#xff0c;小程序商城成为了这个行业的新潮流。乔拓云平台提供了一个便捷的平台&#xff0c;让我们可以轻松地进入商城后台管理页面。 浏览器搜索【乔拓云】并登陆平台后&#xff0c;我们可以点击【小程序商城】模块&#x…

Marin说PCB之如何快速打印输出整板的丝印位号图?

当小编我辛辛苦苦加班加点的把手上的板子做到投板评审状态的时候&#xff0c;坐在我旁边的日本同事龟田小郎君说让我把板子上的丝印也要调一下&#xff0c;我当时就急了&#xff0c;这么大的板子&#xff0c;将近1W多PIN 了都&#xff0c;光调丝印都要老半天啊&#xff0c;而且…

GDAL:Warning 1: All options related to creation ignored in update mode

01 警告说明 首先贴出相关代码&#xff1a; out_file_name Rs_{:4.0f}{:02.0f}.tiff.format(year, month) out_path os.path.join(out_dir, out_file_name) mem_driver gdal.GetDriverByName(MEM) mem_ds mem_driver.Create(, len(lon), len(lat), 1, gdal.GDT_Float32) …

怎么把多个视频合成一个视频?6个软件教你轻松合成视频

怎么把多个视频合成一个视频&#xff1f;6个软件教你轻松合成视频 合成多个视频成为一个视频可以通过专业的视频编辑软件或在线工具来实现。以下是六个方便使用的软件&#xff0c;它们可以帮助你轻松合成视频&#xff1a; 迅捷视频剪辑软件&#xff1a;这是专业的视频编辑软…

PTP 对时协议 IEEE1588 网络对时 硬件基础

前言 在很多应用场景有精确对时的需求&#xff0c;例如车载网络&#xff0c;音视频流&#xff0c;工业网络。本文档将会阐述对时的硬件需求。 协议 流行的协议为 IEEE1588 标准指定的对时方法&#xff0c;名为 PTP 对时协议。 网卡硬件要求 找到某型网卡的特性描述&#x…

SQL STRING_SPLIT函数,将指定的分隔符将字符串拆分为子字符串行

文章目录 STRING_SPLIT (Transact-SQL)1、语法2、参数3、样例样例1样例2 STRING_SPLIT (Transact-SQL) STRING_SPLIT 是一个表值函数&#xff0c;它根据指定的分隔符将字符串拆分为子字符串行。 1、语法 STRING_SPLIT ( string , separator [ , enable_ordinal ] ) 2、参数…

需求为何如此多

同事的心态炸了 最近各种需求倒排给M同事的心态整炸了&#xff0c;直接撂挑子&#xff0c;从一个TL转为安静的开发人员了。我目睹了整个过程&#xff0c;大抵是理解他的心情的。早年从PMP培训&#xff0c;到哈啰火种计划培训&#xff0c;到后来也带项目&#xff0c;有一些看法…

Powerdesigner导入mysql8之后注释丢失

目录 一、问题描述及解决思路 二、导入的步骤 1.先按正常步骤建立一个物理数据模型 &#xff08;1&#xff09;点击“文件-新建模型” &#xff08;2&#xff09;选择物理模型和数据库 2.从sql文件导入表 &#xff08;1&#xff09;点击“数据库-Update Model from Data…

【ai早报-01 project】

今天和大家分享一款有趣的开源项目 01 Project。 The 01 Project is building an open-source ecosystem for AI devices. 其主旨是基于开源生态&#xff0c;构建以LLM为核心的产品&#xff0c;提供软硬件方案。 市面上类似产品: Rabbit R1, Humane Pin。 如上图所示的这款产…

kafka(七)——消息偏移(消费者)

概念 消费者消费完消息后&#xff0c;向_consumer_offset主题发送消息&#xff0c;用来保存每个分区的偏移量。 流程说明 consumer发送JoinGroup请求&#xff1b;coordinator选出一个consumer作为leader&#xff0c;并将topics发送给leader消费者&#xff1b;leader consumer…

如何使用Transformer-TTS语音合成模型

1、技术原理及架构图 ​ Transformer-TTS主要通过将Transformer模型与Tacotron2系统结合来实现文本到语音的转换。在这种结构中&#xff0c;原始的Transformer模型在输入阶段和输出阶段进行了适当的修改&#xff0c;以更好地处理语音数据。具体来说&#xff0c;Transformer-TT…

NSSCTF Web方向的例题和相关知识点(一)

[SWPUCTF 2021 新生赛]jicao 解题&#xff1a; 打开环境&#xff0c;是一段php代码 包含了flag.php文件&#xff0c;设定了一个POST请求的id和GET请求的json 语句会对GET请求的数据进行json解码 如果id和json变量的值都等于设定字符串&#xff0c;则得到 flag 我们可以使用…

如何让加快OpenHarmony编译速度?

OpenHarmony 有两种编译方式&#xff0c;一种是通过 hb 工具编译&#xff0c;一种是通过 build.sh 脚本编译。本文笔者将提升 build.sh 方式编译速度的方法整理如下&#xff1a; 因为笔者只用 build.sh 脚本编译&#xff0c;没用过 hb 工具&#xff0c;好像下面的选项也可以用于…

Python中使用tkinter模块和类结构的结合使用举例——编写制作一个简单的加数GUI界面

Python中使用tkinter模块和类结构的结合使用举例——编写制作一个简单的加数GUI界面 这里写目录标题 Python中使用tkinter模块和类结构的结合使用举例——编写制作一个简单的加数GUI界面一、tkinter模块和类的简述1.1 tkinter的简要介绍1.2 类结构的简要介绍 二、基于类机构和t…

拼多多强付费二阶段断流怎么办?分几种情况解决

关于断流的问题应该有不少人遇到过&#xff0c;即使是强付费&#xff0c;也不是一直有流量&#xff0c;到了二阶段说断流就断流&#xff0c;同样不能幸免。那么强付费二阶段直接断流是什么原因呢?今天跟大家讲一下强付费断流可能遇到的几种情况&#xff0c;要怎么应对。 第一…