前端Vue3+ts,后端SpringBoot,使用Socket.D - Websocket

在使用时,发现很少有相关博客,特此进行记录,不足之处望指出

1、 前端下载依赖包

pnpm install @noear/socket.d@2.5.16

Socket.D说明文档

新建socket-d.ts文件

使用了全局单例,防止多次创建链接,官方示例是直接挂载在Window,传递的token是用来给后端进行权限校验的,不需要的话可以删除,以下为全部代码

import { SocketD } from '@noear/socket.d'
import { localStg } from '@/utils/storage'
import { isEmpty } from '@/utils/is-empty'
import { Session } from '@noear/socket.d/transport/core/Session'
import { ClientSession } from '@noear/socket.d/transport/client/ClientSession'

type MessageCallback = (val: { type: SocketDType.ChannelMap; msg: string }) => void

class SocketClient {
  private readonly serverUrl: string = `sd:ws://${import.meta.env.VITE_FILE_SOCKETD_HOST}/?u=a&p=2`
  private readonly tokenName: string = import.meta.env.VITE_HERDER_TOKEN_NAME
  private readonly token: string = ''
  private static instance: SocketClient
  private client: ClientSession | null = null
  private listeners = new Map<string, MessageCallback[]>()

  private constructor() {
    if (isEmpty(localStg.get('token'))) {
      throw new Error('未登录!')
    }
    this.token = localStg.get('token') as string
  }

  /**
   * 获取单例实例
   */
  public static getInstance(): SocketClient {
    if (!SocketClient.instance) {
      SocketClient.instance = new SocketClient()
    }
    return SocketClient.instance
  }

  /**
   * 初始化 Socket
   */
  public async init(): Promise<void> {
    if (isEmpty(localStg.get('token'))) {
      throw new Error('未登录!')
    }

    if (this.client) {
      console.warn('Socket 客户端已初始化,无需重复初始化!')
      return
    }

    try {
      this.client = await SocketD.createClient(this.serverUrl)
        .config((c) => c.metaPut(this.tokenName, this.token))
        .listen(
          SocketD.newEventListener()
            .doOnOpen((s: Session) => {
              console.log('连接成功!')
            })
            .doOnMessage((s, m) => {
              this.emit('message', m.dataAsString())
            })
            .doOn('/alarm', (s, m) => {
              this.emit('alarm', m.dataAsString())
            })
            .doOn('/appstore', (s, m) => {
              this.emit('appstore', m.dataAsString())
            })
            .doOn('/notice', (s, m) => {
              this.emit('notice', m.dataAsString())
            })
        )
        .open()
      console.log('Socket 客户端初始化完成!')
    } catch (e) {
      this.emit('error', { type: '连接失败', msg: '连接失败,请稍后再试!' })
      throw e
    }
  }

  /**
   * 关闭链接
   * @param callback
   */
  public close(callback?: () => void): void {
    this.client?.close()
    this.client = null
    this.listeners.clear()
    console.log('Socket 客户端已关闭!')
    callback?.()
  }

  /**
   * 添加事件监听器
   * @param event 事件名称
   * @param callback 回调函数
   */
  public on(event: SocketDType.ChannelEvent, callback: MessageCallback): void {
    if (!this.listeners.has(event)) {
      this.listeners.set(event, [])
    }
    this.listeners.get(event)?.push(callback)
  }

  /**
   * 移除事件监听器
   * @param event 事件名称
   * @param callback 回调函数
   */
  public off(event: SocketDType.ChannelEvent, callback: MessageCallback): void {
    const callbacks = this.listeners.get(event)
    if (callbacks) {
      const index = callbacks.indexOf(callback)
      if (index !== -1) {
        callbacks.splice(index, 1)
      }
      if (callbacks.length === 0) {
        this.listeners.delete(event)
      }
    }
  }

  /**
   * 发送
   * @param channel
   * @param data
   * @private
   */
  public send(channel: SocketDType.ChannelUrl, data: any) {
    this.handleSendError(channel, data)
    this.client?.send(channel, SocketD.newEntity(data))
  }

  /**
   * 发送并请求
   * @param channel
   * @param data
   * @param callback
   * @private
   */
  public sendAndRequest(
    channel: SocketDType.ChannelUrl,
    data: any,
    callback?: (type: string, value: any) => void
  ) {
    this.handleSendError(channel, data)
    this.client?.sendAndRequest(channel, SocketD.newEntity(data)).thenReply((reply) => {
      callback?.('收到回复', reply)
    })
  }

