P4. 微服务: 匹配系统(上)

news2025/1/15 23:39:30

P4. 微服务: 匹配系统 上

    • Tips
    • 0 概述
    • 1 匹配系统流程
    • 2 游戏系统流程
    • 3 websocket 前后端通信的基础配置
      • 3.1 websocket 的需要的配置
      • 3.2 websocket 连接的建立
      • 3.3 为 websocket 连接添加 jwt 验证
    • 4 实现匹配界面和对战界面的切换
    • 5 匹配系统的客户端和 websocket 后端交互部分
      • 5.1 明确业务逻辑过程
      • 5.2 前端通过 socket 向后端发送消息
      • 5.3 后端通过 socket 向前端返回结果
    • 6 解决匹配系统其他问题
      • 6.1 页面切换判断问题
      • 6.2 地图同步问题
    • 7 拓展
      • 7.1 聊天功能

Tips

  • 做任何一个业务,先分析整体的流程,再想怎么用代码实现各部分。
  • 对于类似匹配系统这种通信复杂的,最好把系统画出来明确一下。


0 概述

  • 观前须知: 整个匹配系统比较复杂,因此分上下章阐述,本章尚未涉及到微服务,只是简单的设计并实现了匹配系统,未考虑到多并发,线程等问题,在下章中会进行改进,开一个微服务进行实现。
  • 本章首先介绍了匹配系统和游戏系统的整个流程,需要明确为什么匹配系统要用微服务
  • 另外,本章的关键点在于理解为什么匹配系统要用 websocket 协议,websocket 协议的原理是什么,如何使用 websocket 实现通信,前后端分别如何建立 websocket 连接,前端如何向后端发送消息,后端如何向前端发送消息。
  • 在学习完成后思考一下该怎么通过 websocket 来实现一个聊天对话功能


1 匹配系统流程

在这里插入图片描述

整个匹配流程如上图所示,匹配系统实际上就是用户的集合,是类似于 MySQL 的单独的程序(微服务)。

(1) 客户端先发送匹配请求给后端

(2) 后端把每个用户信息发送给匹配系统 (把用户扔到匹配池)

(3) 匹配系统根据匹配规则将用户进行匹配,有匹配结果 {user1, user2} 之后立刻返回给后端

(4) 后端根据匹配结果中的 {user1, user2} 根据每个用户对应的 socket 连接向客户端返回匹配成功结果

在介绍完匹配系统的流程后,分析一下以下几个问题:

Q1. 什么时候用微服务?

微服务可以理解成一个额外的程序,实现某个逻辑比较独立的功能。

可以发现,整个匹配流程是异步的,也就是在用户发送匹配请求之后,不知道要过多久才有结果,等待时间未知。

当面对异步计算量大的操作时,需要维护额外的服务进行操作。

Q2. 为什么用后端用 websocket 协议?

传统的 http 协议的特点是一问一答,中间返回过程的时间很短,像上一节中 botCRUD 操作就是传统的 http

而匹配系统的特点是发送请求后不知道过多长时间才有结果,同时也可能返回多次结果,因此不能用 http 协议,

websocket 协议的特点是客户端和服务端都可以主动发送请求(全双工,两边对称),因此后端采用 websocket 协议。

介绍一下 websocket 的基本原理:

每一个前端建立的连接都会在后端进行维护,维护的实际上是一个 WebSocketServer 类的实例,每一个连接都开一个线程维护(多线程并发)。每一个连接的独有信息,比如匹配的用户可以用 private 存下来,而对于所有连接共有的信息,比如匹配池的用户,可以用 static 静态变量存起来。

简单来说就是每来一个连接就开一个线程,每一个线程 new 一个 WebSocketServer 实例来维护这个连接。



2 游戏系统流程

在这里插入图片描述

在P1.创建菜单与游戏界面中介绍的游戏都是在本地端实现的,然而对于匹配到的对局需要相同的地图,并且不能把裁判逻辑等放在前端,方便外挂出现。因此需要在后端实现一个 Game 维护整个游戏地图生成和裁判逻辑等。

对于回合制游戏大多把裁判逻辑放在后端,但对于 fps 游戏等需要大量实时返回的游戏会把部分逻辑放在前端,否则延迟太高。

(1) 创建游戏地图,并且返回给对局的两个用户 client1, client2 (本章6.2节实现的部分)

