Springboot WebSocket + Vue3 整合一篇通超详细图文

1.前言

Springboot+vue3整合WebSocket(粗略简单能用)

2.后端代码

2.1.Maven

        <!--WebSocket-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-websocket</artifactId>
        </dependency>

2.2.WebSocket配置类

2.2.1.WebSocket

package com.cn.websocket;

import jakarta.websocket.Session;

public class WebSocket {
    
    
    /**
     * session
     */
    private Session session;
    /**
     * 账号id
     */
    private String userId;

    public Session getSession() {
    
    
        return session;
    }

    public void setSession(Session session) {
    
    
        this.session = session;
    }

    public String getUserId() {
    
    
        return userId;
    }

    public void setUserId(String userId) {
    
    
        this.userId = userId;
    }
}

2.2.2.WebSocketConfig

package com.cn.websocket;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Service;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;

@Configuration
@EnableWebSocket
@Service
public class WebSocketConfig {
    
    
    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
    
    
        return new ServerEndpointExporter();
    }
}

2.2.3.WebSocketUtil

package com.cn.websocket;

import cn.hutool.core.date.DateUtil;
import com.alibaba.fastjson.JSON;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component;

import jakarta.websocket.*;
import jakarta.websocket.server.PathParam;
import jakarta.websocket.server.ServerEndpoint;

import java.io.IOException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * @program: tools
 * @Description: 通过这个类进行连接WebSocket的,默认发信息就进入onMessage解析
 */
@Component
@ServerEndpoint(value = "/test/webSocket/{userId}")
@Slf4j
public class WebSocketUtil {
    
    
	/**
	 * 登录连接数 应该也是线程安全的
	 */
	private static int loginCount = 0;
	/**
	 * user 线程安全的
	 */
	private static final Map<String, WebSocket> userMap = new ConcurrentHashMap<String, WebSocket>();

	/**
	 * @Description: 收到消息触发事件,这个消息是连接人发送的消息
	 * @Param [messageInfo, session]
	 * @Return: void
	 * {
	 * "userId": "test2",
	 * "message": "你收到了嘛?这是用户test发的消息!"
	 * }
	 **/
	@OnMessage
	public void onMessage(String messageInfo, Session session) throws IOException, InterruptedException {
    
    
		if (StringUtils.isBlank(messageInfo)) {
    
    
			return;
		}
		// 当前用户
		String userIdTo = session.getPathParameters().get("userId");
		// JSON数据
		log.info("onMessage:{}", messageInfo);
		Map map = JSON.parseObject(messageInfo, Map.class);
		// 接收人
		String userId = (String) map.get("userId");
		// 消息内容
		String message = (String) map.get("message");
		// 发送给指定用户
		sendMessageTo(message, userId);
		log.info(DateUtil.now() + " | " + userIdTo + " 私人消息-> " + message, userId);
	}

	/**
	 * @Description: 打开连接触发事件
	 * @Param [account, session, config]
	 * @Return: void
	 **/
	@OnOpen
	public void onOpen(@PathParam("userId") String userId, Session session, EndpointConfig config) {
    
    
		WebSocket webSocket = new WebSocket();
		webSocket.setUserId(userId);
		webSocket.setSession(session);
		boolean containsKey = userMap.containsKey(userId);
		if (!containsKey) {
    
    
			// 添加登录用户数量
			addLoginCount();
			userMap.put(userId, webSocket);
		}
		log.info("打开连接触发事件!已连接用户: " + userId);
		log.info("当前在线人数: " + loginCount);

	}

	/**
	 * @Description: 关闭连接触发事件
	 * @Param [session, closeReason]
	 * @Return: void
	 **/
	@OnClose
	public void onClose(@PathParam("userId") String userId, Session session, CloseReason closeReason) {
    
    
		boolean containsKey = userMap.containsKey(userId);
		if (containsKey) {
    
    
			// 删除map中用户
			userMap.remove(userId);
			// 减少断开连接的用户
			reduceLoginCount();
		}
		log.info("关闭连接触发事件!已断开用户: " + userId);
		log.info("当前在线人数: " + loginCount);

	}

	/**
	 * @Description: 传输消息错误触发事件
	 * @Param [error :错误]
	 * @Return: void
	 **/
	@OnError
	public void onError(Throwable error) {
    
    
		log.info("onError:{}", error.getMessage());
	}

