前言
在现代 Web 开发中,前端和后端的协作变得越来越重要,特别是在需要实时交互和数据更新的应用场景中。WebSocket 技术作为一种全双工通信协议,使得前端和后端之间的实时数据传输变得更加高效和稳定。本篇博客将会探讨如何设计和实现一个实时匹配系统,其中前端负责展示用户界面并与后端进行交互,而后端则通过 WebSocket 协议来处理数据通信。
前端
onMounted: 当组件被挂载的时候执行的函数
onUnmonted: 当组件被卸载的时候执行的函数
初步调试阶段,我们是将token传进user.id的
store/pk.js:
import ModuleUser from './user'
export default {
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'
export default createStore({
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"
export default {
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 = new WebSocket(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
package org.example.backend.consumer.utils;
import io.jsonwebtoken.Claims;
import org.example.backend.utils.JwtUtil;
public class JwtAuthentication {
public static Integer getUserId(String token) {
int userId = -1; //-1表示不存在
try {
Claims claims = JwtUtil.parseJWT(token);
userId = Integer.parseInt(claims.getSubject());
} catch (Exception e) {
throw new RuntimeException(e);
}
return userId;
}
}
修改后端
consumer/WebSocketServer.java
如果可以正常解析出jwt token的话表示登录成功,否则登录不成功,直接close
...
@OnOpen
public void onOpen(Session session, @PathParam("token") String token) throws IOException {
// 建立连接
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>
export default {
}
</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';
export default {
setup() {
const store = useStore();
let match_btn_info = ref("开始匹配");
const click_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
package org.example.backend.consumer;
import com.alibaba.fastjson.JSONObject;
import org.example.backend.consumer.utils.Game;
import org.example.backend.consumer.utils.JwtAuthentication;
import org.example.backend.mapper.UserMapper;
import org.example.backend.pojo.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.Iterator;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArraySet;
@Component
@ServerEndpoint("/websocket/{token}") // 注意不要以'/'结尾
public class WebSocketServer {
final private static ConcurrentHashMap<Integer, WebSocketServer> users = new ConcurrentHashMap<>();
final private static CopyOnWriteArraySet<User> matchpool = new CopyOnWriteArraySet<>();
private User user;
private Session session = null;
private static UserMapper userMapper;
@Autowired
public void setUserMapper(UserMapper userMapper) {
WebSocketServer.userMapper = userMapper;
}
@OnOpen
public void onOpen(Session session, @PathParam("token") String token) throws IOException {
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);
}
@OnClose
public void onClose() {
System.out.println("disconnected!");
if (this.user != null) {
users.remove(this.user.getId());
matchpool.remove(this.user);
}
}
private void startMatching() {
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 = new Game(13, 14, 20);
game.createMap();
JSONObject respA = new JSONObject();
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 = new JSONObject();
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());
}
}
private void stopMatching() {
System.out.println("stop matching");
matchpool.remove(this.user);
}
@OnMessage
public void onMessage(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();
} else if ("stop-matching".equals(event)) {
stopMatching();
}
}
@OnError
public void onError(Session session, Throwable error) {
error.printStackTrace();
}
public void sendMessage(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'
export default {
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 = new WebSocket(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
package org.example.backend.consumer.utils;
import java.util.Random;
public class Game {
final private Integer rows;
final private Integer cols;
final private Integer inner_walls_count;
final private int[][] g;
final private static int[] dx = {-1, 0, 1, 0}, dy = {0, 1, 0, -1};
public Game(Integer rows, Integer cols, Integer inner_walls_count) {
this.rows = rows;
this.cols = cols;
this.inner_walls_count = inner_walls_count;
this.g = new int[rows][cols];
}
public int[][] getG() {
return g;
}
private boolean check_connectivity(int sx, int sy, int tx, int ty) {
if (sx == tx && sy == ty) return true;
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;
return true;
}
}
}
g[sx][sy] = 0;
return false;
}
private boolean draw() { // 画地图
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 = new Random();
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;
}
}
return check_connectivity(this.rows - 2, 1, 1, this.cols - 2);
}
public void createMap() {
for (int i = 0; i < 1000; i ++ ) {
if (draw())
break;
}
}
}
Game 类主要用于生成一个带有边界和内部随机墙壁的连通游戏地图,使用了深度优先搜索(DFS)算法来检查地图的连通性。
功能概述:
- 初始化地图的大小和墙壁数量。
- 生成并检查地图的连通性。
- 提供获取地图的方法以便其他类或模块使用。
在前端的pk.js中:
export default {
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 与后端保持实时连接,后端则处理客户端的请求并返回实时数据。
转载自CSDN-专业IT技术社区
原文链接:https://blog.csdn.net/m0_63267251/article/details/158685822