  /**
   * 发送并订阅
   * @param channel
   * @param data
   * @param callback
   * @private
   */
  public sendAndSubscribe(
    channel: SocketDType.ChannelUrl,
    data: any,
    callback?: (type: string, value: any) => void
  ) {
    this.handleSendError(channel, data)
    this.client
      ?.sendAndRequest(
        channel,
        SocketD.newEntity(data).metaPut(SocketD.EntityMetas.META_RANGE_SIZE, '3')
      )
      .thenReply((reply) => {
        if (reply.isEnd()) {
          callback?.('订阅结束', reply)
        } else {
          callback?.('订阅回复', reply)
        }
      })
  }

  /**
   * 发送前校验
   * @param channel
   * @param data
   * @private
   */
  private handleSendError(channel: SocketDType.ChannelUrl, data: any) {
    if (!this.client) throw new Error('客户端尚未正确初始化!')

    if (isEmpty(channel)) throw new Error('信息通道不能为空!')

    if (isEmpty(data)) throw new Error('信息内容不能为空!')
  }

  /**
   * 触发事件
   * @param event 事件名称
   * @param data 事件数据
   * @private
   */
  private emit(event: SocketDType.ChannelEvent, data: any): void {
    const callbacks = this.listeners.get(event)
    if (callbacks) {
      callbacks.forEach((callback) => callback(data))
    }
  }
}

export const socketClient = SocketClient.getInstance()

2、页面使用

const handleMessage = (val: any) => {
  console.log('收到消息:', val)
}

const handleAlarm = (val: any) => {
  console.log('收到 alarm 消息:', val)
}

onMounted(async () => {
  await socketClient.init()
  // await socketClient.init((val{ hint: string; msg: string })=>{}) // 可以传递一个回调函数,用于接受所有的信息
  socketClient.on('message', handleMessage)
  socketClient.on('alarm', handleAlarm)
  socketClient.send('/alarm', '开始')

  setTimeout(() => {
    socketClient.off('alarm', handleAlarm)
    setTimeout(() => {
      socketClient.close()
      // socketClient.close(() => {}) // 可传递回调函数,用于处理关闭后的炒作
    }, 2000)
  }, 5000)
})

3、后台代码(官方示例)

public class WebSocketToSocketd extends ToSocketdWebSocketListener {
    public WebSocketToSocketd() {
        super(new ConfigDefault(false));

        setListener(buildListener());
    }

