注解式 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:想要源码可以联系我......

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

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

相关文章

掌握网络抓取技术:利用RobotRules库的Perl下载器一览小红书的世界

引言 在信息时代的浪潮下&#xff0c;人们对于获取和分析海量网络数据的需求与日俱增。网络抓取技术作为满足这一需求的关键工具&#xff0c;正在成为越来越多开发者的首选。而Perl语言&#xff0c;以其卓越的文本处理能力和灵活的特性&#xff0c;脱颖而出&#xff0c;成为了…

Android 属性动画及自定义3D旋转动画

Android 动画框架 其中包括&#xff0c;帧动画、视图动画&#xff08;补间动画&#xff09;、属性动画。 在Android3.0之前&#xff0c;视图动画一家独大&#xff0c;之后属性动画框架被推出。属性动画框架&#xff0c;基本可以实现所有的视图动画效果。 视图动画的效率较高…

【第七篇】使用BurpSuite进行主动、被动扫描和主动、被动爬虫

文章目录 前言主动扫描被动扫描主动爬虫被动爬虫前言 Burp Scanner 既可以用作全自动扫描仪,也可以用作增强手动测试工作流程的强大手段。 扫描网站涉及两个阶段: 抓取内容和功能: Burp Scanner 首先在目标站点周围导航,密切反映真实用户的行为。它对站点的结构和内容以及…

数字社会下的智慧公厕:构筑智慧城市的重要组成部分

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

【攻防世界】web2(逆向解密)

进入题目环境&#xff0c;查看页面信息&#xff1a; <?php $miwen"a1zLbgQsCESEIqRLwuQAyMwLyq2L5VwBxqGA3RQAyumZ0tmMvSGM2ZwB4tws";function encode($str){$_ostrrev($str);// echo $_o;for($_00;$_0<strlen($_o);$_0){$_csubstr($_o,$_0,1);$__ord($_c)1;…

龙蜥社区「人人都可以参与开源」——体验开源成为“开源人“

龙蜥社区「人人都可以参与开源」体验开源——让更多的人了解开源&#xff01; 龙蜥社区开源概述&#xff1a;龙蜥社区开源的探索过程:龙蜥社区收获总结:AtomGit评测:服务设计上:功能结构上:安全设计上: AtomGit测评总结: 龙蜥社区开源概述&#xff1a; 在追求技术的路上少不了…

铸造大型基础平板的结构应该怎样设计

设计大型基础平板的结构时&#xff0c;需要考虑以下几个方面&#xff1a; 地质条件&#xff1a;首先要了解工程所在地的地质条件&#xff0c;包括土质、地下水位、地震状况等。根据地质条件来选择合适的基础类型&#xff0c;如浅基、深基或地下连续墙等。 荷载分析&#xff1a…

Proxmox VE qm 方式一键创建Windows虚拟机

前言 实现qm 方式一键创建Windows虚拟机&#xff0c;提高效率。 qm 一键创建Windows虚拟机 以下实现在线下载镜像&#xff0c;创建虚拟机&#xff0c;安装系统需要自己手动安装哦&#xff0c;如果想实现全自动安装系统&#xff0c;建议部署自己的内网pxe server 系统参考各参…

【C语言】整数和浮点数在内存中的存储

点这里是个人主页~ 这次的内容是比较底层的奥&#xff0c;对于理解编程很重要~ 整数浮点数在内存中的存储 一、 整数在内存中的存储二、大小端字节序和字节序判断大小端的概念一道简单关于大小端排序的百度面试题 三、简单理解数据类型存储范围例一例二例三例四例五例六 四、 …

STM32F4 IAP跳转APP问题及STM32基于Ymodem协议IAP升级笔记

STM32F4 IAP 跳转 APP问题 ST官网IAP例程Chapter1 STM32F4 IAP 跳转 APP问题1. 概念2. 程序2.1 Bootloader 程序 问题现象2.2. APP程序 3. 代码4. 其他问题 Chapter2 STM32-IAP基本原理及应用 | ICP、IAP程序下载流程 | 程序执行流程 | 配置IAP到STM32F4xxxChapter3 STM32基于Y…