	/**
	 * @Description: 发送指定用户信息
	 * @Param [message:信息, userId:用户]
	 * @Return: void
	 **/
	public void sendMessageTo(String message, String userId) throws IOException {
    
    
		for (WebSocket user : userMap.values()) {
    
    
			if (user.getUserId().equals(userId)) {
    
    
				user.getSession().getAsyncRemote().sendText(message);
			}
		}
	}

	/**
	 * @Description: 发给所有人
	 * @Param [message:信息]
	 * @Return: void
	 **/
	public void sendMessageAll(String message) throws IOException {
    
    
		for (WebSocket item : userMap.values()) {
    
    
			item.getSession().getAsyncRemote().sendText(message);
		}
	}

	/**
	 * @Description: 连接登录数增加
	 * @Param []
	 * @Return: void
	 **/
	public static synchronized void addLoginCount() {
    
    
		loginCount++;
	}

	/**
	 * @Description: 连接登录数减少
	 * @Param []
	 * @Return: void
	 **/
	public static synchronized void reduceLoginCount() {
    
    
		loginCount--;
	}

	/**
	 * @Description: 获取用户
	 * @Param []
	 * @Return: java.util.Map<java.lang.String, com.cn.webSocket.WebSocket>
	 **/
	public synchronized Map<String, WebSocket> getUsers() {
    
    
		return userMap;
	}

}

3.简易接口

package com.cn.controller.test.websocket;

import cn.hutool.core.date.DateUtil;
import com.cn.common.AjaxResult;
import com.cn.websocket.WebSocket;
import com.cn.websocket.WebSocketUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.io.IOException;
import java.util.Map;
import java.util.Set;

/**
 * @program: tools
 */
@Slf4j
@RestController
@RequestMapping("/test/webSocket")
public class WebSocketController {
    
    
    @Autowired
    private WebSocketUtil webSocketUtil;

    /**
     * 获取在线用户信息
     *
     * @return ok
     */
    @GetMapping(value = "/getUser")
    public AjaxResult getUser() {
    
    
        Map<String, WebSocket> users = webSocketUtil.getUsers();
        Set<String> ids = users.keySet();
        return AjaxResult.success(ids);
    }

    /**
     * 发送全体消息
     *
     * @param message 消息内容
     * @return ok
     */
    @GetMapping(value = "/sendMessageAll")
    public AjaxResult sendMessageAll(@RequestParam String message) {
    
    
        try {
    
    
            webSocketUtil.sendMessageAll(message);
            log.info(DateUtil.now() + " | " + "admin" + " 全体消息-> " + message);
        } catch (IOException e) {
    
    
            e.printStackTrace();
            log.info("消息推送失败!");
        }
        return AjaxResult.success("消息推送成功:" + message);
    }

    /**
     * 发送消息给某人
     *
     * @return ok
     */
    @GetMapping(value = "/sendMessageTo")
    public AjaxResult sendMessageTo(@RequestParam String message, @RequestParam String userId) {
    
    
        try {
    
    
            webSocketUtil.sendMessageTo(message, userId);
            log.info(DateUtil.now() + " | " + "admin" + " 私人消息-> " + message, userId);
        } catch (IOException e) {
    
    
            e.printStackTrace();
            log.info("消息推送失败!");
        }
        return AjaxResult.success();
    }
}

3.前端代码

结构:
在这里插入图片描述

3.1.WebSocket.js(统一配置)

// 信息提示
import {
    
     ElMessage } from 'element-plus'
import {
    
     useUserStore } from '@/stores'

// WebSocket地址
const url = 'ws://127.0.0.1:8001/test/webSocket/'

// WebSocket实例
let ws

// 重连定时器实例
let reconnectTimer

// WebSocket重连开关
let isReconnecting = false

