前言
在现代 Web 开发中,前端和后端的协作至关重要,特别是在需要实时交互和数据更新的应用场景中。WebSocket 技术作为一种全双工通信协议,使得前端和后端之间的实时数据传输更加高效和稳定。本文探讨如何设计和实现一个实时匹配系统,其中前端负责展示用户界面并与后端进行交互,后端则通过 WebSocket 协议处理数据通信。
前端状态管理
Vuex Store 配置
在 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,
}
}
在 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,
}
})
建立 WebSocket 连接
在 views/pk/PKIndex.vue 中建立连接并监听事件:
<template>
<PlayGround/>
</template>
<script>
import PlayGround from '@/components/PlayGround.vue'
import { onMounted, onUnmounted } from 'vue'
import { useStore } from 'vuex'
export default {
name: "PKindex",
components: { 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)
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(() => {
if (socket) socket.close()
})
}
}
</script>
<style scoped></style>
JWT 验证优化
若使用 userId 建立 WS 连接,用户可伪装成任意用户,存在安全隐患。建议改用 JWT Token 验证。
修改前端连接 URL:
const socketUrl = `ws://127.0.0.1:3000/websocket/${store.state.user.token}/`
后端添加 JWT 解析逻辑 consumer/utils/JwtAuthentication.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 的 onOpen 方法,验证 Token 有效性:
@OnOpen
public void onOpen(Session session, @PathParam("token") String token) throws IOException {
this.session = session;
System.out.println("connected to websocket");
Integer userId = JwtAuthentication.getUserId(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 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>
点击匹配按钮时,通过 WebSocket 发送 start-matching 或 stop-matching 事件。
后端匹配逻辑
consumer/WebSocketServer.java 核心逻辑:
@Component
@ServerEndpoint("/websocket/{token}")
public class WebSocketServer {
private final static ConcurrentHashMap<Integer, WebSocketServer> users = new ConcurrentHashMap<>();
private final 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;
Integer userId = JwtAuthentication.getUserId(token);
this.user = userMapper.selectById(userId);
if (this.user != null) {
users.put(userId, this);
} else {
this.session.close();
}
}
@OnClose
public {
(.user != ) {
users.remove(.user.getId());
matchpool.remove(.user);
}
}
{
matchpool.add(.user);
(matchpool.size() >= ) {
Iterator<User> it = matchpool.iterator();
it.next(), b = it.next();
matchpool.remove(a);
matchpool.remove(b);
(, , );
game.createMap();
();
respA.put(, );
respA.put(, b.getUsername());
respA.put(, b.getPhoto());
respA.put(, game.getG());
users.get(a.getId()).sendMessage(respA.toJSONString());
();
respB.put(, );
respB.put(, a.getUsername());
respB.put(, a.getPhoto());
respB.put(, game.getG());
users.get(b.getId()).sendMessage(respB.toJSONString());
}
}
{
matchpool.remove(.user);
}
{
JSONObject.parseObject(message);
data.getString();
(.equals(event)) {
startMatching();
} (.equals(event)) {
stopMatching();
}
}
{
(.session) {
{
.session.getBasicRemote().sendText(message);
} (IOException e) {
e.printStackTrace();
}
}
}
}
地图生成算法
后端需实现 Game 类生成游戏地图,将前端 JS 逻辑翻译为 Java。
consumer/utils/Game.java:
package org.example.backend.consumer.utils;
import java.util.Random;
public class Game {
private final Integer rows;
private final Integer cols;
private final Integer inner_walls_count;
private final int[][] g;
private final 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 < ; i++) {
sx + dx[i], y = sy + dy[i];
(x >= && x < .rows && y >= && y < .cols && g[x][y] == ) {
(check_connectivity(x, y, tx, ty)) {
g[sx][sy] = ;
;
}
}
}
g[sx][sy] = ;
;
}
{
( ; i < .rows; i++) {
( ; j < .cols; j++) {
g[i][j] = ;
}
}
( ; r < .rows; r++) {
g[r][] = g[r][.cols - ] = ;
}
( ; c < .cols; c++) {
g[][c] = g[.rows - ][c] = ;
}
();
( ; i < .inner_walls_count / ; i++) {
( ; j < ; j++) {
random.nextInt(.rows);
random.nextInt(.cols);
(g[r][c] == || g[.rows - - r][.cols - - c] == ) ;
(r == .rows - && c == || r == && c == .cols - ) ;
g[r][c] = g[.rows - - r][.cols - - c] = ;
;
}
}
check_connectivity(.rows - , , , .cols - );
}
{
( ; i < ; i++) {
(draw()) ;
}
}
}
该算法使用深度优先搜索(DFS)检查地图连通性,确保生成的迷宫可通行。
前端接收匹配结果
在 views/PKindex.vue 中处理后端返回的匹配成功消息:
<script>
// ... imports
export default {
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(() => {
if (socket) socket.close();
store.commit("updateStatus", "matching");
})
}
}
</script>
当收到 start-matching 事件时,更新对手信息、地图数据,并在延迟后切换至游戏界面。
总结
本方案实现了基于 Spring Boot 和 Vue 3 的实时游戏匹配系统。前端利用 Vuex 管理状态并通过 WebSocket 保持连接,后端通过 JWT 验证身份并使用线程安全集合维护匹配池。系统包含界面切换、地图生成算法及消息同步机制,确保了前后端数据的实时一致性。


