注解式 WebSocket - 构建 群聊、单聊 系统

news2024/10/7 3:18:47

目录

前言

注解式 WebSocket 构建聊天系统

群聊系统(基本框架)

群聊系统(添加昵称)

单聊系统

WebSocket 作用域下无法注入 Spring Bean 对象?

考虑离线消息


前言


很久之前,咱们聊过 WebSocket 编程式的写法,但是有些过于繁琐,这次来看看更接近现代的注解式,构建 群聊、单聊 有多么便利.

注解式 WebSocket 构建聊天系统


群聊系统(基本框架)

a)定义 WebSocket 配置类.

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

/**
 * 注入对象 ServerEndpointExporter
 * 这个 bean 会自动注册使用了 @ServerEndpoint 注解声明的 WebSocket endpoint
 */

@Configuration
class WebSocketConfig {

    @Bean
    fun serverEndpointExporter() = ServerEndpointExporter()

}

b)WebSocket 实现类

import org.springframework.stereotype.Component
import java.util.concurrent.CopyOnWriteArraySet
import javax.websocket.OnClose
import javax.websocket.OnError
import javax.websocket.OnMessage
import javax.websocket.OnOpen
import javax.websocket.Session
import javax.websocket.server.ServerEndpoint

/**
 * 虽然此处 @Component 默认是单例的,但是 SpringBoot 还是会为每个 WebSocket 初始化一个 bean,
 * 因此可以使用一个静态的 Set 保存起来(CopyOnWriteArraySet 相比于 HashSet 是线程安全的)
 */
@ServerEndpoint(value = "/websocket")
@Component
class MyWebSocket {

    companion object {
        //用来存放每个客户端对应的 MyWebSocket 对象
        private val webSocketSet = CopyOnWriteArraySet<MyWebSocket>()
    }

    //与某个客户都安连接的会话,需要通过他来给客户都安发送数据
    private lateinit var session: Session

    /**
     * 连接成功调用的方法
     */
    @OnOpen
    fun onOpen(session: Session) {
        //获取当前连接客户端 session
        this.session = session
        //加入到 set 中
        webSocketSet.add(this)
        println("当前在线人数为: ${webSocketSet.size}")
        this.session.asyncRemote.sendText("恭喜您成功连接上 WebSocket,当前在线人数为: ${webSocketSet.size}")
    }

    /**
     * 收到客户端消息时调用的方法
     */
    @OnMessage
    fun onMessage(message: String, session: Session) {
        println("收到客户端的消息: $message")
        //群发消息
        allSend(message)
    }

    @OnError
    fun onError(session: Session, error: Throwable) {
        println("连接异常")
        error.printStackTrace()
    }

    @OnClose
    fun onClose() {
        webSocketSet.remove(this)
        println("有人下线!当前在线人数: ${webSocketSet.size}")
    }

    /**
     * 自定义群发消息
     * basicRemote: 阻塞式
     * asyncRemote: 非阻塞式
     * 大部分情况下更推荐使用 asyncRemote, 详情: https://blog.csdn.net/who_is_xiaoming/article/details/53287691
     */
    private fun allSend(message: String) {
        webSocketSet.forEach {
            //it.session.basicRemote.sendText(message)
            it.session.asyncRemote.sendText(message)
        }
    }

}

c)客户端开发

<!DOCTYPE HTML>
<html>

<head>
  <meta charset="UTF-8">
  <title>My WebSocket</title>
  <style>
    #message {
      margin-top: 40px;
      border: 1px solid gray;
      padding: 20px;
    }
  </style>
</head>

<body>
  <button onclick="conectWebSocket()">连接WebSocket</button>
  <button onclick="closeWebSocket()">断开连接</button>
  <hr />
  <br />
  消息:<input id="text" type="text" />
  <button onclick="send()">发送消息</button>
  <div id="message"></div>
