SpringBoot+Vue 整合websocket实现简单聊天窗口

news2024/12/29 8:56:42

效果图

1 输入临时名字充当账号使用
image-1694448636449

2 进入聊天窗口
image-1694448674599

3 发送消息 (复制一个页面,输入其他名字,方便展示效果)
image-1694448766333

4 其他窗口效果
image-1694448783698

代码实现

后端SpringBoot项目,自行创建

pom依赖

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

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-websocket</artifactId>
            <version>2.7.12</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba.fastjson2</groupId>
            <artifactId>fastjson2</artifactId>
            <version>2.0.23</version>
        </dependency>

WebSocketConfig.java

package com.dark.wsdemo.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;

/**
 * WebSocket配置类。开启WebSocket的支持
 */
@Configuration
public class WebSocketConfig {

    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }

}

WebSocketServer.java

package com.dark.wsdemo.service;

import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.dark.wsdemo.vo.MessageVo;
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.util.Date;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * WebSocket的操作类
 */
@Component
@Slf4j
@ServerEndpoint("/websocket/{name}")
public class WebSocketServer {

    /**
     * 静态变量,用来记录当前在线连接数,线程安全的类。
     */
    private static final AtomicInteger onlineSessionClientCount = new AtomicInteger(0);

    /**
     * 存放所有在线的客户端
     */
    private static final Map<String, Session> onlineSessionClientMap = new ConcurrentHashMap<>();

    /**
     * 连接 name 和连接会话
     */
    private String name;

    @OnOpen
    public void onOpen(@PathParam("name") String name, Session session) {
        /**
         * session.getId():当前session会话会自动生成一个id,从0开始累加的。
         */
        Session beforeSession = onlineSessionClientMap.get(name);
        if (beforeSession != null) {
            //在线数减1
            onlineSessionClientCount.decrementAndGet();
            log.info("连接已存在,关闭之前的连接 ==> session_id = {}, name = {}。", beforeSession.getId(), name);
            //通知之前其他地方连接被挤掉
            sendToOne(name, "您的账号在其他地方登录,您被迫下线。");
            // 从 Map中移除
            onlineSessionClientMap.remove(name);
            //关闭之前的连接
            try {
                beforeSession.close();
            } catch (Exception e) {
                log.error("关闭之前的连接异常,异常信息为:{}", e.getMessage());
            }
        }
        log.info("连接建立中 ==> session_id = {}, name = {}", session.getId(), name);
        onlineSessionClientMap.put(name, session);

        //在线数加1
        onlineSessionClientCount.incrementAndGet();
        this.name = name;
        sendToOne(name, "连接成功");
        log.info("连接建立成功,当前在线数为:{} ==> 开始监听新连接:session_id = {}, name = {}。", onlineSessionClientCount, session.getId(), name);
    }


    @OnClose
    public void onClose(@PathParam("name") String name, Session session) {
        if (name == null || name.equals("")) {
            name = this.name;
        }
        // 从 Map中移除
        onlineSessionClientMap.remove(name);

        //在线数减1
        onlineSessionClientCount.decrementAndGet();
        log.info("连接关闭成功,当前在线数为:{} ==> 关闭该连接信息:session_id = {}, name = {}。", onlineSessionClientCount, session.getId(), name);
    }

    @OnMessage
    public void onMessage(String message, Session session) {
        JSONObject jsonObject = JSON.parseObject(message);
        String toname = jsonObject.getString("name");
        String msg = jsonObject.getString("message");
        log.info("服务端收到客户端消息 ==> fromname = {}, toname = {}, message = {}", name, toname, message);

        /**
         * 模拟约定:如果未指定name信息,则群发,否则就单独发送
         */
        if (toname == null || toname == "" || "".equalsIgnoreCase(toname)) {
            sendToAll(msg);
        } else {
            sendToOne(toname, msg);
        }
    }

    /**
     * 发生错误调用的方法
     *
     * @param session
     * @param error
     */
    @OnError
    public void onError(Session session, Throwable error) {
        log.error("WebSocket发生错误,错误信息为:" + error.getMessage());
        error.printStackTrace();
    }

    /**
     * 群发消息
     *
     * @param message 消息
     */
    private void sendToAll(String message) {
        // 遍历在线map集合
        onlineSessionClientMap.forEach((onlineName, toSession) -> {
            // 排除掉自己
            if (!name.equalsIgnoreCase(onlineName)) {
                log.info("服务端给客户端群发消息 ==> name = {}, toname = {}, message = {}", name, onlineName, message);
                MessageVo messageVo = new MessageVo();
                messageVo.setFrom(name);
                messageVo.setDate(new Date());
                messageVo.setMessage(message);
                toSession.getAsyncRemote().sendText(JSON.toJSONString(messageVo));
            }
        });
    }

