SpringBoot+Vue 中 WebSocket 的使用

        WebSocket 是一种在单个 TCP 连接上进行全双工通信的协议,它使得客户端和服务器之间可以进行实时数据传输,打破了传统 HTTP 协议请求 - 响应模式的限制。

        下面我会展示在 SpringBoot + Vue 中,使用WebSocket进行前后端通信。

后端

1、引入 jar 包

<dependency><!-- 引入 websocket 库,该库提供了对 WebSocket 协议的支持-->
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-websocket</artifactId>
    <version>2.7.14</version>
</dependency>

2、WebSocket 配置类

package com.zecApi.config;

import com.zecApi.config.zecInstantMessaging.ZecInstantMessagingWebSocketHandler;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
import org.springframework.web.socket.server.standard.ServletServerContainerFactoryBean;

/**
 * WebSocketConfig 类是一个配置类,用于配置 Spring 框架的 WebSocket 功能。
 * 该类实现了 WebSocketConfigurer 接口,并重写了 registerWebSocketHandlers 方法,
 * 用于注册 WebSocket 处理器和配置相关的连接信息。
 */
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {

    static {
        System.out.println("----------------------------------");
        System.out.println("------   WebSocket服务启动   -------");
        System.out.println("----------------------------------");
    }

    /**
     * 配置 WebSocket 容器的参数
     * @return ServletServerContainerFactoryBean 实例
     */
    @Bean
    public ServletServerContainerFactoryBean createWebSocketContainer() {
        ServletServerContainerFactoryBean container = new ServletServerContainerFactoryBean();
        // 设置最大文本消息缓冲区大小为 8192 字节
        container.setMaxTextMessageBufferSize(8192);
        // 设置最大二进制消息缓冲区大小为 8192 字节
        container.setMaxBinaryMessageBufferSize(8192);
        return container;
    }

    /**
     * 重写 WebSocketConfigurer 接口的 registerWebSocketHandlers 方法,
     * 该方法用于注册 WebSocket 处理器并配置连接的相关信息。
     *
     * @param registry 用于注册 WebSocket 处理器的注册表对象
     */
    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        // 注册WebSocket处理器,并设置允许的来源
        registry.addHandler(zecInstantMessagingWebSocketHandler(), "/ZecInstantMessaging/ZecInstantMessagingWebSocketHandler/{account}")
                .setAllowedOrigins("*"); // 允许所有来源,生产环境建议指定具体的域
    }

    /**
     * 定义一个名为 ZecInstantMessagingWebSocketHandler 的 Bean,该 Bean 是一个自定义的 WebSocket 处理器。
     * Spring 会将该 Bean 注入到应用程序中,以便在 WebSocket 连接时使用。
     *
     * @return 返回一个 ZecInstantMessagingWebSocketHandler 实例
     */
    @Bean
    public ZecInstantMessagingWebSocketHandler zecInstantMessagingWebSocketHandler() {
        return new ZecInstantMessagingWebSocketHandler();
    }

}

3、WebSocket 处理器类 

package com.zecApi.config.zecInstantMessaging;

import com.zecApi.zecInstantMessaging.controller.MessageListController;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;

import javax.annotation.Resource;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;

/**
 * 自定义的 WebSocket 处理器类,继承自 TextWebSocketHandler,用于处理 WebSocket 连接、消息收发和连接关闭等操作。
 */
//@Component
@ServerEndpoint("/ZecInstantMessaging/ZecInstantMessagingWebSocketHandler/{account}")
public class ZecInstantMessagingWebSocketHandler extends TextWebSocketHandler {

    /**
     * 用于存储账号和 WebSocketSession 映射关系的并发哈希表。
     * 键为账号,值为对应的 WebSocketSession 对象,方便根据账号查找对应的会话。
     * 采用 ConcurrentHashMap 保证在多线程环境下的线程安全。
     */
    private static final ConcurrentHashMap<String, WebSocketSession> sessionPool = new ConcurrentHashMap<>();

    /**
     * 用于存储所有 WebSocketSession 的并发列表。
     * 该列表用于存储所有当前活跃的 WebSocket 会话,方便进行广播等操作。
     * 采用 CopyOnWriteArrayList 保证在多线程环境下的线程安全。
     */
    private static final CopyOnWriteArrayList<WebSocketSession> sessions = new CopyOnWriteArrayList<>();

    @Resource
    MessageListController messageListController;

