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页面,发现控制台接收到了消息,但是页面既没有弹框,也没有消息文字!
欢迎各位大佬,如知原因麻烦告知,非常感谢!
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/192683.html