(2) 等待两个玩家都输入下一步操作(可以客户端手动输入,也可以通过执行 Bot 代码的微服务发送结果),如果长时间未获得输入,则判定未输入操作的玩家超时直接判输,否则传给裁判函数进行判断

(3) 判断新局面的情况是否合法,如果有不合法的直接判输赢,合法则继续下一回合直到分出胜负



3 websocket 前后端通信的基础配置

3.1 websocket 的需要的配置

  • 首先要安装2个依赖 spring-boot-starter-websocket, fastjson (前后端以 json 格式通信)。

  • 再创建 config.WebSocketConfig 配置类,启用 WebSocket 支持。

@Configuration
public class WebSocketConfig {
    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }
}
  • config.SecurityConfig 配置中添加如下函数,放行 websocket 连接。
@Override
public void configure(WebSecurity web) throws Exception {
    web.ignoring().antMatchers("/websocket/**");
}

3.2 websocket 连接的建立

  • 添加 consumer.WebSocketConfig 类,实现后端 websocket 连接相关功能。

首先说明一下几个函数的作用:

onOpen: 在创建 websocket 连接时触发,获取当前连接对应的 user 并且放到 users 中,users 是用于通过 userId 找到对应的连接,这样在匹配成功时可以找到用户对应的连接。

onClose: 在关闭连接时触发,把 userusers 中移除。

onMessage: 后端接收到前端消息时触发。

sendMessage: 后端向当前连接发送消息。

websocket 连接中,每个连接通过一个 Session 对象来维护。sendMessage 是一个异步通信过程,需要加一个锁维护。

ConCurrentHashMap 是一个线程安全的哈希表,把 userId 映射到 WebSocketServer 实例。

WebSocketServer 中注入 userMapper 需要 setUserMapper 特殊注入,和之前的不同。

@Component
@ServerEndpoint("/websocket/{token}")  // 注意不要以'/'结尾
public class WebSocketServer {

    private Session session = null;
    private User user;
    private static ConcurrentHashMap<Integer, WebSocketServer> users = new ConcurrentHashMap<>();

    private static UserMapper userMapper;

    @Autowired
    private void setUserMapper(UserMapper userMapper) {
        WebSocketServer.userMapper = userMapper;
    }

    @OnOpen
    public void onOpen(Session session, @PathParam("token") String token) {
        this.session = session;
        System.out.println("connected!");
        Integer userId = Integer.parseInt(token);
        this.user = userMapper.selectById(userId);

        if (this.user != null) {
            users.put(userId, this);
        } else {
            this.session.close();
        }
    }

    @OnClose
    public void onClose() {
        System.out.println("disconnected!");
        if (this.user != null) {
            users.remove(this.user.getId());
        }
    }

    @OnMessage
    public void onMessage(String message, Session session) {
        System.out.println("received!");
    }

    @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();
            }
        }
    }
}
  • 在前端进行调试,实现前端 websocket 连接建立。

    前端建立 websocket 是通过 socketUrljs 内置的 WebSocket 类来实例化 WebSocket 对象实现,该对象包含的函数和后端 websocket 包含的类似。

    onMounted 是指组件挂载时触发的函数,可以理解成页面加载完成后触发,简单来说就是在 pk 页面加载完成后建立一个 websocket 连接,通过 socketUrl 和后端连接起来。

export default {
    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(() => {
            socket.close();
        });
    }
}

3.3 为 websocket 连接添加 jwt 验证

之前实现的 socketUrl 是直接传用户的 id,显然这样很不安全,前端只要更改 socketUrl 就可以用别人的身份进行对局,因此需要把 id 改成 token 进行 jwt 验证。

前端只需要修改 socketUrl,后端需要从 token 中解析出 userId

consumer.utils.JwtAuthentication

public class JwtAuthentication {
    public static Integer getUserId(String token) {
        int userId = -1;
        try {
            Claims claims = JwtUtil.parseJWT(token);
            userId = Integer.parseInt(claims.getSubject());
        } catch (Exception e) {
            throw new RuntimeException(e);
        }

        return userId;
    }
}

consumer.WebSocketServer

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


4 实现匹配界面和对战界面的切换

  • 首先模仿 user.js 创建 pk.js 包含所有 pk 页面所需的全局变量 status, opponent_username, opponent_photo, socket,其中 status 表示当前是匹配界面还是对战界面。

  • pk 页面通过 v-if="$store.state.pk.status === 'xxx'" 来实现界面切换。

    <template>
        <PlayGround v-if="$store.state.pk.status === 'playing'" />
        <MatchGround v-if="$store.state.pk.status === 'matching'" />
    </template>
    
  • 自行设计 MatchGround 页面内容,需要提供匹配按钮,让用户进行匹配。