</body>
<script type="text/javascript">

  var websocket = null;

  function conectWebSocket() {
    //判断当前浏览器是否支持WebSocket
    if ('WebSocket' in window) {
      websocket = new WebSocket("ws://localhost:9000/websocket");
    } else {
      alert('Not support websocket')
    }

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

    //连接成功建立的回调方法
    websocket.onopen = function (event) {
      setMessageInnerHTML("tips: 连接成功!");
    }

    //接收到消息的回调方法
    websocket.onmessage = function (event) {
      setMessageInnerHTML(event.data);
    }

    //连接关闭的回调方法
    websocket.onclose = function () {
      setMessageInnerHTML("tips: 关闭连接");
    }

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

  }

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


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

  //发送消息
  function send() {
    var message = document.getElementById('text').value;
    websocket.send(message);
  }

</script>

</html>

d)效果如下:

打开两个浏览器,依次点击建立连接

左边的浏览器中输入:"你好,我是 cyk",效果如下

群聊系统(添加昵称)

上述聊天系统中可以看到,并不知道当前消息是哪一个用户发出的,因此这里我们改造一下,让每个消息前携带用户名.

a)客户端改造:在用户点击 "连接 WebSocket" 之前输入昵称,以此作为消息的身份标识.

b)服务端改造:

之后在 WebSocket 注解标记的每一个方法中,都可以通过 @PathParam("nickname") nickname: String 获取到 nickname.

尽管如此,再上图中我还是使用成员变量 nickname 在 WebSocket 第一次建立连接的时候通过 @onOpen 标记的方法进行保存. 如下:

    /**
     * 连接成功调用的方法
     */
    @OnOpen
    fun onOpen(
        session: Session,
        @PathParam("nickname") nickname: String
    ) {
        //获取当前连接客户端 session
        this.session = session
        this.nickname = nickname
        //加入到 set 中
        webSocketSet.add(this)
        println("$nickname 上线,当前在线人数为: ${webSocketSet.size}")
        allSend("系统消息: $nickname 上线!")
    }

发送的消息携带上昵称

    @OnMessage
    fun onMessage(message: String, session: Session) {
        println("收到客户端的消息: $message")
        //群发消息
        allSend("$nickname: $message")
    }

c)效果如下:

单聊系统

a)服务器开发:需要通过一个 map 来记录用户的 session 信息(key:用户唯一标识,value: session)

ChatMsg:用来接收客户端传入的 JSON 消息(通过 ObjectMapper 反序列化).

onOpen:记录用户信息到 map 中.

opMessage:将消息转发给目标人物.

import com.fasterxml.jackson.databind.ObjectMapper
import org.springframework.stereotype.Component
import java.util.concurrent.ConcurrentHashMap
import javax.websocket.OnClose
import javax.websocket.OnError
import javax.websocket.OnMessage
import javax.websocket.OnOpen
import javax.websocket.Session
import javax.websocket.server.PathParam
import javax.websocket.server.ServerEndpoint

data class ChatMsg (
    val targetName: String = "", //目标
    val msg: String = "",        //消息
)

/**
 * 虽然此处 @Component 默认是单例的,但是 SpringBoot 还是会为每个 WebSocket 初始化一个 bean,
 * 因此可以使用一个静态的 Set 保存起来(CopyOnWriteArraySet 相比于 HashSet 是线程安全的)
 */
@ServerEndpoint(value = "/websocket/{nickname}")
@Component
class MyWebSocket {

    companion object {
        //用来存放每个客户端对应的 MyWebSocket 对象
        private val webSocketMap = ConcurrentHashMap<String, Session>()
    }

    //与某个客户都安连接的会话,需要通过他来给客户都安发送数据
    private lateinit var session: Session //用来记录当前连接者会话
    private lateinit var nickname: String

    /**
     * 连接成功调用的方法
     */
    @OnOpen
    fun onOpen(
        session: Session,
        @PathParam("nickname") nickname: String
    ) {
        //获取当前连接客户端 session
        this.session = session
        this.nickname = nickname
        //加入到 set 中
        webSocketMap[nickname] = session
        println("$nickname 上线,当前在线人数为: ${webSocketMap.size}")
        allSend("系统消息: $nickname 上线!")
    }