// WebSocket对象
const websocket = {
    
    
  // WebSocket建立连接
  Init(username) {
    
    
    // 判断浏览器是否支持WebSocket
    if (!('WebSocket' in window)) {
    
    
      ElMessage.error('您的浏览器不支持 WebSocket')
      return
    }

    // 创建WebSocket实例
    ws = new WebSocket(url + username)

    // 监听WebSocket连接
    ws.onopen = () => {
    
    
      // ElMessage.success('WebSocket连接成功')
    }

    // 监听WebSocket连接错误信息
    ws.onerror = (e) => {
    
    
      console.log('WebSocket重连开关', isReconnecting)
      console.log('WebSocket数据传输发生错误', e)
      // ElMessage.error('WebSocket传输发生错误')
      // 打开重连
      reconnect()
    }

    // 监听WebSocket接收消息
    ws.onmessage = (e) => {
    
    
      console.log('WebSocket接收后端消息:' + e.data)
      // 心跳消息不做处理
      if (e.data === 'ok') {
    
    
        return
      }

      // 调用回调函数处理接收到的消息
      if (websocket.onMessageCallback) {
    
    
        websocket.onMessageCallback(e.data)
      }

      ElMessage.success(e.data)
    }
  },

  // WebSocket连接关闭方法
  Close() {
    
    
    // 关闭断开重连机制
    isReconnecting = true
    ws.close()
    // ElMessage.error('WebSocket断开连接')
  },

  // WebSocket发送信息方法
  Send(data) {
    
    
    // 处理发送数据JSON字符串
    const msg = JSON.stringify(data)
    // 发送消息给后端
    ws.send(msg)
  },

  // 暴露WebSocket实例,其他地方调用就调用这个
  getWebSocket() {
    
    
    return ws
  },

  // 新增回调函数用于处理收到的消息
  onMessageCallback: null,

  // 设置消息处理回调函数
  setMessageCallback(callback) {
    
    
    this.onMessageCallback = callback
  }
}

// 监听窗口关闭事件,当窗口关闭时-每一个页面关闭都会触发-扩张需求业务
window.onbeforeunload = function () {
    
    
  // 在窗口关闭时关闭 WebSocket 连接
  websocket.Close()
  console.log('WebSocket窗口关闭事件触发')
}

// 浏览器刷新重新连接
// 刷新页面后需要重连-并且是在登录之后
if (performance.getEntriesByType('navigation')[0].type === 'reload') {
    
    
  console.log('WebSocket浏览器刷新了')

  // 延迟一定时间再执行 WebSocket 初始化,确保页面完全加载后再进行连接
  setTimeout(() => {
    
    
    console.log('WebSocket执行刷新后重连...')
    // 刷新后重连
    // 获取username(假设为测试username写死,现在是动态获取)
    const username = useUserStore().user.username
    websocket.Init(username)
  }, 200) // 适当调整延迟时间
}

// 重连方法
function reconnect() {
    
    
  console.log('WebSocket重连开关', isReconnecting)
  // 判断是否主动关闭连接
  if (isReconnecting) {
    
    
    return
  }
  // 重连定时器-每次WebSocket错误方法onerror触发它都会触发
  reconnectTimer && clearTimeout(reconnectTimer)
  reconnectTimer = setTimeout(function () {
    
    
    console.log('WebSocket执行断线重连...')
    // 获取username(假设为测试username写死,现在是动态获取)
    const username = useUserStore().user.username
    websocket.Init(username)
    isReconnecting = false
  }, 4000)
}

// 暴露对象
export default websocket

3.2 WebSocket.vue

<script setup>
import {
    
     ref, onMounted, onBeforeUnmount } from 'vue'
import {
    
     getUserService, sendMessageToService } from '../../api/websocket'
import websocket from '../../utils/websocket'

// 收到的消息
const receivedMessage = ref('')
// 输入框中的消息
const inputMessage = ref('')
// 输入框中的用户ID
const inputUserId = ref('')
// 在线用户
const userList = ref([])

const getMessageCallback = (message) => {
    
    
  receivedMessage.value = message
}

// 在组件挂载时设置消息处理回调
onMounted(() => {
    
    
  websocket.setMessageCallback(getMessageCallback)
})

// 在组件销毁前取消消息处理回调
onBeforeUnmount(() => {
    
    
  websocket.setMessageCallback(null)
})

// 获取在线用户
const getUserList = async () => {
    
    
  const res = await getUserService()
  userList.value = res.data.data
}

// 发送消息
const sendMessage = async () => {
    
    
  const userId = inputUserId.value
  const message = inputMessage.value

  // 调用发送消息的接口
  await sendMessageToService({
    
     message, userId })
}
</script>

<template>
  <div>
    <!-- 输入框和按钮 -->
    <el-input v-model="inputUserId" placeholder="输入用户ID"></el-input>
    <el-input v-model="inputMessage" placeholder="输入消息"></el-input>
    <el-button type="" @click="sendMessage">发送消息</el-button>
  </div>
  <hr />
  <div>
    <div>收到的消息: {
    
    {
    
     receivedMessage }}</div>
  </div>
  <hr />
  <div>
    <el-button type="primary" @click="getUserList">获取在线用户列表</el-button>
    <div class="userList">
      <ul>
        <li v-for="item in userList" :key="item.index">
          {
    
    {
    
     item }}
        </li>
      </ul>
    </div>
  </div>
