跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客GitHub 精选镜像工具UI配色美学隐私政策关于联系
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
Java大前端java算法

Spring Boot 与 Vue 3 实时游戏匹配实战:WebSocket 前后端对接

综述由AI生成Spring Boot 结合 Vue 3 构建实时游戏匹配系统,核心在于 WebSocket 全双工通信与 JWT 安全验证。前端通过 Vuex 管理连接状态与对手信息,动态切换匹配与对战界面;后端维护用户会话池与匹配队列,利用 DFS 算法生成随机地图并同步至客户端。该方案解决了传统 HTTP 轮询的延迟问题,实现了低延迟的双人在线对战体验。

DevOpsTeam发布于 2026/4/11更新于 2026/5/2311 浏览
Spring Boot 与 Vue 3 实时游戏匹配实战:WebSocket 前后端对接

前言

在现代 Web 开发中,前端和后端的协作至关重要,特别是在需要实时交互的场景。WebSocket 作为全双工通信协议,能显著提升数据传输效率。本文将探讨如何设计并实现一个实时匹配系统,前端负责界面交互,后端通过 WebSocket 处理数据通信。

前端状态管理

首先搭建 Vuex 状态管理,用于维护 WebSocket 连接、对手信息及游戏状态。

在 store/pk.js 中定义基础状态:

import ModuleUser from './user';

export default {
  state: {
    socket: null, // WebSocket 链接
    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 中将 pk 模块引入:

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 中处理连接逻辑。注意使用模板字符串构建 URL,并在组件挂载时初始化连接。

<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>

此时前端已能与后端建立基础连接。但直接使用 userId 存在安全隐患,用户可伪装成任意 ID。因此我们需要引入 JWT 验证。

安全验证:JWT

修改连接 URL,将 token 作为路径参数传递:

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

后端需解析 token 并校验用户身份。在 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);
  User user = userMapper.selectById(userId);
  
  if (user != null) {
    users.put(userId, this);
    this.user = user;
  } else {
    this.session.close();
  }
}

界面切换与布局

根据 Vuex 中的 status 状态,动态渲染匹配或对战组件。

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

创建 components/MatchGround.vue 实现匹配界面。使用 Bootstrap 栅格布局,左右各占一半显示头像和用户名,中间放置操作按钮。

<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>

点击按钮后,前端发送 start-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 void onClose() {
    if (this.user != null) {
      users.remove(this.user.getId());
      matchpool.remove(this.user);
    }
  }

  private void startMatching() {
    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() {
    matchpool.remove(this.user);
  }

  @OnMessage
  public void onMessage(String message, Session session) {
    JSONObject data = JSONObject.parseObject(message);
    String event = data.getString("event");
    if ("start-matching".equals(event)) {
      startMatching();
    } else if ("stop-matching".equals(event)) {
      stopMatching();
    }
  }

  public void sendMessage(String message) {
    synchronized (this.session) {
      try {
        this.session.getBasicRemote().sendText(message);
      } catch (IOException e) {
        e.printStackTrace();
      }
    }
  }
}

这里使用了 ConcurrentHashMap 存储在线用户,CopyOnWriteArraySet 存储匹配池,确保线程安全。当池中人数达到 2 人时,生成游戏地图并通知双方。

游戏地图同步

后端生成的地图数据需要同步到前端。前端接收 start-matching 事件后,更新 Vuex 状态并切换到对战界面。

<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 = () => {
        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 = () => {
        store.commit("updateStatus", "matching");
      };
    });

    onUnmounted(() => {
      if (socket) socket.close();
    });
  }
};
</script>

同时,store/pk.js 中需增加 gamemap 字段及对应的 mutation:

state: {
  // ... existing fields
  gamemap: null,
},
mutations: {
  // ... existing mutations
  updateGamemap(state, gamemap) {
    state.gamemap = gamemap;
  }
}

后端 Game.java 类负责生成随机地图,使用 DFS 算法检查连通性,确保地图合法且可通行。

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 = 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;
    }
  }
}

总结

通过上述步骤,我们完成了一个基于 Spring Boot 和 Vue 3 的实时游戏匹配系统。核心在于利用 WebSocket 实现低延迟通信,结合 JWT 保障安全性,并通过 Vuex 管理全局状态以驱动界面切换。这种架构有效解决了传统 HTTP 轮询的延迟问题,为双人在线对战提供了稳定的技术支撑。

目录

  1. 前言
  2. 前端状态管理
  3. 建立 WebSocket 连接
  4. 安全验证:JWT
  5. 界面切换与布局
  6. 后端匹配逻辑
  7. 游戏地图同步
  8. 总结
  • 💰 8折买阿里云服务器限时8折了解详情
  • Magick API 一键接入全球大模型注册送1000万token查看
  • 🤖 一键搭建Deepseek满血版了解详情
  • 一键打造专属AI 智能体了解详情
极客日志微信公众号二维码

微信扫一扫,关注极客日志

微信公众号「极客日志V2」,在微信中扫描左侧二维码关注。展示文案:极客日志V2 zeeklog

更多推荐文章

查看全部
  • Eino 组件核心篇:Retriever 在 RAG 中的检索应用
  • 开箱即用的 AI 写作工具:蛙蛙写作 AI 体验
  • Python 装饰器的 10 个应用场景
  • Python 代码运行方式及常见问题解决
  • 现代 C++ 编程核心基础与高级特性指南
  • C++ 多线程同步实战:互斥锁(mutex)详解
  • 前缀和算法:和为 K 的子数组与和可被 K 整除的子数组
  • Python 输入与输出格式化详解
  • Ubuntu 系统安装 OpenClaw 并接入飞书机器人
  • 2026年AI漫剧工具排行榜:11款软件横向对比
  • 企业落地大模型的十大挑战与行动指南
  • 深入理解网络 IP 协议与 TTL 机制:从原理到实践
  • TS3AudioBot 完整指南:从零打造 TeamSpeak 音乐机器人
  • 从人工运维到 ChatOps:大模型驱动运维自动化变革
  • 基于 AIStarter 一键安装 ComfyUI 黎黎原上咩 7.0 整合包
  • 深入理解 SPA:单页面应用的优缺点
  • JavaScript 事件循环(Event Loop)
  • 文心大模型 4.5 开源:解锁 AI 从封闭到开放的势能
  • Office Copilot 区域限制问题的排查与修复方案
  • DeepSeek 深度使用指南:提示词技巧与本地知识库搭建

相关免费在线工具

  • Keycode 信息

    查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online

  • Escape 与 Native 编解码

    JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online

  • JavaScript / HTML 格式化

    使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online

  • JavaScript 压缩与混淆

    Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online

  • 加密/解密文本

    使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online

  • Gemini 图片去水印

    基于开源反向 Alpha 混合算法去除 Gemini/Nano Banana 图片水印,支持批量处理与下载。 在线工具,Gemini 图片去水印在线工具,online