    /**
     * 收到客户端消息时调用的方法
     */
    @OnMessage
    fun onMessage(messageJson: String, session: Session) {
        println("收到客户端的消息: $messageJson")
        //单独发送消息
        val mapper = ObjectMapper()
        val message = mapper.readValue(messageJson, ChatMsg::class.java)
        val targetSession = webSocketMap[message.targetName]
        val postSession = this.session
        if(targetSession == null) {
            postSession.asyncRemote.sendText("当前用户不存在或者不在线!")
        } else {
            postSession.asyncRemote.sendText("${nickname}: ${message.msg}") //发送者获取自己的消息
            targetSession.asyncRemote.sendText("${nickname}: ${message.msg}") //接收者获取发送者的消息
        }
    }

    @OnError
    fun onError(session: Session, error: Throwable) {
        println("连接异常")
        error.printStackTrace()
    }

    @OnClose
    fun onClose() {
        webSocketMap.remove(nickname)
        println("${nickname} 下线!当前在线人数: ${webSocketMap.size}")
        allSend("系统消息: $nickname 下线!")
    }


    /**
     * 自定义群发消息
     * basicRemote: 阻塞式
     * asyncRemote: 非阻塞式
     * 大部分情况下更推荐使用 asyncRemote, 详情: https://blog.csdn.net/who_is_xiaoming/article/details/53287691
     */
    private fun allSend(message: String) {
        webSocketMap.forEach {
            it.value.asyncRemote.sendText(message)
        }
    }

}

b)客户端开发

<!DOCTYPE HTML>
<html>

<head>
  <meta charset="UTF-8">
  <title>My WebSocket</title>
  <style>
    #message {
      margin-top: 40px;
      border: 1px solid gray;
      padding: 20px;
    }
  </style>
</head>

<body>
  <div>
    <span>昵称: </span>
    <input type="text" id="nickname">
  </div>
  <button onclick="conectWebSocket()">连接WebSocket</button>
  <button onclick="closeWebSocket()">断开连接</button>
  <hr />
  <br />

  <div>
    <span>targetName: </span>
    <input type="text" id="targetName">
  </div>

  <div>
    <span>消息: </span>
    <input id="text" type="text" />
  </div>
  <button onclick="send()">发送消息</button>
  <div id="message"></div>
</body>
<script type="text/javascript">

  var websocket = null;

  function conectWebSocket() {
    //判断当前浏览器是否支持WebSocket
    if ('WebSocket' in window) {
      let nickname = document.getElementById("nickname").value
      if (nickname == null || nickname == "") {
        alert("请先输入昵称!")
        return
      }
      websocket = new WebSocket("ws://localhost:9000/websocket/" + nickname);
    } else {
      alert('Not support websocket')
    }

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

    //连接成功建立的回调方法
    websocket.onopen = function (event) {
      setMessageInnerHTML("tips: 连接成功!");
    }

    //接收到消息的回调方法
    websocket.onmessage = function (event) {
      setMessageInnerHTML(event.data);
    }

    //连接关闭的回调方法
    websocket.onclose = function () {
      setMessageInnerHTML("tips: 关闭连接");
    }

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

  }

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


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

  //发送消息
  function send() {
    var message = document.getElementById('text').value;
    var targetName = document.getElementById('targetName').value;
    var chatMsg = {
      "targetName": targetName,
      "msg": message
    }
    websocket.send(JSON.stringify(chatMsg));
  }

</script>

</html>

 

WebSocket 作用域下无法注入 Spring Bean 对象?

这是因为 Spring 管理 Bean 对象默认都是单例的,而 WebSocket 却是多例的,因此注入 Spring 中的 Bean 对象会冲突. 

解决办法:通过 set 方法注入一个静态的 Bean 即可.

@ServerEndpoint("/websocket/{id}")
@Component
class ChatRoom {

    companion object {
        private lateinit var userInfoRepo: UserInfoRepo
    }

    @Resource
    fun setUserInfoRepo(userInfoRepo: UserInfoRepo) {
        Companion.userInfoRepo = userInfoRepo
    }


}

考虑离线消息

只需要再添加一个 ConcurrentHashMap 来记录用户和离线消息~ 

考虑到消息可能过大,放在内存中不太合适,也可以通过专门设计一个张数据库表来存放用户的离线消息.

当用户再次上线,触发 onOpen 方法时,就可以恢复离线消息啦~

Ps:想要源码可以联系我......

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

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

相关文章

Nuxt 3 项目中配置 Tailwind CSS