    /**
     * 当与客户端的 WebSocket 连接建立成功后,此方法会被调用。
     *
     * @param session 代表与客户端建立的 WebSocket 会话对象。
     */
    @Override
    public void afterConnectionEstablished(WebSocketSession session) {
        try {
            // 从 URI 中获取 account 参数
            String account = extractAccountFromSession(session);
            // 检查是否成功获取到账号信息
            if (account == null) {
                // 若未获取到账号信息,打印错误信息并关闭连接
                System.out.println("客户端连接失败,未获取到账号信息");
                session.close();
                return;
            }
            // 打印客户端连接成功信息,包含账号信息
            System.out.println("账号 " + account + " 已上线");
            // 将账号和对应的 WebSocketSession 存入 sessionPool 中
            sessionPool.put(account, session);
            // 将该 WebSocketSession 存入 sessions 列表中
            sessions.add(session);
            // 遍历 sessionPool 中的所有账号,打印在线账号信息
            for (String key : sessionPool.keySet()) {
                System.out.println("在线账号: " + key);
            }
            // 打印当前在线人数
            System.out.println("在线人数:" + sessionPool.size());
        } catch (IOException e) {
            // 处理关闭连接时可能出现的异常
            System.out.println("处理连接建立时出现 I/O 异常: " + e.getMessage());
        }
    }

    /**
     * 前端发送信息来的时候,用此方法进行接收并处理
     * @param session
     * @param message
     */
    @Override
    protected void handleTextMessage(WebSocketSession session, TextMessage message) {
        try {
            // 从接收到的 TextMessage 对象中提取消息的具体内容
            String messages = message.getPayload();
            //把发来的信息按照 / 分成数组
            String[] splitMsg = messages.split("/");
            switch (splitMsg[0]){
                case  "chat":
                    System.out.println("聊天");
                    // 获取我的账号
                    String MyAccount = splitMsg[1];
                    // 获取好友的账号
                    String friendAccount = splitMsg[2];
                    // 获取发送的信息
                    String msg = splitMsg[3];
                    // 对发来的信息进行操作(我这里是调用接口把信息存到数据库)
                    int i = messageListController.insMessage(MyAccount,friendAccount,msg);
                    if (i>0){
                        // 获取对方的 session
                        WebSocketSession friendSession = sessionPool.get(friendAccount);
                        // 把信息发送给前端
                        session.sendMessage(new TextMessage("对方已收到信息!"));
                        // 把信息发给好友
                        try {
                            friendSession.sendMessage(new TextMessage(messages));
                        }catch (NullPointerException e){
                            System.out.println("对方不在线");
                        }
                    }else {
                        session.sendMessage(new TextMessage("消息发送失败,请稍后重试!"));
                    }
                    break;
                default:
                    System.out.println("默认");
            }

        } catch (IOException e) {
            // 若在处理消息或发送确认消息过程中出现 I/O 异常,捕获该异常
            // 并打印错误信息,包含异常的具体描述,便于后续排查问题
            System.out.println("处理消息时出现异常: " + e.getMessage());
        }
    }

    /**
     * 当与客户端的 WebSocket 连接关闭时,此方法会被调用。
     *
     * @param session 代表与客户端建立的 WebSocket 会话对象。
     * @param status 表示连接关闭的状态信息。
     */
    @Override
    public void afterConnectionClosed(WebSocketSession session, org.springframework.web.socket.CloseStatus status) {
        // 从 WebSocketSession 的 URI 中提取账号信息
        String account = extractAccountFromSession(session);
        // 检查是否成功获取到账号信息
        if (account != null) {
            // 从 sessionPool 中移除该账号对应的 WebSocketSession
            sessionPool.remove(account);
            // 从 sessions 列表中移除该 WebSocketSession
            sessions.remove(session);
            // 打印客户端断开连接信息,包含账号信息和当前在线人数
            System.out.println("账号 " + account + " 已下线" );
            // 打印当前在线人数
            System.out.println("在线人数:" + sessionPool.size());
        }
    }

    /**
     * 从 WebSocketSession 的 URI 中提取账号信息。
     *
     * @param session 代表与客户端建立的 WebSocket 会话对象。
     * @return 提取到的账号信息,如果未找到则返回 null。
     */
    private String extractAccountFromSession(WebSocketSession session) {
        if (session.getUri() == null) {
            return null;
        }
        // 获取 WebSocketSession 的 URI 并转换为字符串
        String uri = session.getUri().toString();
        // 查找 URI 中最后一个斜杠的位置
        int index = uri.lastIndexOf("/");
        // 检查是否找到斜杠且斜杠后面还有字符
        if (index != -1 && index < uri.length() - 1) {
            // 提取斜杠后面的部分作为账号信息
            return uri.substring(index + 1);
        }
        // 若未找到合适的账号信息,返回 null
        return null;
    }
}

