注解式 WebSocket - 构建 群聊、单聊 系统
目录
前言
注解式 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 {@Beanfun 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/*** 连接成功调用的方法*/@OnOpenfun onOpen(session: Session) {//获取当前连接客户端 sessionthis.session = session//加入到 set 中webSocketSet.add(this)println("当前在线人数为: ${webSocketSet.size}")this.session.asyncRemote.sendText("恭喜您成功连接上 WebSocket,当前在线人数为: ${webSocketSet.size}")}/*** 收到客户端消息时调用的方法*/@OnMessagefun onMessage(message: String, session: Session) {println("收到客户端的消息: $message")//群发消息allSend(message)}@OnErrorfun onError(session: Session, error: Throwable) {println("连接异常")error.printStackTrace()}@OnClosefun 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() {//判断当前浏览器是否支持WebSocketif ('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 标记的方法进行保存. 如下:
/*** 连接成功调用的方法*/@OnOpenfun onOpen(session: Session,@PathParam("nickname") nickname: String) {//获取当前连接客户端 sessionthis.session = sessionthis.nickname = nickname//加入到 set 中webSocketSet.add(this)println("$nickname 上线,当前在线人数为: ${webSocketSet.size}")allSend("系统消息: $nickname 上线!")}
发送的消息携带上昵称
@OnMessagefun 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.ServerEndpointdata 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/*** 连接成功调用的方法*/@OnOpenfun onOpen(session: Session,@PathParam("nickname") nickname: String) {//获取当前连接客户端 sessionthis.session = sessionthis.nickname = nickname//加入到 set 中webSocketMap[nickname] = sessionprintln("$nickname 上线,当前在线人数为: ${webSocketMap.size}")allSend("系统消息: $nickname 上线!")}/*** 收到客户端消息时调用的方法*/@OnMessagefun 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.sessionif(targetSession == null) {postSession.asyncRemote.sendText("当前用户不存在或者不在线!")} else {postSession.asyncRemote.sendText("${nickname}: ${message.msg}") //发送者获取自己的消息targetSession.asyncRemote.sendText("${nickname}: ${message.msg}") //接收者获取发送者的消息}}@OnErrorfun onError(session: Session, error: Throwable) {println("连接异常")error.printStackTrace()}@OnClosefun 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() {//判断当前浏览器是否支持WebSocketif ('WebSocket' in window) {let nickname = document.getElementById("nickname").valueif (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}@Resourcefun setUserInfoRepo(userInfoRepo: UserInfoRepo) {Companion.userInfoRepo = userInfoRepo}}
考虑离线消息
只需要再添加一个 ConcurrentHashMap 来记录用户和离线消息~
考虑到消息可能过大,放在内存中不太合适,也可以通过专门设计一个张数据库表来存放用户的离线消息.
当用户再次上线,触发 onOpen 方法时,就可以恢复离线消息啦~
Ps:想要源码可以联系我......
相关文章:

注解式 WebSocket - 构建 群聊、单聊 系统
目录 前言 注解式 WebSocket 构建聊天系统 群聊系统(基本框架) 群聊系统(添加昵称) 单聊系统 WebSocket 作用域下无法注入 Spring Bean 对象? 考虑离线消息 前言 很久之前,咱们聊过 WebSocket 编程式…...

无线游戏手柄的测试(Windows11系统手柄调试方法)
实物 1、把游戏手柄的无线接收器插入到电脑usb接口中 2、【控制面板】----【查看设备和打印机】 3、【蓝牙和其它设备】--【更多设备和打印机设置】 4、鼠标右键【游戏控制器设置】 5、【属性】 6、【测试】(每个按键是否正常) 7、【校准】(…...
计算机的各种转换
一、存量容量的转换 特别注意:1 B 8 bit 转换为:1024 2(10) 括号中的数字为2的指数(即多少次方) 1KB2(10)B1024B; 括号中的数字为2的指数(即多少次方) 1MB2(10)KB1024KB2(20)B; 1GB2(10)MB1024MB2(3…...

Git分布式版本控制系统——Git常用命令(一)
一、获取Git仓库--在本地初始化仓库 执行步骤如下: 1.在任意目录下创建一个空目录(例如GitRepos)作为我们的本地仓库 2.进入这个目录中,点击右键打开Git bash窗口 3.执行命令git init 如果在当前目录中看到.git文件夹&#x…...

【Node.js】短链接
原文链接:Nodejs 第六十二章(短链接) - 掘金 (juejin.cn) 短链接是一种缩短长网址的方法,将原始的长网址转换为更短的形式。短链接的主要用途之一是在社交媒体平台进行链接分享。由于这些平台对字符数量有限制,长网址可…...

详解 Redis 在 Centos 系统上的安装
文章目录 详解 Redis 在 Centos 系统上的安装1. 使用 yum 安装 Redis 52. 创建符号链接3. 修改配置文件4. 启动和停止 Redis 详解 Redis 在 Centos 系统上的安装 1. 使用 yum 安装 Redis 5 如果是Centos8,yum 仓库中默认的 redis 版本就是5,直接 yum i…...

C语言 | Leetcode C语言题解之第17题电话号码的字母组合
题目: 题解: char phoneMap[11][5] {"\0", "\0", "abc\0", "def\0", "ghi\0", "jkl\0", "mno\0", "pqrs\0", "tuv\0", "wxyz\0"};char* digits…...

wordpress全站开发指南-面向开发者及深度用户(全中文实操)--wordpress中的著名循环
wordpress中的著名循环 首先,在深入研究任何代码之前,我们首先要确保我们有不止一篇博客文章可以工作。因此,我们要去自己的wordpress站点,从侧边栏单机Posts(文章),进行创建 在执行代码的时候会优先执行single.php如…...

libVLC 提取视频帧使用QGraphicsView渲染
在前面章节中,我们讲解了如何使用QWidget渲染每一帧视频数据,这种方法对 CPU 负荷较高。 libVLC 提取视频帧使用QWidget渲染-CSDN博客 后面又讲解了使用OpenGL渲染每一帧视频数据,使用 OpenGL去绘制,利用 GPU 减轻 CPU 计算负荷…...

大厂Java笔试题之判断字母大小写
/*** 题目:如果一个由字母组成的字符串,首字母是大写,那么就统计该字符串中大写字母的数量,并输出该字符串中所有的大写字母。否则,就输出* 该字符串不是首字母大写*/ public class Demo2 {public static void main(St…...

场景文本检测识别学习 day02(AlexNet论文阅读、ResNet论文精读)
怎么读论文 在第一遍阅读的时候,只需要看题目,摘要和结论,先看题目是不是跟我的方向有关,看摘要是不是用到了我感兴趣的方法,看结论他是怎么解决摘要中提出的问题,或者怎么实现摘要中的方法,然…...

4.9日总结
1.MySQL概述 1.数据库基本概念:存储数据的仓库,数据是有组织的进行存储 2.数据库管理系统:操纵和管理数据库的大型软件 3.SQL:操作关系型数据库的编程语言,定义了一套操作型数据库统一标准 2.MySQL数据库 关系型数…...

python第四次作业
1、找出10000以内能被5或6整除,但不能被两者同时整除的数(函数) def func():for i in range(10001):if (i % 5 0 or i % 6 0) and i % 30 ! 0:print(i,end " ")func() 2、写一个方法,计算列表所有偶数下标元素的…...
工业通信原理——Modbus-TCP通信规约定义
工业通信原理——Modbus-TCP通信规约定义 前言 Modbus TCP是一种基于TCP/IP协议的通信规约,用于在客户机和服务器之间进行数据通信。 Modbus-TCP通信规约定义 Modbus TCP通信规约的定义,包括客户机请求和服务器响应的基本流程: 连接建立…...

Vue - 4( 8000 字 Vue 入门级教程)
一: Vue 初阶 1.1 关于不同版本的 Vue Vue.js 有不同版本,如 vue.js 与 vue.runtime.xxx.js,这些版本主要针对不同的使用场景和需求进行了优化,区别主要体现在以下几个方面: 完整版 vs 运行时版: vue.js&…...
5.118 BCC工具之xfsslower.py解读
一,工具简介 xfsslower显示了XFS的读取、写入、打开和fsync操作,这些操作慢于一个阈值。 二,代码示例 #!/usr/bin/env pythonfrom __future__ import print_function from bcc import BPF import argparse from time import strftime# arguments examples = ""…...

Spark编程基础
一、RDD入门 1.RDD是什么? RDD是一个容错的、只读的、可进行并行操作的数据结构,是一个分布在集群各个节点中的存放元素的集合,即弹性分布式数据集。 2.RDD的三种创建方式 第一种是将程序中已存在的集合(如集合、列表、数组&a…...
React 状态管理:高效处理数组数据的5种方法
1.原因 为什么在 React 中,状态(state)如果是数组类型,需要单独处理?主要有以下几个原因: 不可变性(Immutability): React 中的状态是不可变的,意味着我们不能直接修改状态,而是要创建一个新的状态对象。对于数组来说,直接修改数组元素是不符合 React 的设计原则的…...
SSH和交换机端口安全概述
交换机的安全是一个很重要的问题,因为它可能会遭受到一些恶意的攻击,例如MAC泛洪攻击、DHCP欺骗和耗竭攻击、中间人攻击、CDP 攻击和Telnet DoS 攻击等,为了防止交换机被攻击者探测或者控制,必须采取相应的措施来确保交换机的安全…...

K-means聚类算法的原理、应用与实例
文章目录 K-means 聚类算法:原理K-means 聚类算法的应用K-means 聚类算法的优化与改进 一个使用 K-means 聚类算法进行客户细分的简单实例 K-means 聚类算法:原理 K-means 算法是一种经典的无监督学习方法,用于对未标记的数据集进行分群&…...
模型参数、模型存储精度、参数与显存
模型参数量衡量单位 M:百万(Million) B:十亿(Billion) 1 B 1000 M 1B 1000M 1B1000M 参数存储精度 模型参数是固定的,但是一个参数所表示多少字节不一定,需要看这个参数以什么…...
pam_env.so模块配置解析
在PAM(Pluggable Authentication Modules)配置中, /etc/pam.d/su 文件相关配置含义如下: 配置解析 auth required pam_env.so1. 字段分解 字段值说明模块类型auth认证类模块,负责验证用户身份&am…...
第25节 Node.js 断言测试
Node.js的assert模块主要用于编写程序的单元测试时使用,通过断言可以提早发现和排查出错误。 稳定性: 5 - 锁定 这个模块可用于应用的单元测试,通过 require(assert) 可以使用这个模块。 assert.fail(actual, expected, message, operator) 使用参数…...

Springcloud:Eureka 高可用集群搭建实战(服务注册与发现的底层原理与避坑指南)
引言:为什么 Eureka 依然是存量系统的核心? 尽管 Nacos 等新注册中心崛起,但金融、电力等保守行业仍有大量系统运行在 Eureka 上。理解其高可用设计与自我保护机制,是保障分布式系统稳定的必修课。本文将手把手带你搭建生产级 Eur…...

Python爬虫(一):爬虫伪装
一、网站防爬机制概述 在当今互联网环境中,具有一定规模或盈利性质的网站几乎都实施了各种防爬措施。这些措施主要分为两大类: 身份验证机制:直接将未经授权的爬虫阻挡在外反爬技术体系:通过各种技术手段增加爬虫获取数据的难度…...
LLM基础1_语言模型如何处理文本
基于GitHub项目:https://github.com/datawhalechina/llms-from-scratch-cn 工具介绍 tiktoken:OpenAI开发的专业"分词器" torch:Facebook开发的强力计算引擎,相当于超级计算器 理解词嵌入:给词语画"…...

EtherNet/IP转DeviceNet协议网关详解
一,设备主要功能 疆鸿智能JH-DVN-EIP本产品是自主研发的一款EtherNet/IP从站功能的通讯网关。该产品主要功能是连接DeviceNet总线和EtherNet/IP网络,本网关连接到EtherNet/IP总线中做为从站使用,连接到DeviceNet总线中做为从站使用。 在自动…...
精益数据分析(97/126):邮件营销与用户参与度的关键指标优化指南
精益数据分析(97/126):邮件营销与用户参与度的关键指标优化指南 在数字化营销时代,邮件列表效度、用户参与度和网站性能等指标往往决定着创业公司的增长成败。今天,我们将深入解析邮件打开率、网站可用性、页面参与时…...

企业如何增强终端安全?
在数字化转型加速的今天,企业的业务运行越来越依赖于终端设备。从员工的笔记本电脑、智能手机,到工厂里的物联网设备、智能传感器,这些终端构成了企业与外部世界连接的 “神经末梢”。然而,随着远程办公的常态化和设备接入的爆炸式…...

安宝特案例丨Vuzix AR智能眼镜集成专业软件,助力卢森堡医院药房转型,赢得辉瑞创新奖
在Vuzix M400 AR智能眼镜的助力下,卢森堡罗伯特舒曼医院(the Robert Schuman Hospitals, HRS)凭借在无菌制剂生产流程中引入增强现实技术(AR)创新项目,荣获了2024年6月7日由卢森堡医院药剂师协会࿰…...