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

  • 4
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
### 回答1: Netty-WebSocket-Spring-Boot-Starter是一个用于将Websocket集成到Spring Boot应用程序中的库。它使用Netty作为底层框架,提供了一种快速和可靠的方来处理异步通信。 这个库提供了一种简单的方法来创建Websocket端点,只需要使用注释和POJO类即可。在这些端点上可以添加动态的事件处理程序,以处理连接、断开连接和消息事件等。 此外,Netty-WebSocket-Spring-Boot-Starter还包括了一些安全性的特性,如基于令牌的授权和XSS保护,可以帮助您保持您的Websocket应用程序安全。 总的来说,Netty-WebSocket-Spring-Boot-Starter提供了一种快速和易于使用的方构建Websocket应用程序,使得它成为应用程序开发人员的有用工具。 ### 回答2: netty-websocket-spring-boot-starter 是一个开源的 Java Web 开发工具包,主要基于 Netty 框架实现WebSocket 协议的支持,同时集成了 Spring Boot 框架,使得开发者可以更加方便地搭建 WebSocket 服务器。 该工具包提供了 WebSocketServer 配置类,通过在 Spring Boot 的启动配置类中调用 WebSocketServer 配置类,即可启动 WebSocket 服务器。同时,该工具包还提供了多种配置参数,如端口号、URI 路径、SSL 配置、认证配置等等,可以根据业务需求进行自定义配置。 此外,该工具包还提供了一些可扩展的接口和抽象类,如 WebSocketHandler、ChannelHandlerAdapter 等,可以通过继承和实现这些接口和抽象类来实现业务逻辑的处理和拓展。 总的来说,netty-websocket-spring-boot-starter 提供了一个高效、简单、易用的 WebSocket 服务器开发框架,可以减少开发者的开发成本和工作量。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

陈亦康

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值