跳到主要内容 【Spring Boot开发实战手册】掌握Springboot开发技巧和窍门(十三)前端匹配界面、后端匹配WebSocket | 极客日志
Python
【Spring Boot开发实战手册】掌握Springboot开发技巧和窍门(十三)前端匹配界面、后端匹配WebSocket 前言 在现代 Web 开发中,前端和后端的协作变得越来越重要,特别是在需要实时交互和数据更新的应用场景中。WebSocket 技术作为一种全双工通信协议,使得前端和后端之间的实时数据传输变得更加高效和稳定。本篇博客将会探讨如何设计和实现一个实时匹配系统,其中前端负责展示用户界面并与后端进行交互,而后端则通过 WebSocket 协议来处理数据通信。 * * 前端 onMounted: 当组件被挂载…
不知所云 发布于 2026/4/7 更新于 2026/4/12 31K 浏览前言
在现代 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" ,//ma tching表示匹配界面,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" { onMounted, onUnmounted } ; { useStore } exportdefault{name: ,components:{// ContentBase, PlayGround,},setup(){const store =useStore();//字符串中有${}表达式操作的话要用``,不能用引号const socketUrl =`ws:// : /websocket/${store.state.user. }/`;let socket =null;onMounted(()=>{//当当前页面打开时调用 socket =newWebSocket(socketUrl);//js自带的WebSocket() socket.onopen=()=>{//连接成功时调用的函数 console.log( ); store.commit( ,socket);} socket.onmessage=msg=>{//前端接收到信息时调用的函数const data =JSON.parse(msg.data);//不同的框架数据定义的格式不一样 console.log(data);} socket.onclose=()=>{//关闭时调用的函数 console.log( );}});onUnmounted(()=>{//当当前页面关闭时调用 socket.close();//卸载的时候断开连接});}}</script><style scoped></style>
微信扫一扫,关注极客日志 微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
相关免费在线工具 curl 转代码 解析常见 curl 参数并生成 fetch、axios、PHP curl 或 Python requests 示例代码。 在线工具,curl 转代码在线工具,online
Base64 字符串编码/解码 将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
Base64 文件转换器 将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
Markdown 转 HTML 将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML 转 Markdown 互为补充。 在线工具,Markdown 转 HTML在线工具,online
HTML 转 Markdown 将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML 转 Markdown在线工具,online
JSON 压缩 通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online
import
from
"vue"
import
from
"vuex"
"PKindex"
127.0
.0
.1
3000
id
"connected!"
"updateSocket"
"disconnected!"
至此,前端与后端就可以通过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 {
实现前端逻辑
对战界面和匹配界面的切换 <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){
@ServerEndpoint('/websocket/{token}'):这是 WebSocket 的入口点,指定了 WebSocket 服务的 URL 路径。{token} 是一个路径参数,用来验证和识别用户的身份。
users:使用 ConcurrentHashMap 存储所有连接的用户,用户 ID 是键,WebSocketServer 实例是值。
matchpool:使用 CopyOnWriteArraySet 存储正在匹配的用户。CopyOnWriteArraySet 是线程安全的集合类。
user:当前 WebSocket 连接对应的用户信息。
session:Session 对象表示 WebSocket 连接的会话。
@OnOpen:WebSocket 连接建立时会调用这个方法。
session:WebSocket 会话。
@PathParam('token'):从 URL 中提取用户的身份验证 token。
使用 JwtAuthentication.getUserId(token) 从 token 获取用户 ID,然后查询数据库获取用户信息。
如果用户存在,就将 WebSocketServer 实例与用户 ID 关联,并保存到 users 中;否则关闭连接。
@OnClose:WebSocket 连接关闭时调用这个方法。
如果用户存在,移除 users 和 matchpool 中该用户的信息。
游戏匹配逻辑startMatching方法:
启动匹配过程,将当前用户添加到 matchpool 中。
如果匹配池中有两个以上的用户(至少两个人可以开始匹配),则开始配对:
从 matchpool 中取出两名用户。
创建一个新的游戏实例,并生成地图。
向这两名用户发送匹配成功的消息,消息内容包括对方用户名、头像和游戏地图。
停止匹配,将当前用户从 matchpool 中移除。
@OnMessage:当 WebSocket 收到消息时会调用此方法。
解析收到的消息,根据 event 字段的值来决定是开始匹配还是停止匹配。
发送消息给当前 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" ){</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)算法来检查地图的连通性。
初始化地图的大小和墙壁数量。
生成并检查地图的连通性。
提供获取地图的方法以便其他类或模块使用。
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:{}}
'matching' → 正在匹配阶段(显示匹配界面)
'playing' → 游戏进行中(显示对战界面)
socket:存储 WebSocket 对象,用于实时通信。
opponent_username & opponent_photo:记录对手信息。
gamemap:存储游戏地图或游戏数据(例如双方棋盘或战斗场景等)。
updateSocket: 更新 WebSocket 对象
updateOpponent: 更新对手的用户名和头像
updateStatus: 更新游戏状态(matching ↔ playing)
updateGamemap: 更新游戏地图信息
总结 通过本文的介绍,您应该对如何使用 WebSocket 实现前端与后端的实时匹配有了一个清晰的理解。在开发过程中,前端和后端需要通过紧密的配合来确保实时数据的正确传输和处理。前端负责展示用户的操作界面并通过 WebSocket 与后端保持实时连接,后端则处理客户端的请求并返回实时数据。