5 匹配系统的客户端和 websocket 后端交互部分

5.1 明确业务逻辑过程

在这里插入图片描述

用户在点击匹配按钮之后,(1)向 websocket 后端发送一个请求,(2)后端接收到请求之后把用户放到匹配池之中,(3)在匹配池匹配到两个用户之后将结果给后端,(4)最后返回结果给用户。在用户点击取消匹配之后,应该移出匹配池。

可以发现以上的过程涉及以下几个问题:

  • 前端如何通过 websocket 连接发送消息给后端,发送消息的格式是什么,后端又如何返回结果给前端
  • 如何区分匹配操作和取消操作

5.2 前端通过 socket 向后端发送消息

前端点击按钮之后通过 socket.send() 向后端发送消息,格式为 JSON 格式,通过设置 event 域来区分匹配和取消操作。

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",
        }));
    }
}

后端在 onMessage() 函数中接收到消息,将前端发送回来的 JSON 格式信息进行解析,根据 event 判断接下来的操作,可以发现通常是把 onMessage 当做路由来使用。

先用内存存储匹配池,后面用到微服务再改,这边用的是线程安全的容器。

这边有个常用的小细节,在判断字符串相等的时候通常是 "str".equals(var) 的格式,避免出错。

private static CopyOnWriteArrayList<User> matchpool = new CopyOnWriteArrayList<>();

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

private void stopMatching() {
    System.out.println("Stop Matching!");
    matchpool.remove(this.user);
}

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

5.3 后端通过 socket 向前端返回结果

先写一个傻瓜式匹配规则,也不考虑并发等问题,因为后面改成微服务还会改,这边只是调试一下用的。

每当匹配池有两个用户可以匹配则进行匹配,结果返回给前端是先通过之前定义的 users 找到匹配用户的 socket 连接,再通过连接调用 sendMessage 向前端发送消息。

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

        JSONObject respA = new JSONObject();
        respA.put("event", "match_success");
        respA.put("opponent_username", b.getUsername());
        respA.put("opponent_photo", b.getPhoto());
        users.get(a.getId()).sendMessage(respA.toJSONString());

        JSONObject respB = new JSONObject();
        respB.put("event", "match_success");
        respB.put("opponent_username", a.getUsername());
        respB.put("opponent_photo", a.getPhoto());
        users.get(b.getId()).sendMessage(respB.toJSONString());
    }
}

前端同样地,在 onmessage 中接收后端返回过来的结果。

PkIndexView.vue

socket.onmessage = msg => {
    const data = JSON.parse(msg.data);
    console.log(data);
    if (data.event === "match_success") {
        store.commit("UpdateOpponent", {
            username: data.opponent_username,
            photo: data.opponent_photo,
        });
        setTimeout(() => {
            store.commit("UpdateStatus", "playing");
        }, 2000);
    }
}


6 解决匹配系统其他问题

6.1 页面切换判断问题

在用户匹配成功后,切换到其他页面应该判定为自动放弃,再回到匹配页面。

onUnmounted(() => {
    socket.close();
    store.commit("UpdateStatus", "matching");
});

6.2 地图同步问题

当两个用户匹配成功之后,由于地图生成逻辑是放在前端生成的,因此两名玩家的地图是不同的,需要解决这个问题。

解决方法是将地图生成的逻辑放到后端统一生成,在 consumer.utils.Game 实现 Game 类统一管理游戏流程。

地图生成的逻辑在P1.创建菜单与游戏界面中介绍,这边只要翻译成 Java 的就行。

在匹配成功之后,将地图生成并返回给前端:

private void startMatching() {
    /* ... */
    while (matchpool.size() >= 2) {
        Game game = new Game(13, 14, 20);
        game.createMap();

        JSONObject respA = new JSONObject();
        respA.put("gamemap", game.getG());
        users.get(a.getId()).sendMessage(respA.toJSONString());
    }
}

之后在前端将 gamemap 存到全局变量中,并且使用该变量在 gamemap.js 中渲染出来地图。



7 拓展

7.1 聊天功能

思考一下如果希望添加对话框的聊天功能该如何实现?

