【Spring Boot开发实战手册】掌握Springboot开发技巧和窍门(十三)前端匹配界面、后端匹配WebSocket

【Spring Boot开发实战手册】掌握Springboot开发技巧和窍门(十三)前端匹配界面、后端匹配WebSocket

前言

在现代 Web 开发中,前端和后端的协作变得越来越重要,特别是在需要实时交互和数据更新的应用场景中。WebSocket 技术作为一种全双工通信协议,使得前端和后端之间的实时数据传输变得更加高效和稳定。本篇博客将会探讨如何设计和实现一个实时匹配系统,其中前端负责展示用户界面并与后端进行交互,而后端则通过 WebSocket 协议来处理数据通信。


前端

onMounted: 当组件被挂载的时候执行的函数
onUnmonted: 当组件被卸载的时候执行的函数
初步调试阶段,我们是将token传进user.id的
store/pk.js:

import ModuleUser from'./user'exportdefault{state:{socket:null,//ws链接opponent_username:"",opponent_photo:"",status:"matching",//matching表示匹配界面,playing表示对战界面},getters:{},mutations:{updateSocket(state,socket){ state.socket = socket;},updateOpponent(state,opponent){ state.opponent_username = opponent.username; state.opponent_photo = opponent.photo;},updateStatus(state,status){ state.status = status;}},actions:{},modules:{user: ModuleUser,}}

将pk引入store中

store/index.js

import{ createStore }from'vuex'import ModuleUser from'./user'import ModulePk from'./pk'exportdefaultcreateStore({state:{},getters:{},mutations:{},actions:{},modules:{user: ModuleUser,pk: ModulePk,}})

前端与后端建立连接
views/pk/PKIndex.vue

<template><PlayGround/></template><script>//import ContentBase from "@/components/ContentBase.vue"import PlayGround from"@/components/PlayGround.vue"import{ onMounted, onUnmounted }from"vue";import{ useStore }from"vuex"exportdefault{name:"PKindex",components:{// ContentBase, PlayGround,},setup(){const store =useStore();//字符串中有${}表达式操作的话要用``,不能用引号const socketUrl =`ws://127.0.0.1:3000/websocket/${store.state.user.id}/`;let socket =null;onMounted(()=>{//当当前页面打开时调用 socket =newWebSocket(socketUrl);//js自带的WebSocket() socket.onopen=()=>{//连接成功时调用的函数 console.log("connected!"); store.commit("updateSocket",socket);} socket.onmessage=msg=>{//前端接收到信息时调用的函数const data =JSON.parse(msg.data);//不同的框架数据定义的格式不一样 console.log(data);} socket.onclose=()=>{//关闭时调用的函数 console.log("disconnected!");}});onUnmounted(()=>{//当当前页面关闭时调用 socket.close();//卸载的时候断开连接});}}</script><style scoped></style>

至此,前端与后端就可以通过websocket互相连接了。

在这里插入图片描述


在这里插入图片描述

将token改成jwt验证

若使用userId建立ws连接,用户可伪装成任意用户,因此这是不安全的

const socketUrl =`ws://127.0.0.1:3000/websocket/${store.state.user.token}/`;

添加ws的jwt验证,根据token判断用户是否存在
consumer/utils/JwtAuthenciation.java

packageorg.example.backend.consumer.utils;importio.jsonwebtoken.Claims;importorg.example.backend.utils.JwtUtil;publicclassJwtAuthentication{publicstaticIntegergetUserId(String token){int userId =-1;//-1表示不存在try{Claims claims =JwtUtil.parseJWT(token); userId =Integer.parseInt(claims.getSubject());}catch(Exception e){thrownewRuntimeException(e);}return userId;}}

修改后端
consumer/WebSocketServer.java

如果可以正常解析出jwt token的话表示登录成功,否则登录不成功,直接close

...@OnOpenpublicvoidonOpen(Session session,@PathParam("token")String token)throwsIOException{// 建立连接this.session = session;System.out.println("connected to websocket");int userId =Integer.parseInt(token);this.user = userMapper.selectById(userId);if(user !=null){ users.put(userId,this);}else{this.session.close();}}...

实现前端逻辑

对战界面和匹配界面的切换

views/pk/PKindexView.vue

<template><PlayGround v-if="$store.state.pk.status === 'playing'"/><MatchGround v-if="$store.state.pk.status === 'matching'"/></template>

创建匹配页面
components/MatchGround.vue

<template><div class="matchground"></div></template><script>exportdefault{}</script><style scoped> div.matchground {width: 60vw;height: 70vh;margin: 40px auto; background-color: lightblue;}</style>

设置此时的状态是匹配

在这里插入图片描述


最终效果:

在这里插入图片描述

匹配界面

用grid系统布局自己头像:对手头像= 6 : 6
逻辑很简单,只要点击匹配按钮,就向后端发送请求开始匹配。
components/MatchGround.vue

<template><div class="matchground"><div class="row"><div class="col-6"><div class="user-photo"><img :src="$store.state.user.photo" alt=""></div><div class="user-username">{{ $store.state.user.username }}</div></div><div class="col-6"><div class="user-photo"><img :src="$store.state.pk.opponent_photo" alt=""></div><div class="user-username">{{ $store.state.pk.opponent_username }}</div></div><div class="col-12" style="text-align: center; padding-top: 15vh;"><button @click="click_match_btn" type="button"class="btn btn-warning btn-lg">{{ match_btn_info }}</button></div></div></div></template><script>import{ ref }from'vue'import{ useStore }from'vuex';exportdefault{setup(){const store =useStore();let match_btn_info =ref("开始匹配");constclick_match_btn=()=>{if(match_btn_info.value ==="开始匹配"){ match_btn_info.value ="取消"; store.state.pk.socket.send(JSON.stringify({event:"start-matching",}));}else{ match_btn_info.value ="开始匹配"; store.state.pk.socket.send(JSON.stringify({event:"stop-matching",}));}}return{ match_btn_info, click_match_btn,}}}</script><style scoped> div.matchground {width: 60vw;height: 70vh;margin: 40px auto; background-color:rgba(50,50,50,0.5);} div.user-photo { text-align: center; padding-top: 10vh;} div.user-photo>img { border-radius:50%;width: 20vh;} div.user-username { text-align: center; font-size: 24px; font-weight:600;color: white; padding-top: 2vh;}</style>

整体结构:

  • 外层 div.matchground 作为容器。
  • 内部是 Bootstrap 栅格布局 row + col-6/col-12。

左侧用户信息:

  • img 显示用户头像,src 来自 Vuex 状态 store.state.user.photo。
  • div 显示用户名,{{ $store.state.user.username }}。

右侧对手信息:

  • 类似用户,显示对手头像和用户名,来自 store.state.pk.opponent_photo 和 store.state.pk.opponent_username。

按钮:

  • 居中显示,绑定点击事件 @click=“click_match_btn”。
  • 按钮文字使用 match_btn_info 变量绑定(响应式)。

点击事件逻辑:

  • 如果按钮显示“开始匹配”:
    • 改成“取消”。
    • 通过 WebSocket store.state.pk.socket.send 发事件 “start-matching”。
  • 如果按钮显示“取消”:
    • 改回“开始匹配”。
    • 发事件 “stop-matching”。
  • 返回模板使用:
    • return { match_btn_info, click_match_btn },模板可以直接使用这些变量和方法。

页面展示 两位玩家头像 + 名字。

  • 中间有 匹配按钮,点击会:
  • 改变按钮文字(“开始匹配” ↔ “取消”)。
  • 通过 WebSocket 发送匹配事件给后端。

使用 Vue 3 Composition API + Vuex + Bootstrap 栅格布局 实现

后端consumer/WebSocketServer.java

packageorg.example.backend.consumer;importcom.alibaba.fastjson.JSONObject;importorg.example.backend.consumer.utils.Game;importorg.example.backend.consumer.utils.JwtAuthentication;importorg.example.backend.mapper.UserMapper;importorg.example.backend.pojo.User;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.stereotype.Component;importjavax.websocket.*;importjavax.websocket.server.PathParam;importjavax.websocket.server.ServerEndpoint;importjava.io.IOException;importjava.util.Iterator;importjava.util.concurrent.ConcurrentHashMap;importjava.util.concurrent.CopyOnWriteArraySet;@Component@ServerEndpoint("/websocket/{token}")// 注意不要以'/'结尾publicclassWebSocketServer{finalprivatestaticConcurrentHashMap<Integer,WebSocketServer> users =newConcurrentHashMap<>();finalprivatestaticCopyOnWriteArraySet<User> matchpool =newCopyOnWriteArraySet<>();privateUser user;privateSession session =null;privatestaticUserMapper userMapper;@AutowiredpublicvoidsetUserMapper(UserMapper userMapper){WebSocketServer.userMapper = userMapper;}@OnOpenpublicvoidonOpen(Session session,@PathParam("token")String token)throwsIOException{this.session = session;System.out.println("connected!");Integer userId =JwtAuthentication.getUserId(token);this.user = userMapper.selectById(userId);if(this.user !=null){ users.put(userId,this);}else{this.session.close();}System.out.println(users);}@OnClosepublicvoidonClose(){System.out.println("disconnected!");if(this.user !=null){ users.remove(this.user.getId()); matchpool.remove(this.user);}}privatevoidstartMatching(){System.out.println("start matching!"); matchpool.add(this.user);while(matchpool.size()>=2){Iterator<User> it = matchpool.iterator();User a = it.next(), b = it.next(); matchpool.remove(a); matchpool.remove(b);Game game =newGame(13,14,20); game.createMap();JSONObject respA =newJSONObject(); respA.put("event","start-matching"); respA.put("opponent_username", b.getUsername()); respA.put("opponent_photo", b.getPhoto()); respA.put("gamemap", game.getG()); users.get(a.getId()).sendMessage(respA.toJSONString());JSONObject respB =newJSONObject(); respB.put("event","start-matching"); respB.put("opponent_username", a.getUsername()); respB.put("opponent_photo", a.getPhoto()); respB.put("gamemap", game.getG()); users.get(b.getId()).sendMessage(respB.toJSONString());}}privatevoidstopMatching(){System.out.println("stop matching"); matchpool.remove(this.user);}@OnMessagepublicvoidonMessage(String message,Session session){// 当做路由System.out.println("receive message!");JSONObject data =JSONObject.parseObject(message);String event = data.getString("event");if("start-matching".equals(event)){startMatching();}elseif("stop-matching".equals(event)){stopMatching();}}@OnErrorpublicvoidonError(Session session,Throwable error){ error.printStackTrace();}publicvoidsendMessage(String message){synchronized(this.session){try{this.session.getBasicRemote().sendText(message);}catch(IOException e){ e.printStackTrace();}}}}

@ServerEndpoint(“/websocket/{token}”):这是 WebSocket 的入口点,指定了 WebSocket 服务的 URL 路径。{token} 是一个路径参数,用来验证和识别用户的身份。
users:使用 ConcurrentHashMap 存储所有连接的用户,用户 ID 是键,WebSocketServer 实例是值。
matchpool:使用 CopyOnWriteArraySet 存储正在匹配的用户。CopyOnWriteArraySet 是线程安全的集合类。
user:当前 WebSocket 连接对应的用户信息。
session:Session 对象表示 WebSocket 连接的会话。

onOpen方法:

  • @OnOpen:WebSocket 连接建立时会调用这个方法。
  • session:WebSocket 会话。
  • @PathParam(“token”):从 URL 中提取用户的身份验证 token。
  • 使用 JwtAuthentication.getUserId(token) 从 token 获取用户 ID,然后查询数据库获取用户信息。
  • 如果用户存在,就将 WebSocketServer 实例与用户 ID 关联,并保存到 users 中;否则关闭连接。

onClose方法:

  • @OnClose:WebSocket 连接关闭时调用这个方法。
  • 如果用户存在,移除 users 和 matchpool 中该用户的信息。

游戏匹配逻辑startMatching方法:
启动匹配过程,将当前用户添加到 matchpool 中。
如果匹配池中有两个以上的用户(至少两个人可以开始匹配),则开始配对:

  • 从 matchpool 中取出两名用户。
  • 创建一个新的游戏实例,并生成地图。
  • 向这两名用户发送匹配成功的消息,消息内容包括对方用户名、头像和游戏地图。

stopMatching方法:

  • 停止匹配,将当前用户从 matchpool 中移除。

消息处理onMessage方法:

  • @OnMessage:当 WebSocket 收到消息时会调用此方法。
  • 解析收到的消息,根据 event 字段的值来决定是开始匹配还是停止匹配。

发送消息sendMessage方法:

  • 发送消息给当前 WebSocket 客户端。
  • 使用 synchronized 确保线程安全。

后端返回信息给前端后,在前端接受并处理信息
views/PKindex.vue

<template><PlayGround v-if="$store.state.pk.status === 'playing'"/><MatchGround v-if="$store.state.pk.status === 'matching'"/></template><script>import PlayGround from'@/components/PlayGround.vue';import MatchGround from'@/components/MatchGround.vue';import{ onMounted, onUnmounted }from'vue'import{ useStore }from'vuex'exportdefault{components:{ PlayGround, MatchGround,},setup(){const store =useStore();const socketUrl =`ws://127.0.0.1:3000/websocket/${store.state.user.token}/`;let socket =null;onMounted(()=>{ store.commit("updateOpponent",{username:"我的对手",photo:"https://cdn.acwing.com/media/article/image/2022/08/09/1_1db2488f17-anonymous.png",}) socket =newWebSocket(socketUrl); socket.onopen=()=>{ console.log("connected!"); store.commit("updateSocket", socket);} socket.onmessage=msg=>{const data =JSON.parse(msg.data);if(data.event ==="start-matching"){// 匹配成功 store.commit("updateOpponent",{username: data.opponent_username,photo: data.opponent_photo,});setTimeout(()=>{ store.commit("updateStatus","playing");},2000); store.commit("updateGamemap", data.gamemap);}} socket.onclose=()=>{ console.log("disconnected!");}});onUnmounted(()=>{ socket.close(); store.commit("updateStatus","matching");})}}</script><style scoped></style>

const store = useStore(); 用于访问 Vuex 状态管理对象,获取和修改全局状态。
const socketUrl = ws://127.0.0.1:3000/websocket/${store.state.user.token}/; 构建 WebSocket 连接的 URL,使用当前用户的 token 来进行身份验证。
在 onMounted 生命周期钩子中:
使用 store.commit(“updateOpponent”, …) 设置默认的对手信息(用户名和头像)。
创建 WebSocket 实例并建立连接,连接成功时通过 socket.onopen 事件回调执行:
连接成功后,调用 store.commit(“updateSocket”, socket) 更新全局状态中的 WebSocket 实例。
通过 socket.onmessage 监听消息,接收从 WebSocket 服务器发送的数据:
如果 data.event 为 “start-matching”,表示匹配成功。
更新对手信息和游戏地图,并在 2 秒后通过 store.commit(“updateStatus”, “playing”) 更新状态为 “playing”,表示开始游戏。
通过 socket.onclose 监听 WebSocket 连接关闭事件。

在 onUnmounted 生命周期钩子中:
关闭 WebSocket 连接。
更新 pk.status 状态为 “matching”,表示重新回到匹配阶段。

基于 WebSocket 的游戏匹配系统,具体流程如下:

  • 当组件挂载时,通过 WebSocket 与服务器建立连接。
  • 在匹配阶段,用户与服务器实时交换数据,匹配成功后显示对手的信息并开始游戏。
  • 游戏过程中的状态(如对手信息和游戏地图)通过 Vuex 管理,并根据状态动态- 渲染不同的界面组件(MatchGround 或 PlayGround)。
  • 在组件卸载时关闭 WebSocket 连接,确保没有资源泄漏。

解决同步问题

首先要在后端创建一个Game类实现游戏流程,其实就是把之前在前端写的js全部翻译成Java就好了
consumer/utils/Game.java

packageorg.example.backend.consumer.utils;importjava.util.Random;publicclassGame{finalprivateInteger rows;finalprivateInteger cols;finalprivateInteger inner_walls_count;finalprivateint[][] g;finalprivatestaticint[] dx ={-1,0,1,0}, dy ={0,1,0,-1};publicGame(Integer rows,Integer cols,Integer inner_walls_count){this.rows = rows;this.cols = cols;this.inner_walls_count = inner_walls_count;this.g =newint[rows][cols];}publicint[][]getG(){return g;}privatebooleancheck_connectivity(int sx,int sy,int tx,int ty){if(sx == tx && sy == ty)returntrue; g[sx][sy]=1;for(int i =0; i <4; i ++){int x = sx + dx[i], y = sy + dy[i];if(x >=0&& x <this.rows && y >=0&& y <this.cols && g[x][y]==0){if(check_connectivity(x, y, tx, ty)){ g[sx][sy]=0;returntrue;}}} g[sx][sy]=0;returnfalse;}privatebooleandraw(){// 画地图for(int i =0; i <this.rows; i ++){for(int j =0; j <this.cols; j ++){ g[i][j]=0;}}for(int r =0; r <this.rows; r ++){ g[r][0]= g[r][this.cols -1]=1;}for(int c =0; c <this.cols; c ++){ g[0][c]= g[this.rows -1][c]=1;}Random random =newRandom();for(int i =0; i <this.inner_walls_count /2; i ++){for(int j =0; j <1000; j ++){int r = random.nextInt(this.rows);int c = random.nextInt(this.cols);if(g[r][c]==1|| g[this.rows -1- r][this.cols -1- c]==1)continue;if(r ==this.rows -2&& c ==1|| r ==1&& c ==this.cols -2)continue; g[r][c]= g[this.rows -1- r][this.cols -1- c]=1;break;}}returncheck_connectivity(this.rows -2,1,1,this.cols -2);}publicvoidcreateMap(){for(int i =0; i <1000; i ++){if(draw())break;}}}

Game 类主要用于生成一个带有边界和内部随机墙壁的连通游戏地图,使用了深度优先搜索(DFS)算法来检查地图的连通性。

功能概述:

  • 初始化地图的大小和墙壁数量。
  • 生成并检查地图的连通性。
  • 提供获取地图的方法以便其他类或模块使用。

在前端的pk.js中:

exportdefault{state:{status:"matching",// matching表示匹配界面,playing表示对战界面socket:null,opponent_username:"",opponent_photo:"",gamemap:null,},getters:{},mutations:{updateSocket(state, socket){ state.socket = socket;},updateOpponent(state, opponent){ state.opponent_username = opponent.username; state.opponent_photo = opponent.photo;},updateStatus(state, status){ state.status = status;},updateGamemap(state, gamemap){ state.gamemap = gamemap;}},actions:{},modules:{}}

status:当前游戏状态

  • “matching” → 正在匹配阶段(显示匹配界面)
  • “playing” → 游戏进行中(显示对战界面)

socket:存储 WebSocket 对象,用于实时通信。
opponent_username & opponent_photo:记录对手信息。
gamemap:存储游戏地图或游戏数据(例如双方棋盘或战斗场景等)。

updateSocket: 更新 WebSocket 对象
updateOpponent: 更新对手的用户名和头像
updateStatus: 更新游戏状态(matching ↔ playing)
updateGamemap: 更新游戏地图信息

展示结果:

在这里插入图片描述

总结

通过本文的介绍,您应该对如何使用 WebSocket 实现前端与后端的实时匹配有了一个清晰的理解。在开发过程中,前端和后端需要通过紧密的配合来确保实时数据的正确传输和处理。前端负责展示用户的操作界面并通过 WebSocket 与后端保持实时连接,后端则处理客户端的请求并返回实时数据。

Read more

IDEA 插件 Trae AI 全攻略

在 Java 开发的日常中,你是否经常遇到这些场景:     面对重复的 CRUD 代码,机械敲击键盘却内心抗拒?     接手 legacy 系统,看着几百行的复杂逻辑无从下手?     调试时卡在某个异常,翻遍文档和 Stack Overflow 却找不到答案?     写单元测试时,明明功能简单却要耗费大量时间设计测试用例? 这些问题的核心,在于重复性工作占用了太多创造性时间。而随着 AI 技术的发展,AI 辅助开发工具已成为突破效率瓶颈的关键。在众多工具中,Trae AI作为 IDEA 的一款插件,凭借对 Java 生态的深度适配、与 IDE 的无缝集成以及强大的代码理解能力,逐渐成为开发者的 “编码搭子”。 本文将从基础到进阶,全面讲解 Trae AI 的功能、用法、实战技巧和最佳实践,帮你彻底释放 AI 辅助开发的潜力,让编码效率提升

通义灵码 AI 程序员 实操全指南:从 IDE 安装到全栈需求落地(多文件批量修改 + 报错自动修复 + 跨语言开发)

通义灵码 AI 程序员 实操全指南:从 IDE 安装到全栈需求落地(多文件批量修改 + 报错自动修复 + 跨语言开发)

1. 背景与趋势 随着软件系统复杂度提升,传统开发模式面临代码重复率高、调试周期长、跨语言协作难等挑战。AI辅助编程已从单文件代码补全,演进为项目级代码理解、全流程开发辅助的核心生产力工具。通义灵码作为AI程序员,整合代码生成、重构、调试、多语言协作等能力,可覆盖从需求分析到部署上线的完整开发链路。 2. 核心技术原理 2.1 代码预训练与多语言理解 基于大规模代码语料(覆盖100+编程语言、10TB+开源代码),采用Transformer架构的代码大模型,学习语法规则、语义逻辑、设计模式及最佳实践,支持Java、Python、Go、Rust、TypeScript等主流语言的深度理解。 2.2 上下文感知与长序列处理 支持100K+ Token上下文窗口,可解析项目级代码结构(包括多文件依赖、类继承关系、API调用链),实现跨文件的逻辑一致性校验与修改。 2.3 多模态交互与工具链集成 支持自然语言、代码片段、错误日志、

DeerFlow 2.0实战指南:生产级AI Agent框架的Docker化部署与并行编排

DeerFlow 2.0实战指南:生产级AI Agent框架的Docker化部署与并行编排

前言:为什么要选择DeerFlow 2.0? 最近字节跳动开源的DeerFlow 2.0在GitHub上火了,几天时间收获45,000+星。作为一名技术开发者,我第一时间研究了这个项目。经过深入测试,我发现它解决了传统AI Agent框架在生产环境中的几个关键痛点。 本文将从工程实践角度,带你完整掌握DeerFlow 2.0的核心功能和部署方法。 一、核心技术架构解析 1.1 Docker隔离机制 传统框架的问题:多个任务共享进程,一个任务崩溃影响全局。 DeerFlow 2.0的解决方案:每个任务独立Docker容器。 核心代码实现: # 任务容器创建函数defcreate_task_container(task_id, skill_config): container = docker_client.containers.run( image="deerflow-agent-base:latest", command=

用 OpenClaw 搭建企业微信 AI Agent:从零到自动化客服只需 30 分钟

本教程将指导您使用 OpenClaw 搭建一个企业微信 AI 客服,实现 7×24 小时自动回复。以阿里云为例,其他云服务商(如腾讯云)的流程也基本一致。 🎯 整体流程概览 1. 准备账号:注册企业微信,并获取阿里云百炼大模型 API Key。 2. 部署 OpenClaw:在云服务器上部署 OpenClaw 并接入大模型。 3. 配置企业微信:创建应用或机器人,获取 CorpID、AgentID、Secret 等凭证。 4. 打通连接:将企业微信的回调地址指向 OpenClaw 服务。 5. 测试优化:在微信中测试对话,并通过优化知识库提升准确率。 🛠️ 一、准备工作 (约 5 分钟) 1. 企业微信