官方文档&#xff1a;https://www.tailwindcss.cn/docs/guides/nuxtjs#standard 安装 Tailwind CSS 及其相关依赖 执行如下命令&#xff0c;在 Nuxt 项目中安装 Tailwind CSS 及其相关依赖 npm install -D tailwindcss postcss autoprefixerpnpm install -D tailwindcss post…

字符迁移.

3.字符迁移【算法赛】 - 蓝桥云课 (lanqiao.cn) 问题描述 小蓝最近获得了一个长度为N 的字符串S&#xff0c;他对它爱不释手。 小桥为了考验小蓝对字符串的处理能力&#xff0c;决定给他提出一个挑战&#xff0c;她会进行 Q次操作&#xff1a; 每次操作给定三个整数 l , r , k …

Vue3调试

如何对vue3项目进行调试 调试是开发过程中必备的一项技能&#xff0c;掌握了这项技能&#xff0c;可以很好的定义bug所在。一般在开发vue3项目时&#xff0c;有三种方式。 代码中添加debugger;使用浏览器调试&#xff1a;sourcemap需启用vs code 调试&#xff1a;先开启node服…

夯实智慧新能源数据底座,TiDB Serverless 在 Sandisolar+ 的应用实践

本文介绍了 SandiSolar通过 TiDB Serverless 构建智慧新能源数据底座的思路与实践。作为一家致力于为全球提供清洁电力解决方案的新能源企业&#xff0c;SandiSolar面临着处理大量实时数据的挑战。为了应对这一问题&#xff0c;SandiSolar选择了 TiDB Serverless 作为他们的数据…

PostgrerSQL基本使用与数据备份

前言 上篇了解了 PostgrerSQL 数据库的部署PostgreSQL关系型数据库介绍与部署-CSDN博客&#xff0c;本篇将继续就其基本操作、备份与还原内容做相关介绍。 目录 一、数据库的操作 1. 本机登录 2. 开启远程登录 2.1 开放远程端口 2.2 编辑配置文件 2.3 修改配置密码 2.…

基于单片机高压输电线路微机保护系统设计

**单片机设计介绍&#xff0c;基于单片机高压输电线路微机保护系统设计 文章目录 一 概要二、功能设计三、 软件设计原理图 五、 程序六、 文章目录 一 概要 基于单片机高压输电线路微机保护系统设计是一个涉及电力系统继电保护的复杂工程。该系统主要利用单片机作为控制核心&…

【深度学习】海洋生物数据集,图片分类

文章目录 任务描述数据收集数据处理模型训练指标评测web app代码和帮助 任务描述 收集9种以上的海洋生物图片&#xff0c;然后基于深度学习做一个分类模型&#xff0c;训练完成后&#xff0c;分类模型就可以对未知图片进行分类。 在之后随便传一张图片&#xff0c;分类模型就…

MySQL 50 道查询题汇总,足以巩固大部分查询(附带数据准备SQL、题型分析、演示、50道题的完整SQL)

目录 MySQL 50 道查询题&#xff0c;足以巩固大部分查询数据准备&#xff1a;创建表sql添加表数据sql 50道查询题目汇总01 - 05 题&#xff1a;1、查询 “01” 语文成绩比 “02” 数学成绩高的学生的信息及课程分数2、查询 "01语文课程"比"02数学课程"成绩…

【前端】JavaScript(概念+语法+形式+变量+数组+函数+作用域)

文章目录 JavaScript一、JavsScript概念1.JavaScript的开发方向2.JavaScript和CSS、HTML的关系3.JavaScript运行过程4.JavaScript的组成 二、JavaScript的语法1.JS的书写形式1.行内式2.内嵌式3.外部式4.注释5.输入输出1.prompt和alert2.输出: console.log 2.变量的使用1.创建变…

如何使用CSS构建一个瀑布流布局

如何使用CSS构建一个瀑布流布局 瀑布流布局是一种常见的网页布局方式&#xff0c;其中元素以不同的大小排列&#xff0c;且行与列之间没有不均匀的间隙。在瀑布流布局中&#xff0c;即使某一行或列中的元素较短&#xff0c;下一个元素也会占据空间。 如何实现瀑布流布局 实现…

双连通分量算法