未来工厂大脑:图扑组态软件在智能制造中的应用

组态软件&#xff1a;一般英文简称有三种分别为 HMI/MMI/SCADA&#xff0c;中文翻译为&#xff1a;人机界面/监视控制和数据采集软件。 运行于 PC 平台的一个通用工具软件&#xff0c;涉及各行各业&#xff0c;其主要功能是对生产现场的运行设备进行监控并就危险情况进行报警&…

【学习】使用VScode连接服务器。

step1: 安装 Remote - ssh 扩展 step2&#xff1a; 进入步骤2中&#xff0c;进行文件配置。 step3&#xff1a; 点击箭头进行连接。 step4&#xff1a; 输入密码即可。选择 platform时候&#xff0c;选择使用 Linux&#xff0c;而不是windows。

FreeRTOS创建第一个程序

使用freeRTOS创建任务时使用如下函数 函数的参数 创建一个FreeRTOS任务点亮led灯实现led灯500毫秒翻转一次 具体的代码实现 #include "stm32f10x.h" // Device header #include "Delay.h" #include "freeRTOS.h" #include &quo…

PMP持证者在面试项目经理时有加持吗?

对PMP认证获取后是否在面试中加持很多人是没有体验过的&#xff0c;因为大部分人考取PMP认证的原因是因为公司的要求&#xff0c;没有这个证书可能面临被“优化”的风险。理论上来说一样的道理&#xff0c;PMP认证既然能够保住工作岗位&#xff0c;那么在面试中一定会有相应的作…

利用AI开源引擎平台:构建文本、图片及视频内容审核系统|可本地部署

网络空间的信息量呈现出爆炸式增长。在这个信息多元化的时代&#xff0c;内容审核系统成为了维护网络秩序、保护用户免受有害信息侵害的重要工具。本文将探讨内容审核系统的核心优势、技术实现以及在不同场景下的应用。 开源项目介绍(可本地部署&#xff0c;支持国产化) 思通数…

12.C++常用的算法_遍历算法

文章目录 遍历算法1. for_each()代码工程运行结果 2. transform()代码工程运行结果 3. find()代码工程运行结果 遍历算法 1. for_each() 有两种方式&#xff1a; 1.普通函数 2.仿函数 代码工程 #define _CRT_SECURE_NO_WARNINGS #include<iostream> #include<vect…

layui复选框勾选取消勾选事件监听

监听事件放置位置&#xff1a; form.on(checkbox(equipInputClick), function(data){var a data.elem.checked;var val data.value;if(a true){}else{}});html部分 <input lay-filter"equipInputClick" type"checkbox" lay-skin"primary&quo…

前端做了快两年了,盘点一下我的前端技术栈

前言 前言很简单&#xff0c;年终了&#xff0c;自己写了快两年前端&#xff08;虽然刚毕业&#xff09;&#xff0c;但是大致总结一下哈哈哈哈 我觉得这个话题蛮有意思的&#xff0c;可以看看大家的技术广度&#xff0c;可以进行分享和学习以及讨论 所以这里说一下我对我的…

# ABAP SQL 字符串处理

经常我都要在ABAP的sql语句中对字符串进行处理&#xff0c;现在就总结一下可以用到的方法 文章目录 字符串处理拼接字段运行结果 填充字符串运行结果 截取字符串 SUBSTRING运行结果 CAST转换类型程序运行结果 字符串处理 在SQL语句中&#xff0c;有时候会有需要拼接字段或者是…

车载平板电脑丨车载终端丨浅聊叉车仓储出入库管理解决方案

众所周知&#xff0c;叉车是制造业自动化升级的核心设备。 十三五期间&#xff0c;提出中国制造 2025 战略&#xff0c;其核心内容就是要用机器代替人工&#xff0c;提升生产效率&#xff0c;降低对劳动力的依赖性。而叉车作为起重、搬运等普通劳动力替代的核心设备&#xff0…