    /**
     * 构建监听器
     */
    private  Listener buildListener() {
        return new EventListener()
                .doOnOpen(s -> {
                    System.out.println("onOpen: " + s.sessionId());
                }).doOnMessage((s, m) -> {
                    System.out.println("onMessage: " + m);
                }).doOn("/demo", (s, m) -> {
                    if (m.isRequest()) {
                        s.reply(m, new StringEntity("me to!"));
                    }

                    if (m.isSubscribe()) {
                        int size = m.metaAsInt(EntityMetas.META_RANGE_SIZE);
                        for (int i = 1; i <= size; i++) {
                            s.reply(m, new StringEntity("me to-" + i));
                        }
                        s.replyEnd(m, new StringEntity("welcome to my home!"));
                    }
                }).doOn("/upload", (s, m) -> {
                    if (m.isRequest()) {
                        String fileName = m.meta(EntityMetas.META_DATA_DISPOSITION_FILENAME);
                        if (StrUtils.isEmpty(fileName)) {
                            s.reply(m, new StringEntity("no file! size: " + m.dataSize()));
                        } else {
                            s.reply(m, new StringEntity("file received: " + fileName + ", size: " + m.dataSize()));
                        }
                    }
                }).doOn("/download", (s, m) -> {
                    if (m.isRequest()) {
                        FileEntity fileEntity = new FileEntity(new File("/Users/noear/Movies/snack3-rce-poc.mov"));
                        s.reply(m, fileEntity);
                    }
                }).doOn("/push", (s, m) -> {
                    if (s.attrHas("push")) {
                        return;
                    }

                    s.attrPut("push", "1");

                    while (true) {
                        if (s.attrHas("push") == false) {
                            break;
                        }

                        s.send("/push", new StringEntity("push test"));
                        RunUtils.runAndTry(() -> Thread.sleep(200));
                    }
                }).doOn("/unpush", (s, m) -> {
                    s.attrMap().remove("push");
                })
                .doOnClose(s -> {
                    System.out.println("onClose: " + s.sessionId());
                }).doOnError((s, err) -> {
                    System.out.println("onError: " + s.sessionId());
                    err.printStackTrace();
                });
    }
}
### Vue3Spring Boot 使用 Socket.IO 进行实时通信的集成教程 #### 1. 技术背景 Vue3 是一种现代化前端框架,提供了强大的组件化开发能力;而 Spring Boot 则是一种用于快速构建 Java 后端服务的技术栈。两者结合可以通过 WebSocket 或者更高级别的封装库——Socket.IO 来实现高效的前后端实时通信[^1]。 --- #### 2. 添加依赖项 为了支持 Socket.IO 的功能,在后端需要引入 Netty-SocketIO 库作为服务器端的支持工具,而在前端则需安装 `socket.io-client` 包来处理客户端连接逻辑。 ##### **后端 Maven 依赖** 在项目的 `pom.xml` 文件中加入如下配置: ```xml <dependency> <groupId>com.corundumstudio.socketio</groupId> <artifactId>netty-socketio</artifactId> <version>1.7.19</version> </dependency> ``` 对于 Web 支持的基础包也可以一并添加(如果尚未存在的话)[^2]: ```xml <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> ``` --- #### 3. 配置后端 Socket.IO Server 创建一个新的类文件用来初始化和管理 Socket.IO 服务器实例: ```java import com.corundumstudio.socketio.*; import org.springframework.stereotype.Component; @Component public class SocketIOServer { private final SocketIOServer server; public SocketIOServer() { Configuration config = new Configuration(); config.setHostname("localhost"); config.setPort(8081); // 设置监听端口 this.server = new SocketIOServer(config); // 注册事件处理器 server.addConnectListener(socketIOClient -> System.out.println("New client connected")); server.addEventListener("chatMessage", String.class, (client, data, ackRequest) -> handleChatMessage(client, data)); } public void startServer() { server.start(); // 启动服务器 } public void stopServer() { server.stop(); // 停止服务器 } private void handleChatMessage(SocketIOClient client, String message) { System.out.println("Received Message: " + message); server.getBroadcastOperations().sendEvent("newMessage", message); // 广播消息给所有客户端 } } ``` 上述代码片段定义了一个简单的 Socket.IO 服务器,并设置了基本的消息接收与广播机制。 --- #### 4. 客户端设置 在 Vue3使用 Socket.IO Client 轻松建立与后台的服务交互关系。首先确保已经安装好必要的 npm 模块: ```bash npm install socket.io-client --save ``` 接着可以在任意 Vue 组件内部完成初始化操作: ```javascript <script setup lang="ts"> import { onMounted } from &#39;vue&#39;; import io from &#39;socket.io-client&#39;; let socket = null; onMounted(() => { const url = &#39;http://localhost:8081&#39;; // 对应于后端启动的地址 socket = io(url); // 订阅来自服务器的新消息通知 socket.on(&#39;newMessage&#39;, function(data){ console.log(`收到新消息:${data}`); }); // 发送测试数据至服务器 setTimeout(() => { socket.emit(&#39;chatMessage&#39;, &#39;Hello From Frontend!&#39;); }, 2000); }); </script> <template> <div> <h1>Sending and Receiving Messages via Socket.IO</h1> </div> </template> ``` 以上脚本实现了当页面加载完成后自动尝试向指定 URL 地址发起连接请求的功能^。 --- #### 5. 用户身份验证扩展 考虑到实际应用场景可能涉及敏感信息传输需求,则有必要增加额外的安全防护措施比如 Token-Based Authentication 方案[^3]。具体做法是在每次握手阶段附加有效的令牌供校验之用。 修改后的部分伪代码示意如下所示: ```java server.addBeforeHandshakeListener((handshakeData) -> { String token = handshakeData Singles.getParameterMap().getOrDefault("token", "").toString(); boolean isValid = validateUserToken(token); if (!isValid) throw new HandshakeException("Invalid user credentials."); }); private boolean validateUserToken(String token){ // 自定义解析逻辑... return true; } ``` 与此同时调整前端调用方式以传递参数: ```javascript const options = { query : {&#39;token&#39;: localStorage.getItem(&#39;auth_token&#39;)} }; socket = io.connect(url,options); ``` 这样便能有效保障整个系统的健壮性和可靠性。 --- #### 性能优化建议 尽管原生 WebSocket 已经具备较低延迟特性,但在复杂网络环境下仍推荐采用更高层次抽象层如 Socket.IO 提供断线重连等功能提升稳定性[^4]。 --- ###
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

开发小关

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

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

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

打赏作者

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

抵扣说明:

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

余额充值