1. 连通图概念 连通图&#xff1a;无向图任意两点之间存在通路。 强连通&#xff1a;有向图&#xff08;前提&#xff09;中&#xff0c;任意两点都有至少一条通路&#xff0c;则此图为强连通图。 弱连通图&#xff1a;将有向图的有向边换成无向边得到的图是连通图&#xff0c…

如何在 Ubuntu 上安装和配置 Tomcat 服务器?

简介&#xff1a;最近有粉丝朋友在问如何在 Ubuntu 上安装和配置 Tomcat 服务器&#xff1f;今天特地写这篇文章进行解答&#xff0c;希望能够帮助到大家。 文章目录 Ubuntu上安装和配置Tomcat的详细步骤Tomcat在Linux环境下的安装与配置一、下载并上传Tomcat压缩包二、启动To…

【单片机】CJSH22-CH2O,甲醛传感器,甲醛传感器数据读取处理

原理图 解析程序 逻辑是&#xff1a; 1、初始化串口和定时器10ms中断 2、循环读取一帧数据到rev_CH2O_bufferdata 3、在主函数解析数据rev_CH2O_bufferdata 4、最终的pm2.5数值就是CH2O_value 使用CH2O_value的数据即可。 PPB单位&#xff0c;除以1000就是ppm&#xff0c;再…

华为ensp中高级acl (控制列表) 原理和配置命令 (详解)

作者主页&#xff1a;点击&#xff01; ENSP专栏&#xff1a;点击&#xff01; 创作时间&#xff1a;2024年4月6日23点18分 高级acl&#xff08;Access Control List&#xff09;是一种访问控制列表&#xff0c;可以根据数据包的源IP地址、目标IP地址、源端口、目标端口、协议…

每日五道java面试题之ZooKeeper篇(三)

目录&#xff1a; 第一题. 会话管理第二题. 服务器角色第三题. Zookeeper 下 Server 工作状态第四题. 数据同步第五题. zookeeper 是如何保证事务的顺序一致性的&#xff1f; 第一题. 会话管理 分桶策略&#xff1a;将类似的会话放在同一区块中进行管理&#xff0c;以便于 Zoo…

Chatgpt掘金之旅—有爱AI商业实战篇|社交媒体管理|(七)

演示站点&#xff1a; https://ai.uaai.cn 对话模块 官方论坛&#xff1a; www.jingyuai.com 京娱AI 一、AI技术社交媒体创业有哪些机会&#xff1f; 人工智能&#xff08;AI&#xff09;技术作为当今科技创新的前沿领域&#xff0c;为创业者提供了广阔的机会和挑战。随着AI技…

Lecture 1~3 About Filter

文章目录 空间域上的滤波器- 线性滤波器盒状滤波器Box Filter锐化Sharpening相关运算 vs. 卷积运算 Correlation vs. Convolution - 非线性滤波器高斯滤波器Gaussian filter - 实际问题- 纹理texture 频域上的滤波器 滤波的应用- 模板匹配- 图像金字塔 空间域上的滤波器 图像…

假期别闲着:REST API实战演练之客户端使用Rest API

在上一篇中我们说了一下如何创建简单的rest api&#xff08;假期别闲着&#xff1a;REST API实战演练之创建Rest API-CSDN博客&#xff09;&#xff0c;我们创建了那就是为了使用的&#xff0c;下面我们就看看&#xff0c;通过构建一个客户端程序如何使用我们创建的rest api吧。…

深入了解iPhone GPU技术:优化游戏图形渲染

摘要 了解你的显卡对于在电脑上玩现代图形要求高的游戏非常重要。本文介绍了如何轻松查看你的显卡型号以及为什么显卡在玩电脑游戏时如此关键。 引言 随着电脑游戏的发展&#xff0c;现代游戏对硬件性能的要求越来越高。十年前发布的显卡已经无法满足当前游戏的需求。因此&…

DC9 Debian和sql注入

信息收集 sudo arp-scan -l 列出局域网主机 arp-scan向局域网中所有可能的ip地址发出arp请求包&#xff0c;如果得到arp回应&#xff0c;就证明局域网中某台主机使用了该ip dc9的ip &#xff1a; 192.168.146.133 访问网页 cms为Debian 端口扫描 22端口是filtered 隐藏目…