注解式 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 {

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

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

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

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

【ARM 嵌入式 C 常用数据结构系列 25.1 -- linux 双向链表 list_head 使用详细介绍】

请阅读【嵌入式开发学习必备专栏 】 文章目录 内核双向链表双向链表的数据结构初始化双向链表在双向链表中添加元素遍历双向链表链表使用示例注意事项 内核双向链表 在Linux内核中&#xff0c;双向链表是一种广泛使用的数据结构&#xff0c;允许从任意节点高效地进行前向或后向…

STM32F407-SRAM

SRAM—> 内存 Flash–>硬盘 外置SRAM 可以存储1M数据 地址线&#xff1a;A0-A18&#xff1b;2^18次方&#xff1b;512K个数据块 每个数据块是2字节&#xff1b; 数据线&#xff1a;D0-D15 UB/LB 掩码&#xff1b;低电平有效 UB -》低电平-》数据高字节有效 LB-》低电平…

golang 选择排序

学习笔记&#xff5e; // Author sunwenbo // 2024/4/6 21:49 package mainimport "fmt"/* 选择排序基本介绍选择式排序也属于内部排序法&#xff0c;是从预排序的数据中按指定的规则选出某一元素&#xff0c;经过和其他元素重整&#xff0c;再依原则交换位置后达到…

轻量的 WebHook 工具:歪脖虎克

本篇文章聊聊轻量的网络钩子&#xff08;WebHook&#xff09;工具&#xff1a;歪脖虎克。 写在前面 这是一篇迟到很久的文章&#xff0c;在 21 年和 22 年的时候&#xff0c;我分享过两篇关于轻量的计划任务工具 Cronicle 的文章&#xff1a;《轻量的定时任务工具 Cronicle&a…

Linux(Ubuntu)中创建【samba】服务,用于和Windows系统之间共享文件

目录 1.先介绍一下什么是Samba 2.安装&#xff0c;配置服务 安装 配置&#xff08;smb.conf&#xff09; 配置用户 3.出现的问题&#xff08;Failed to add entry for user XXXX&#xff09; 4.创建文件夹 5.windows访问 1.先介绍一下什么是Samba Samba是一个开源的软…

2024.4.3-[作业记录]-day08-CSS 盒子模型(溢出显示、伪元素)

个人主页&#xff1a;学习前端的小z 个人专栏&#xff1a;HTML5和CSS3悦读 本专栏旨在分享记录每日学习的前端知识和学习笔记的归纳总结&#xff0c;欢迎大家在评论区交流讨论&#xff01; 文章目录 作业 2024.4.3-学习笔记css溢出显示单行文本溢出显示省略号多行文本溢出显示省…

【Android】图解View的工作流程原理

文章目录 入口DecorView如何加载到Window中MeasureSpec MeasureView的测量ViewGroup的测量 LayoutView的layout() Draw1、绘制背景3、绘制View内容4、绘制子View6、绘制装饰 入口 DecorView如何加载到Window中 MeasureSpec 该类是View的内部类&#xff0c;封装View的规格尺寸…

C++资源重复释放问题

这不是自己释放了2次&#xff1b; 可能是类互相引用&#xff0c;有类似现象释放资源时引起&#xff1b;还不太了解&#xff1b; 类对象作为函数参数也会引起&#xff1b; 下面是一个简单示例&#xff1b; #include <iostream> #include <string.h> #include &l…

Spark-Scala语言实战(14)

在之前的文章中&#xff0c;我们学习了如何在spark中使用键值对中的fullOuterJoin&#xff0c;zip&#xff0c;combineByKey三种方法。想了解的朋友可以查看这篇文章。同时&#xff0c;希望我的文章能帮助到你&#xff0c;如果觉得我的文章写的不错&#xff0c;请留下你宝贵的点…

最优算法100例之36-扑克牌顺子

专栏主页:计算机专业基础知识总结(适用于期末复习考研刷题求职面试)系列文章https://blog.csdn.net/seeker1994/category_12585732.html 题目描述 LL今天心情特别好,因为他去买了一副扑克牌,发现里面居然有2个大王,2个小王(一副牌原本是54张^_^)...他随机从中抽出了…

软考113-上午题-【计算机网络】-IPv6、无线网络、Windows命令

一、IPv6 IPv6 具有长达 128 位的地址空间&#xff0c;可以彻底解决 IPv4 地址不足的问题。由于 IPv4 地址是32 位二进制&#xff0c;所能表示的IP 地址个数为 2^32 4 294 967 29640 亿&#xff0c;因而在因特网上约有 40亿个P 地址。 由 32 位的IPv4 升级至 128 位的IPv6&am…

FaaF:利用事实作为评估RAG的函数方法

原文地址&#xff1a;faaf-facts-as-a-function-for-evaluating-rag 2024 年 4 月 5 日 在某些情况下&#xff0c;我们使用其他语言模型来验证RAG的输出结果&#xff0c;但这种方法并未能有效识别出数据生成过程中的错误和缺失。 论文解析 挑战 评估的可靠性和效率&#xff…

PyTorch之计算模型推理时间

一、参考资料 如何测试模型的推理速度 Pytorch 测试模型的推理速度 二、计算PyTorch模型推理时间 1. 计算CPU推理时间 import torch import torchvision import time import tqdm from torchsummary import summarydef calcCPUTime():model torchvision.models.resnet18()…

数据字典

文章目录 一、需求分析二、表设计&#xff08;两张表&#xff09;三、功能实现3.1 数据字典功能3.1.1 列表功能3.1.2 新增数据字典3.1.3 编辑数据字典 3.2 数据字典明细3.2.1 列表功能3.2.2 新增字典明细3.2.3 编辑字典明细 3.3 客户管理功能3.3.1 列表功能3.3.2 新增用户3.3.3…

页表基本原理

页表概念 CPU并不是直接访问物理内存地址&#xff0c;而是通过虚拟地址空间来间接访问物理内存地址&#xff1b;虚拟地址空间是操作系统为每个正在执行的进程分配一个逻辑地址&#xff1b;比如在32位系统(处理器和内存地址总线都是32位)&#xff0c;范围是0~(4G-1)&#xff1b…

docker基础学习指令

文章目录 [toc] docker基础常用指令一、docker 基础命令二、docker 镜像命令1. docker images2. docker search3. docker pull4. docker system df5. docker rmi1. Commit 命令 三、 docker 容器命令1. docker run2. docker logs3. docker top4. docker inspect5. docker cp6. …

CSS-语法、选择器

&#x1f4da;详见 W3scholl&#xff0c;本篇只做快速思维索引。 概述 CSS 是一种描述 HTML 文档样式的语言。 有三种插入样式表的方法&#xff1a; 外部 CSS内部 CSS行内 CSS &#x1f4c5; 外部 CSS 外部样式表存储在.css文件中。HTML 页面必须在 head 部分的<link&g…

Apache Log4j2 Jndi RCE CVE-2021-44228漏洞原理讲解

Apache Log4j2 Jndi RCE CVE-2021-44228漏洞原理讲解 一、什么是Log4j2二、环境搭建三、简单使用Log4j2四、JDNI和RMI4.1、启动一个RMI服务端4.2、启动一个RMI客户端4.3、ldap 五、漏洞复现六、Python批量检测 参考视频&#xff1a;https://www.bilibili.com/video/BV1mZ4y1D7K…

深入浅出 -- 系统架构之Keepalived搭建双机热备

Keepalived重启脚本双机热备搭建 ①首先创建一个对应的目录并下载keepalived安装包&#xff08;提取码:s6aq&#xff09;到Linux中并解压&#xff1a; [rootlocalhost]# mkdir /soft/keepalived && cd /soft/keepalived [rootlocalhost]# wget https://www.keepalived.…