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

Spring Boot 实战:基于 WebSocket 的前后端实时匹配系统实现

综述由AI生成本方案基于 Spring Boot 与 Vue.js 构建实时游戏匹配系统。前端使用 Vuex 管理状态并通过 WebSocket 保持长连接,后端采用 JWT 进行身份验证确保安全性。核心逻辑包含用户匹配池管理、动态地图生成及前后端状态同步。通过优化连接建立与消息处理流程,实现了低延迟的双向通信与流畅的界面切换体验。

剑仙发布于 2026/3/23更新于 2026/6/216 浏览
Spring Boot 实战:基于 WebSocket 的前后端实时匹配系统实现

Spring Boot 实战:基于 WebSocket 的前后端实时匹配系统实现

在现代 Web 开发中,前端和后端的协作至关重要,特别是在需要实时交互和数据更新的应用场景。WebSocket 作为一种全双工通信协议,让前后端的数据传输更高效稳定。本文将探讨如何设计和实现一个实时匹配系统,前端负责展示界面与交互,后端通过 WebSocket 处理数据通信。

前端初始化与状态管理

在 Vue 3 项目中,我们需要先搭建好 Vuex 状态管理来维护 WebSocket 连接和用户信息。这里我们定义了一个 pk 模块,用来存储连接对象、对手信息及当前游戏状态(匹配中或对战中)。

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 中引入这个模块: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>

安全性升级:JWT 验证

直接用 userId 建立连接存在安全风险,用户可能伪装成任意 ID。因此我们需要改用 JWT token 进行身份验证。

前端连接 URL 修改为:

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
public void onOpen(Session session, @PathParam("token") String token) throws IOException {
    this.session = session;
    System.out.println("connected to websocket");
    
    // 尝试解析 token 获取用户 ID
    Integer userId = JwtAuthentication.getUserId(token);
    User user = userMapper.selectById(userId);
    
    if (user != null) {
        users.put(userId, this);
        this.user = user;
    } else {
        // 验证失败直接断开
        this.session.close();
    }
}

匹配界面与 UI 切换

我们需要根据状态动态切换匹配页面和对战页面。使用 Bootstrap 栅格布局将头像和用户名分为左右两列,中间放置操作按钮。

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 > 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 或 stop-matching 事件给后端。后端收到消息后,需要处理匹配池逻辑。

后端匹配逻辑与线程安全

后端核心在于维护一个匹配池,当池中有两名及以上用户时,自动配对并生成游戏地图。

consumer/WebSocketServer.java

@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;
        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 存储匹配池,确保多线程环境下的数据安全。发送消息时使用 synchronized 锁保证会话的线程安全。

游戏地图生成算法

匹配成功后,双方需要同步游戏地图。我们在后端创建一个 Game 类,使用深度优先搜索(DFS)算法生成带有边界和内部随机墙壁的连通地图。

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

前端接收到地图数据后,更新 Vuex 状态并切换到对战界面。

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 = () => {
        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>

总结

本方案展示了如何使用 WebSocket 构建实时匹配系统。前端利用 Vuex 管理全局状态,配合 Vue 生命周期钩子处理连接;后端通过 Java 实现高并发安全的匹配池与地图生成。关键在于前后端状态的同步以及连接的生命周期管理,确保用户体验流畅且数据准确。

目录

  1. Spring Boot 实战:基于 WebSocket 的前后端实时匹配系统实现
  2. 前端初始化与状态管理
  3. 安全性升级:JWT 验证
  4. 匹配界面与 UI 切换
  5. 后端匹配逻辑与线程安全
  6. 游戏地图生成算法
  7. 总结
  • 💰 8折买阿里云服务器限时8折了解详情
  • Magick API 一键接入全球大模型注册送1000万token查看
  • 🤖 一键搭建Deepseek满血版了解详情
  • 一键打造专属AI 智能体了解详情
极客日志微信公众号二维码

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

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

更多推荐文章

查看全部
  • MCP 插件实战:以 browser-tools-mcp 为例配置浏览器调试工具
  • CCF-GESP 一级 C++ 真题解析:手机电量显示
  • VS Code 前端开发常用插件推荐与配置指南
  • 模拟算法基础:核心概念与典型案例分析
  • Python 科赫雪花绘制:数学原理与 turtle 实现
  • ChatGPT 保护指令:提升 GPTs 提示词与知识库文件安全性
  • AI×低代码×工程化:Oinone Pamirs 下一代产品化引擎实践
  • 2026 年 3 月全球 AI 前沿动态与行业洞察
  • 命令行大模型上下文协议(MCP)工具 MCPHost 使用实践
  • AI 正在重写人类能力结构:可得性时代的机遇与风险
  • OpenClaw 在 Manjaro 上的部署与使用指南
  • VSCode AI Copilot 自定义指令配置实战指南
  • OpenClaw 新手入门:环境搭建、模型配置与 WebUI 远程访问
  • AI 剧透功能创意:初级开发者的反压制生存指南
  • Cookie 与 Session:Web 用户状态管理的双刃剑
  • 智能家居插件管理工具技术指南:突破网络限制的本地化优化方案
  • PyTorch 文本引导图像生成与 Stable Diffusion 实践
  • 家庭机器人落地难点分析:技术、成本与隐私
  • LLaMA-Factory 与 HuggingFace Transformers 无缝对接及扩展性分析
  • voidImageViewer:支持 GIF 与 WebP 的轻量级 Windows 看图工具

相关免费在线工具

  • 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