Springboot WebSocket + Vue3 integrates a super detailed picture and text

1 Introduction

Springboot+vue3 integrates WebSocket (rough and simple to use)

2. Backend code

2.1.Maven

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

2.2.WebSocket configuration class

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. Simple interface

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. Front-end code

structure:
Insert image description here

3.1.WebSocket.js (unified configuration)

// 信息提示
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

Request interface

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. Login and exit page

connection closed
Insert image description here
Insert image description here
Insert image description here

4. Page display

4.1.Static display

Insert image description here

4.2. Send message pop-up box is displayed and displayed on the page

Insert image description here
Insert image description here

5. Defect issues

Switch to WebSocket2 page
Disconnect two users

Insert image description here

After reconnecting both users

Insert image description here

5.1. Question 1

Resend the message (picture below), received OK 3 times, but there is no pop-up message!

Insert image description here

5.2. Question 2

Refresh the browser (picture below), there is a pop-up box but no page message! I don’t know why, maybe it’s caused by the inconsistency of js scope (could it be the coverage username?)?

Insert image description here

5.3. Question 3

At this time, I switched to the WebSocket1 page and found that the console received the message, but there was neither a pop-up box nor message text on the page!

Insert image description here

Welcome everyone, if you know the reason, please let me know, thank you very much!

Guess you like

Origin blog.csdn.net/Ying_ph/article/details/134953788