注解式 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 算法是一种经典的无监督学习方法,用于对未标记的数据集进行分群&…...

使用SquareLine Studio创建LVGL项目到IMX6uLL平台
文章目录 前言一、SquareLine Studio是什么?二、下载安装三、工程配置四、交叉编译 前言 遇到的问题:#error LV_COLOR_DEPTH should be 16bit to match SquareLine Studios settings,解决方法见# 四、交叉编译 一、SquareLine Studio是什么…...

MATLAB计算投资组合的cVaR和VaR
计算条件风险价值 (Conditional Value-at-Risk, cVaR) 是一种衡量投资组合风险的方法,它关注的是损失分布的尾部风险。 MATLAB代码如下: clc;close all;clear all;warning off;%清除变量 rand(seed, 100); randn(seed, 100); format long g;% 随机产生数据&#x…...

YOLOv7全网独家改进: 卷积魔改 | 变形条状卷积,魔改DCNv3二次创新
💡💡💡本文独家改进: 变形条状卷积,DCNv3改进版本,不降低精度的前提下相比较DCNv3大幅度运算速度 💡💡💡强烈推荐:先到先得,paper级创新,直接使用; 💡💡💡创新点:1)去掉DCNv3中的Mask;2)空间域上的双线性插值转改为轴上的线性插值; 💡💡💡…...

使用vue3搭建一个CRM(客户关系管理)系统
目录 1. 需求分析 2. 设计 3. 技术选型 4. 开发环境搭建 5. 前端开发 6. 后端开发 7. 数据库搭建 8. 测试 9. 部署 10. 维护和迭代 总结 搭建一个CRM(客户关系管理)系统是一个复杂的项目,涉及到需求分析、设计、开发、测试和部署等…...

Linux虚拟内存简介
Linux,像多数现代内核一样,采用了虚拟内存管理技术。该技术利用了大多数程序的一个典型特征,即访问局部性(locality of reference),以求高效使用CPU和RAM(物理内存)资源。大多数程序…...

合并单元格的excel文件转换成json数据格式
github地址: https://github.com/CodeWang-Ay/DataProcess 类型1 需求1: 类似于数据格式: https://blog.csdn.net/qq_44072222/article/details/120884158 目标json格式 {"位置": 1, "名称": "nba球员", "国家": "美国"…...

云平台和云原生
目录 1.0 云平台 1.1.0 私有云、公有云、混合云 1.1.1 私有云 1.1.2 公有云 1.1.3 混合云 1.2 常见云管理平台 1.3 云管理的好处 1.3.1 多云的统一管理 1.3.2 跨云资源调度和编排需要 1.3.3 实现多云治理 1.3.4 多云的统一监控和运维 1.3.5 统一成本分析和优化 1.…...

ES6 => 箭头函数
目录 语法基本形式 参数 函数体 特点 箭头函数(Arrow Function)是ES6(ECMAScript 2015)中引入的一种新的函数语法,它提供了一种更简洁的方式来编写函数。箭头函数有几个显著的特点和优势,下面我们来详细…...

vue将html生成pdf并分页
jspdf html2canvas 此方案有很多的css兼容问题,比如虚线边框、svg、页数多了内容显示不全、部分浏览器兼容问题,光是解决这些问题就耗费了我不少岁月和精力 后面了解到新的技术方案: jspdf html-to-image npm install --save html-to-i…...

数字社会下的智慧公厕:构筑智慧城市的重要组成部分
智慧城市已经成为现代城市发展的趋势,而其中的数字化转型更是推动未来社会治理体系和治理能力现代化的必然要求。在智慧城市建设中,智慧公厕作为一种新形态的信息化公共厕所,扮演着重要角色。本文智慧公厕源头实力厂家广州中期科技有限公司&a…...