</template>

<style scoped>
.userList {
    
    
  background-color: pink;
}
</style>

3.3 WebSocket2.vue

<script setup>
import {
    
     ref } from 'vue'
import websocket from '../../utils/websocket'
import {
    
     getUserService, sendMessageToService } from '../../api/websocket'
import {
    
     useUserStore } from '@/stores'
// 用户信息
const userStore = useUserStore()
const username = userStore.user.username
// 收到的消息
const receivedMessage = ref('')

// 输入框中的消息
const inputMessage = ref('')
// 输入框中的用户ID
const inputUserId = ref('')

// 连接WebSocket
const connectWebSocket = async () => {
    
    
  await new Promise((resolve) => {
    
    
    websocket.Init(username)

    // 在 WebSocket 连接成功时调用 resolve
    websocket.getWebSocket().onopen = () => {
    
    
      console.log('WebSocket连接成功')
      resolve()
    }
  })

  // 使用 ref 包装 onmessage 回调
  websocket.getWebSocket().onmessage = (event) => {
    
    
    // 处理消息,这里你可以根据实际需求更新页面上的数据
    console.log('收到的消息WebSocket2:', event.data)
    // 更新收到的消息
    receivedMessage.value = event.data
  }
}

// 断开WebSocket连接
const disconnectWebSocket = () => {
    
    
  websocket.Close()
}

// 获取在线用户
const userList = ref([])
const getUserList = async () => {
    
    
  const res = await getUserService()
  userList.value = res.data.data
}

// 发送消息
const sendMessage = async () => {
    
    
  const userId = inputUserId.value // 使用输入框中的用户ID
  const message = inputMessage.value

  // 调用发送消息的接口
  await sendMessageToService({
    
     message, userId })

  // 清空输入框
  // inputMessage.value = ''
}
</script>

<template>
  <div>
    <el-button type="primary" @click="connectWebSocket">连接WebSocket</el-button>
    <el-button type="danger" @click="disconnectWebSocket">断开WebSocket</el-button>
    <br />
    <!-- 输入框和按钮 -->
    <el-input v-model="inputUserId" placeholder="输入用户ID" style="width: 120px"></el-input>
    <el-input v-model="inputMessage" placeholder="输入消息"></el-input>
    <el-button type="" @click="sendMessage">发送消息</el-button>
  </div>
  <hr />
  <div>
    <span>收到的消息:{
    
    {
    
     receivedMessage }}</span>
  </div>
  <hr />
  <div>
    <el-button type="primary" @click="getUserList">获取在线用户列表</el-button>
    <div class="userList">
      <ul>
        <li v-for="item in userList" :key="item.index">
          {
    
    {
    
     item }}
        </li>
      </ul>
    </div>
    <div>
      <span>有缺点无法获取实时数据,必须断开在连接,应该是由于不是和登录一个作用域导致的</span>
    </div>
  </div>
</template>

<style scoped>
.userList {
    
    
  background-color: pink;
}
</style>

3.4.WebSocket.js

请求接口

import request from '@/utils/request'

// 获取用户信息
export const getUserService = () => request.get('/test/webSocket/getUser')

// 发送指定消息
export const sendMessageToService = ({
     
      message, userId }) => {
    
    
  const params = {
    
    
    message,
    userId
  }
  return request.get('/test/webSocket/sendMessageTo', {
    
     params })
}

3.5.登录退出页

连接关闭
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

4.页面展示

4.1.静态展示

在这里插入图片描述

4.2.发送消息弹框展示并展示在页面上

在这里插入图片描述
在这里插入图片描述

5.缺陷问题

切换到WebSocket2页面
断开两个用户的连接

在这里插入图片描述

在重新连接两个用户

在这里插入图片描述

5.1.问题1

重新发送信息(下图),接收了3次OK,但是没有弹框消息了!

在这里插入图片描述

5.2.问题2

刷新浏览器(下图),有了弹框但是页面消息没有!不知道为啥,估计是js作用域不一致导致的(难道是覆盖率用户名?)?

在这里插入图片描述

5.3.问题3

此时切换到WebSocket1页面,发现控制台接收到了消息,但是页面既没有弹框,也没有消息文字!

在这里插入图片描述

欢迎各位大佬,如知原因麻烦告知,非常感谢!

猜你喜欢

转载自blog.csdn.net/Ying_ph/article/details/134953788