    /**
     * 指定发送消息
     *
     * @param toName
     * @param message
     */
    private void sendToOne(String toName, String message) {
        // 通过name查询map中是否存在
        Session toSession = onlineSessionClientMap.get(toName);
        if (toSession == null) {
            log.error("服务端给客户端发送消息 ==> toname = {} 不存在, message = {}", toName, message);
            return;
        }
        // 异步发送
        log.info("服务端给客户端发送消息 ==> toname = {}, message = {}", toName, message);
        MessageVo messageVo = new MessageVo();
        messageVo.setFrom(name);
        messageVo.setDate(new Date());
        messageVo.setMessage(message);
        toSession.getAsyncRemote().sendText(JSON.toJSONString(messageVo));

    }

}

MessageVo.java

package com.dark.wsdemo.vo;

import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;

import java.util.Date;

@Data
public class MessageVo {
    private String from;
    //json时候格式化为时间格式
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    private Date date;
    private String message;
}

Vue代码实现

App.vue

<template>
  <div id="app">
    <!-- Modal Dialog -->
    <div class="modal" v-if="!username">
      <div class="modal-content">
        <h2>请输入你的名字</h2>
        <input type="text" v-model="inputUsername" />
        <button @click="setUsername">确定</button>
      </div>
    </div>

    <!-- Chat Box -->
    <div class="chat-box" v-if="username">
      <div class="chat-history">
        <div v-for="msg in messages" :key="msg.id" :class="[msg.type, 'message']">
          <div class="info">
            <span class="from">{{ msg.from }}</span>
            <span class="date">{{ msg.date }}</span>
          </div>
          <div class="bubble">
            {{ msg.message }}
          </div>
        </div>
      </div>
      <div class="chat-input">
        <input type="text" v-model="inputMessage" @keyup.enter="sendMessage" placeholder="请输入消息..."/>
        <button @click="sendMessage">发送</button>
      </div>
    </div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      inputMessage: '',
      inputUsername: '',
      messages: [],
      username: '',
      ws: null,
    };
  },
  methods: {
    setUsername() {
      if (this.inputUsername.trim() === '') return;
      this.username = this.inputUsername.trim();
      this.ws = new WebSocket(`ws://localhost:8081/websocket/${this.username}`);

      this.ws.addEventListener('message', (event) => {
        const data = JSON.parse(event.data);
        this.messages.push({ ...data, type: 'left', id: this.messages.length });
      });
    },
    sendMessage() {
      if (this.inputMessage.trim() === '') return;
      const message = {
        from: this.username,
        date: new Date().toLocaleString(),
        message: this.inputMessage.trim(),
      };
      this.ws.send(JSON.stringify(message));
      this.messages.push({ ...message, type: 'right', id: this.messages.length });
      this.inputMessage = '';
    },
  },
};
</script>

<style>
/* Modal Styles */
.modal {
  display: flex;
  justify-content: center;
  align-items: center;
  position: fixed;
  left: 0;
  top: 0;
  width: 100%;
  height: 100%;
  background-color: rgba(0, 0, 0, 0.5);
  z-index: 9999;
}

.modal-content {
  background-color: #fff;
  padding: 20px;
  width: 300px;
  text-align: center;
  border-radius: 10px;
  box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}

/* Chat Box Styles */
#app {
  background-color: #f2f2f2;
  display: flex;
  justify-content: center;
  align-items: center;
  height: 100vh;
  margin: 0;
  font-family: Arial, sans-serif;
}

.chat-box {
  box-shadow: 0 8px 16px 0 rgba(0,0,0,0.2);
  width: 300px;
  height: 400px;
  border-radius: 8px;
  overflow: hidden;
  display: flex;
  flex-direction: column;
}

.chat-history {
  flex: 1;
  overflow-y: auto;
  padding: 10px;
  background-color: #fff;
}

.message {
  padding: 5px 0;
}

.info {
  font-size: 12px;
  color: gray;
  margin-bottom: 4px;
}

.left .bubble {
  background-color: #e6e6e6;
  border-radius: 15px;
  padding: 12px;
  display: inline-block;
}

.right .bubble {
  background-color: #007bff;
  color: white;
  border-radius: 15px;
  padding: 12px;
  display: inline-block;
  margin-left: auto;
}