聊天功能就是用户A发送消息 content,用户B接收到 content

在匹配过程中我们已经学习过 websocket 的具体使用方法: (1) 客户端向后端发送消息(2) 后端向客户端发送消息

因此可以用户A首先向后端发送消息,event 可以设置为 send_message,再添加 content 域记录发送的消息,后端接收到 message 后根据对手用户B的 id 找到对应的 socket 之后发送给用户B的客户端 message 即可。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1813047.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

启明智显工业级HMI芯片Model3功耗特性分享

前言&#xff1a; 【启明智显】专注于HMI&#xff08;人机交互&#xff09;及AIoT&#xff08;人工智能物联网&#xff09;产品和解决方案的提供商&#xff0c;我们深知彩屏显示方案在现代物联网应用中的重要性。为此&#xff0c;我们一直致力于为客户提供彩屏显示方案相关的技…

MySQL系列-语法说明以及基本操作(一)

1、前言 主要讲解MySQL的基本语法 官网文档 https://docs.oracle.com/en-us/iaas/mysql-database/doc/getting-started.html 关于MySQL的基本语法&#xff0c;关于数据类型、表的操作、数据操作、事务、备份等&#xff0c;可参考 http://www.voidme.com/mysql 2、数据类型 数…

ARM32开发--PWM高级定时器

目录 文章目录 前言 目标 学习内容 需求 高级定时器通道互补输出 开发流程 通道配置 打开互补保护电路 完整代码 练习题 总结 前言 在嵌入式软件开发中&#xff0c;PWM&#xff08;脉冲宽度调制&#xff09;技术被广泛应用于控制各种电子设备的亮度、速度等参数。…

STM32驱动带编码器的直流减速电机

STM32驱动带编码器的直流减速电机 文章目录 STM32驱动带编码器的直流减速电机硬件材料MG310 直流减速电机直流电机原理减速器实物接线霍尔编码器 TB6612电机驱动接线说明程序设计定时器生成PWM驱动电机速度计算 实验结果程序下载 硬件材料 主控板&#xff1a;STM32F407ZG直流减…

【漏洞复现】WVP视频平台未授权漏洞

漏洞描述&#xff1a; WVP视频平台api/user存在未授权访问漏洞&#xff0c;攻击者可利用漏洞获取当前系统管理员用户名及密码进行登录系统。 搜索语法: Fofa-Query: body"国标28181" 漏洞详情&#xff1a; 1.WVP视频平台。 2.POC: GET /api/user/all HTTP/1.1 …

Redis项目中竟然有这么多种使用场景!!

下面蜗牛哥依次对Redis 15种业务场景举例说明和解释&#xff1a; 1. 排行榜和计数器 针对Redis作为排行榜和计数器的使用场景&#xff0c;下面是一个Java Spring Boot应用的案例&#xff0c;其中使用Redis来实现一个简单的文章点赞功能&#xff0c;并将点赞数用作排行榜的依据…

Django UpdateView视图

UpdateView是Django中的一个通用视图&#xff0c;用于处理对象的更新操作。它允许用户更新一个已经存在的对象。UpdateView通常与一个模型表单一起使用&#xff0c;这样用户就可以看到当前对象的值&#xff0c;并可以修改它们。 1&#xff0c;添加视图 Test/app3/views.py fr…

什么是覆盖索引 ?

走当前索引就足够&#xff0c;而无需回表就能找到所有数据&#xff0c;就叫覆盖索引。 比如 key1 上有索引。&#xff08;它是一个普通的二级索引&#xff09;。 那么select key1 from s1 where key1 a 这种就叫覆盖索引。 表现就是explain时&#xff0c; Extra 那里显示 …

鸿蒙开发文件管理:【@ohos.fileio (文件管理)】

文件管理 该模块提供文件存储管理能力&#xff0c;包括文件基本管理、文件目录管理、文件信息统计、文件流式读写等常用功能。 说明&#xff1a; 本模块首批接口从API version 6开始支持。后续版本的新增接口&#xff0c;采用上角标单独标记接口的起始版本。 导入模块 impor…

AI创意广告案例分析️可口可乐、麦当劳、伊利、钟薛高等一线品牌各显神通