前端

1、WebSocket 工具JS

        这个是用来连接 webSocket 的。

// 引入了一个获取 SessionStorage 里 account 的方法
import {getAccountBySessionStorage} from "@/common/StorageUtils";

// 定义 WebSocket 实例变量,用于存储当前的 WebSocket 连接
let webSocket;

/**
 * WebSocket还有一个readyState属性,可以用来获取当前连接的状态。
 * readyState有四个可能的值:0(连接尚未建立)、1(连接已建立,可以通信)、2(连接正在关闭)、3(连接已关闭)
 */

/**
 * 获取 websocket 实例
 * @returns {*}
 */
export async function getWebSocket() {
    if (webSocket === undefined){
        await connectWebSocket(getAccountBySessionStorage());
    }
    return webSocket;
}

/**
 * 封装一个连接 webSocket 的操作
 */
// 连接 WebSocket 的函数,返回一个 Promise
export function connectWebSocket(account) {
    return new Promise((resolve, reject) => {
        if (account!== null && account!== undefined) {
            // 检查是否已经存在有效的 WebSocket 连接
            if (webSocket && webSocket.readyState === WebSocket.OPEN) {
                resolve(webSocket);
                return;
            }
            // 如果存在连接但已经关闭,先断开
            if (webSocket && webSocket.readyState!== WebSocket.CLOSED) {
                webSocket.close();
            }
            // 进行 websocket 连接
            webSocket = new WebSocket(`ws://localhost:8088/ZecInstantMessaging/ZecInstantMessagingWebSocketHandler/${account}`);
            webSocket.onopen = function () {
                resolve(webSocket);
            };
            webSocket.onerror = function (error) {
                reject(error);
            };
            webSocket.onclose = function () {
                // 可以在这里添加连接关闭后的处理逻辑
            };
        } else {
            // 当 account 为 undefined 时,拒绝该 Promise 并返回错误信息
            reject(new Error('当前登录账号无效!'));
        }
    });
}

/**
 * 主动断开 WebSocket 连接
 */
export function disconnectWebSocket() {
    if (webSocket && webSocket.readyState !== WebSocket.CLOSED) {
        webSocket.close();
        webSocket = null;
    }
}

2、VUE 页面

        我这里写的是发送方的 websocket ,接收方和这个也是一样用 webSocket.onmessage 再写一个方法,专门用来接收用的,可以放在 onMounted 里,后端是根据 session 发送的,虽然是同一个页面,但是接收的客户端不一样。

<template>
  <div style="width: 100%;height: 120px;">
    <div>
      <textarea v-model="message" style="height: 50px;resize: none; width: 97%;font-family: 'Arial'; font-size: 14px;padding: 5px"></textarea>
    </div>
    <div>
      <el-button style="margin-top: 5px;position: fixed; right: 12px;" @click="sendMessage">发送</el-button>
    </div>
  </div>
</template>

<script>
import {ref} from "vue";
import {getWebSocket} from "@/common/webSocketUtil";

export default {
  name: "DataScreen",
  setup(){
    let message = ref("");
    // 聊天页面记录的好友的账号(我这里单独写一个页面就直接写死账号了,主要用来演示 websocket,你们用的话可以替换成自己的)
    let friendAccount = ref("739560950");
    // 我的账号
    let myAccount = ref("931539684");
    // 发送消息
    // 发送消息
    const sendMessage = async () => {
      const msg = "chat" + "/" + myAccount.value + "/" + friendAccount.value + "/" + message.value + "/注释:标识、我的账号、好友的账号、信息";
      // 开始发送
      try {
        const webSocket = await getWebSocket();
        webSocket.send(msg);
        webSocket.onmessage = async function (event) {
          if (event.data === "对方已收到信息!") {
            // 对方收到信息后的操作
            console.log("对方已收到信息!")
            message.value = ''; // 清空输入框
          } else {
            // 对方没有收到信息后的操作
            console.log("对方没有收到信息!")
            // throw new Error(response);

          }
        }
      } catch (error) {
        console.error('信息发送失败:', error);
      }
    };

    return{
      sendMessage,
      message
    }
  }
}
</script>

<style scoped>

</style>

测试

后端打印

         这是后端接收到前端的信息。

 前端打印

        这是发送信息到后端,并接收后端返回来的数据。

        这是我单独拎出来写的,如果有问题可以联系我进行调整。

        如果还不清楚具体怎么用的,我后面会把那个 “零式生态链” 加上即时通讯的功能然后发出来,也可以参考一下。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值