.chat-input {
  display: flex;
  padding: 10px;
  background-color: #f7f7f7;
  border-top: 1px solid #ccc;
}

input {
  flex: 1;
  padding: 8px;
  border: 1px solid #ccc;
  border-radius: 4px;
  margin-right: 10px;
}

button {
  padding: 10px 20px;
  background-color: #007bff;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}

button:hover {
  background-color: #0056b3;
}
</style>

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

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

相关文章

docker安装xxl-job连接数据库时显示无法连接问题

背景&#xff1a; 在项目中需要定时任务调度&#xff0c;需要在docker容器中安装xxl-job 遇到的问题 部署成功后&#xff0c;可以访问xxl-job登录界面&#xff0c;点登录没反应&#xff0c;但过一段时间就弹出数据库拒绝连接&#xff0c;说MyBatis连接用户失败 原因&#xf…

华为云API图像识别Image的趣味性—AI识别迈克尔·杰克逊

云服务、API、SDK&#xff0c;调试&#xff0c;查看&#xff0c;我都行 阅读短文您可以学习到&#xff1a;人工智能AI图像识别的图像识别、名人识别 1 IntelliJ IDEA 之API插件介绍 API插件支持 VS Code IDE、IntelliJ IDEA等平台、以及华为云自研 CodeArts IDE&#xff0c;基…

深度学习算法

深度学习算法 1. 各种网络框架及其联系1.1 两阶段与一阶段区别1.1.1 detectron算法框架套路&#xff1a;1.1.2 multi-stage1.1.3 two-stage 算法1.1.4 one-stage 算法 2. 常用算法2.1 SS(选择性搜索算法&#xff0c;Selective Search) 3. 神经元模型4. 神经网络分类4.1 前馈神经…

Linux内核分析与应用5-中断

本系列是对 陈莉君 老师 Linux 内核分析与应用[1] 的学习与记录。讲的非常之好&#xff0c;推荐观看 留此记录&#xff0c;蜻蜓点水,可作抛砖引玉 中断机制概述 中断是CPU对系统发生的某个事件作出的一种反应, 当中断发生时,CPU暂停正在执行的程序,保留现场后,自动转去执行相应…

一本快速入门Java的书

关于这本书 很高兴&#xff0c;我又一本书籍《Java编程动手学》上市了。记得早在2017年&#xff0c;在我跟人邮出版社的傅道坤编辑合作完《Tomcat内核设计剖析》这本书后&#xff0c;傅编就问我考不考虑写一本面向Java初学者的图书&#xff0c;当时我谢绝了傅编的邀请。一来是我…

总结986

时间记录&#xff1a; 7:10起床 8:00~下午2:00课程设计&#xff0c;偷学了3小时 2:17~3:55午觉 4:10~5:30计网 5:35~6:41数据结构 7:00~7:22继续数据结构课后习题重做 7:23~8:07考研政治&#xff0c;做题20道纠错 8:15~8:39每日长难句 8:39~10:21 14年tex2纠错标记 1…

Unity下如何实现RTMP或RTSP播放端录像?

好多开发者问我们&#xff0c;Unity环境下&#xff0c;除了RTSP或RTMP的播放&#xff0c;如果有录像诉求&#xff0c;怎么实现&#xff1f;实际上录像相对播放来说&#xff0c;更简单一些&#xff0c;因为不涉及到绘制&#xff0c;只要拉流下来数据&#xff0c;直接写mp4文件就…

pytorch代码实现之SAConv卷积

SAConv卷积 SAConv卷积模块是一种精度更高、速度更快的“即插即用”卷积&#xff0c;目前很多方法被提出用于降低模型冗余、加速模型推理速度&#xff0c;然而这些方法往往关注于消除不重要的滤波器或构建高效计算单元&#xff0c;反而忽略了特征内部的模式冗余。 原文地址&am…

BUUCTF Reverse/[羊城杯 2020]login(python程序)

查看信息,python文件 动调了一下&#xff0c;该程序创建了一个线程来读入数据&#xff0c;而这个线程的代码应该是放在内存中直接执行的&#xff0c;本地看不到代码&#xff0c;很蛋疼 查了下可以用PyInstaller Extractor工具来解包&#xff0c;可以参考这个Python解包及反编译…

华为云云服务器云耀L实例评测 | 在华为云耀L实例上搭建电商店铺管理系统:一次场景体验

&#x1f337;&#x1f341; 博主猫头虎&#xff08;&#x1f405;&#x1f43e;&#xff09;带您 Go to New World✨&#x1f341; &#x1f984; 博客首页——&#x1f405;&#x1f43e;猫头虎的博客&#x1f390; &#x1f433; 《面试题大全专栏》 &#x1f995; 文章图文…

sqli第一关

1.在下使用火狐访问sqlilabs靶场并使用burpsuite代理火狐。左为sqlilabs第一关&#xff0c;右为burpsuite。 2.输入?id1 and 11 与?id1 and 12试试 可以看出没有变化哈&#xff0c;明显我们输入的语句被过滤了。在?id1后面尝试各种字符&#xff0c;发现单引号 包…

Linux内核分析与应用4-内存管理

本系列是对 陈莉君 老师 Linux 内核分析与应用[1] 的学习与记录。讲的非常之好&#xff0c;推荐观看 留此记录&#xff0c;蜻蜓点水,可作抛砖引玉 4.1 Linux内存管理机制 lscpu[2] 命令, 类似是优化后的 cat /proc/cpuinfo 实现虚拟内存的几种机制: 当 程序一旦跑起来,那就变成…

IDEA在创建包时如何把包分开实现自动分层

IDEA在创建包时如何把包分开实现自动分层 文章目录 IDEA在创建包时如何把包分开实现自动分层一、为什么要把包分开二、建包时如何把包自动分开三、如何编写配置文件路径&#xff1f; 一、为什么要把包分开 一开始的时候&#xff0c;我也一直以为包连在一起和分开没什么区别&am…

二叉搜索树/二叉排序树/二叉查找树

文章目录 1.概念2.操作3.实现3.1框架3.2BSTree.h3.3test.cpp 1.概念 二叉搜索树又称二叉排序树&#xff0c;它或者是一棵空树&#xff0c;或者是具有以下性质的二叉树: 若它的左子树不为空&#xff0c;则左子树上所有节点的值都小于根节点的值 若它的右子树不为空&#xff0c;…

python 学习笔记(5)——SMTP 使用QQ邮箱发送邮件

目录 发送邮件 1、准备工作&#xff1a; 2、发送纯文本信息内容&#xff1a; 3、发送 HTML 格式的内容&#xff1a; 4、发送带附件的邮件&#xff1a; 5、群发&#xff08;一个邮件&#xff0c;发给多个人&#xff09;&#xff1a; 发送邮件 以下都 以 QQ邮箱 为发送方举…

敏捷开发方法管理项目,适应变化,引领未来

​敏捷开发方法是一种灵活且高效的项目管理方法&#xff0c;旨在应对不断变化的需求和快速发展的项目环境。使用敏捷开发方法可以帮助团队更好地应对不确定性&#xff0c;提高项目的质量和效率。以下是使用敏捷开发方法管理项目的具体步骤&#xff1a; 明确项目目标和范围 在…

算法通过村第六关-树白银笔记|层次遍历

文章目录 前言1. 层次遍历介绍2. 基本的层次遍历与变换2.1 二叉树的层次遍历2.2 层次遍历-自底向上2.3 二叉树的锯齿形层次遍历2.4 N叉树的层次遍历 3. 几个处理每层元素的题目3.1 在每棵树行中找出最大值3.2 在每棵树行中找出平均值3.3 二叉树的右视图3.4 最底层最左边 总结 前…

C高级day4(shell脚本)

一、Xmind整理&#xff1a; 二、上课笔记整理&#xff1a; 1.创建一个文件&#xff0c;给组用户可读权限&#xff0c;所属用户可写权限&#xff0c;其他用户可执行权限&#xff0c;使用if判断文件有哪些权限 #!/bin/bash touch 1 chmod 241 1 if [ -r 1 ] thenecho "文件…

为 DevOps 战士准备的 Linux 命令

点击链接了解详情 这篇文章将帮助理解DevOps工程师所需的大部分重要且经常使用的Linux命令。 要执行这些命令&#xff0c;你可以使用任何Linux机器、虚拟机或在线Linux终端来迅速开始使用这些命令。 系统信息命令&#xff1a; hostname - 显示系统主机的名称。 hostid - 显示…

openGauss学习笔记-66 openGauss 数据库管理-创建和管理schema

文章目录 openGauss学习笔记-66 openGauss 数据库管理-创建和管理schema66.1 背景信息66.2 注意事项66.3 操作步骤66.3.1 创建管理用户及权限schema66.3.2 使用schema66.3.3 schema的搜索路径66.3.4 schema的权限控制66.3.5 删除schema openGauss学习笔记-66 openGauss 数据库管…