AI 相关的教学我们最近做了不少分享&#xff0c;本期计育韬老师则希望和广告人们谈一谈具体的落地案例应用及其内在创意方法论。结合在 Midjourney 频道 LV.14 的部分创作经验&#xff0c;相信能为广大品牌方带来 AI 广告创作的启迪。 担心版权争议&#xff1f; 那就主打 UG…

分离轴定理:凸多边形相交检测算法

分离轴定理&#xff08;Seperating Axis Theorem&#xff09;是一种确定两个凸多边形是否相交的方法。该算法还可用于查找最小穿透向量&#xff0c;这对于物理模拟和许多其他应用非常有用。SAT 是一种快速通用算法&#xff0c;可以消除对每个形状类型对进行碰撞检测代码的需求&…

在Vue2和Vue3中ECharts如何使用,详细步骤,ref,$ref。echarts官网。

不管是在vue2中还是在vue3中其实本质上的原理和步骤都是一样的&#xff0c;只是语法可能有所不同。 为了方便大家看起乱&#xff0c;vue2和vue3我分开写。 echarts官网&#xff1a; https://echarts.apache.org/zh/index.html Vue2篇&#xff1a; 1.导入echarts包&#xf…

LaDM3IL:多实例学习用于免疫库分类

一个人的免疫组库由某一时间点的大量适应性免疫受体组成&#xff0c;代表了该个体的适应性免疫状态。免疫组库分类和相关受体识别有可能为新型疫苗的开发做出贡献。大量的实例对免疫组库分类提出了挑战&#xff0c;这可以表述为大规模多实例学习 (MMIL&#xff0c;Massive Mult…

AI实时免费在线图片工具4:WordArt艺术字生成;IC-Light打光模型;screenshot to code图像直接生成网页

1、艺术字生成WordArt https://modelscope.cn/studios/WordArt/WordArt/summary?reftop.aibase.com 2、打光模型IC-Light https://huggingface.co/spaces/lllyasviel/IC-Light Screenshot to Code图像直接生成网页 https://huggingface.co/spaces/HuggingFaceM4/screen…

Ubuntu,Linux服务器安装Mellanox MCX653105A IB网卡HCA卡驱动

驱动下载地址 https://network.nvidia.com/products/infiniband-drivers/linux/mlnx_ofed/ 选择对应操作系统 进入目录运行 安装成功显示 如果中途报错&#xff0c;需要核对下载的版本&#xff0c;并且把原来安装的卸载

钡铼BL102优化生产流程PLC-MQTT网关快速转换

在智能制造和工业4.0的推动下&#xff0c;优化生产流程、提升自动化水平已成为现代制造业的核心诉求。作为这一进程的关键环节&#xff0c;将传统的PLC控制系统与先进的物联网技术相结合&#xff0c;实现数据的高效采集与远程管理变得尤为重要。钡铼BL102作为一款专为优化生产流…

Jsch上传本地目录文件到服务器

文章目录 1.Jsch简介1.1 什么是Jsch1.2 Jsch使用步骤和简单示例 2.技术关键点3.Jsch实战3.1 maven依赖3.2 功能实现3.3 效果3.4 封装工具类 4.总结 摘要: 在一些框架开发工作中&#xff0c;需要为项目使用说明文档&#xff0c;来指导用户如何正确使用框架。比如通过markdown编写…

易舟云:让中小企业财务管理变得触手可及

在现代企业运营中&#xff0c;财务管理的精准和高效至关重要。对于中小企业来说&#xff0c;一款专业且易于使用的财务软件更是必不可少的工具。今天&#xff0c;我们就来详细了解一款深受中小企业喜爱的财务软件——易舟云。 财务管理的云端革命 随着云技术的发展&#xff0c;…

EVA-CLIP:在规模上改进CLIP的训练技术

摘要 对比性语言-图像预训练&#xff0c;简称CLIP&#xff0c;因其在各种场景中的潜力而备受关注。在本文中&#xff0c;我们提出了EVA-CLIP&#xff0c;一系列模型&#xff0c;这些模型显著提高了CLIP训练的效率和有效性。我们的方法结合了新的表示学习、优化和增强技术&…

Phaser-圆形路径

使用 Phaser 创建一个简单的路径动画 Phaser 是一个强大的 HTML5 游戏框架&#xff0c;适合用于开发 2D 游戏。在本文中&#xff0c;我们将展示如何使用 Phaser 创建一个简单的动画示例&#xff0c;其中一个红色的圆沿着椭圆路径移动。该示例将帮助你理解如何在 Phaser 中使用…