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

2024-06-12 02:52
文章标签 系统 服务 匹配 p4

本文主要是介绍P4. 微服务: 匹配系统(上),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

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 {@Beanpublic 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;@Autowiredprivate void setUserMapper(UserMapper userMapper) {WebSocketServer.userMapper = userMapper;}@OnOpenpublic 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();}}@OnClosepublic void onClose() {System.out.println("disconnected!");if (this.user != null) {users.remove(this.user.getId());}}@OnMessagepublic void onMessage(String message, Session session) {System.out.println("received!");}@OnErrorpublic 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 即可。

这篇关于P4. 微服务: 匹配系统(上)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1052999

相关文章

不懂推荐算法也能设计推荐系统

本文以商业化应用推荐为例,告诉我们不懂推荐算法的产品,也能从产品侧出发, 设计出一款不错的推荐系统。 相信很多新手产品,看到算法二字,多是懵圈的。 什么排序算法、最短路径等都是相对传统的算法(注:传统是指科班出身的产品都会接触过)。但对于推荐算法,多数产品对着网上搜到的资源,都会无从下手。特别当某些推荐算法 和 “AI”扯上关系后,更是加大了理解的难度。 但,不了解推荐算法,就无法做推荐系

基于人工智能的图像分类系统

目录 引言项目背景环境准备 硬件要求软件安装与配置系统设计 系统架构关键技术代码示例 数据预处理模型训练模型预测应用场景结论 1. 引言 图像分类是计算机视觉中的一个重要任务,目标是自动识别图像中的对象类别。通过卷积神经网络(CNN)等深度学习技术,我们可以构建高效的图像分类系统,广泛应用于自动驾驶、医疗影像诊断、监控分析等领域。本文将介绍如何构建一个基于人工智能的图像分类系统,包括环境

水位雨量在线监测系统概述及应用介绍

在当今社会,随着科技的飞速发展,各种智能监测系统已成为保障公共安全、促进资源管理和环境保护的重要工具。其中,水位雨量在线监测系统作为自然灾害预警、水资源管理及水利工程运行的关键技术,其重要性不言而喻。 一、水位雨量在线监测系统的基本原理 水位雨量在线监测系统主要由数据采集单元、数据传输网络、数据处理中心及用户终端四大部分构成,形成了一个完整的闭环系统。 数据采集单元:这是系统的“眼睛”,

嵌入式QT开发:构建高效智能的嵌入式系统

摘要: 本文深入探讨了嵌入式 QT 相关的各个方面。从 QT 框架的基础架构和核心概念出发,详细阐述了其在嵌入式环境中的优势与特点。文中分析了嵌入式 QT 的开发环境搭建过程,包括交叉编译工具链的配置等关键步骤。进一步探讨了嵌入式 QT 的界面设计与开发,涵盖了从基本控件的使用到复杂界面布局的构建。同时也深入研究了信号与槽机制在嵌入式系统中的应用,以及嵌入式 QT 与硬件设备的交互,包括输入输出设

JAVA智听未来一站式有声阅读平台听书系统小程序源码

智听未来,一站式有声阅读平台听书系统 🌟&nbsp;开篇:遇见未来,从“智听”开始 在这个快节奏的时代,你是否渴望在忙碌的间隙,找到一片属于自己的宁静角落?是否梦想着能随时随地,沉浸在知识的海洋,或是故事的奇幻世界里?今天,就让我带你一起探索“智听未来”——这一站式有声阅读平台听书系统,它正悄悄改变着我们的阅读方式,让未来触手可及! 📚&nbsp;第一站:海量资源,应有尽有 走进“智听

【Prometheus】PromQL向量匹配实现不同标签的向量数据进行运算

✨✨ 欢迎大家来到景天科技苑✨✨ 🎈🎈 养成好习惯,先赞后看哦~🎈🎈 🏆 作者简介:景天科技苑 🏆《头衔》:大厂架构师,华为云开发者社区专家博主,阿里云开发者社区专家博主,CSDN全栈领域优质创作者,掘金优秀博主,51CTO博客专家等。 🏆《博客》:Python全栈,前后端开发,小程序开发,人工智能,js逆向,App逆向,网络系统安全,数据分析,Django,fastapi

【区块链 + 人才服务】可信教育区块链治理系统 | FISCO BCOS应用案例

伴随着区块链技术的不断完善,其在教育信息化中的应用也在持续发展。利用区块链数据共识、不可篡改的特性, 将与教育相关的数据要素在区块链上进行存证确权,在确保数据可信的前提下,促进教育的公平、透明、开放,为教育教学质量提升赋能,实现教育数据的安全共享、高等教育体系的智慧治理。 可信教育区块链治理系统的顶层治理架构由教育部、高校、企业、学生等多方角色共同参与建设、维护,支撑教育资源共享、教学质量评估、

软考系统规划与管理师考试证书含金量高吗?

2024年软考系统规划与管理师考试报名时间节点: 报名时间:2024年上半年软考将于3月中旬陆续开始报名 考试时间:上半年5月25日到28日,下半年11月9日到12日 分数线:所有科目成绩均须达到45分以上(包括45分)方可通过考试 成绩查询:可在“中国计算机技术职业资格网”上查询软考成绩 出成绩时间:预计在11月左右 证书领取时间:一般在考试成绩公布后3~4个月,各地领取时间有所不同

系统架构师考试学习笔记第三篇——架构设计高级知识(20)通信系统架构设计理论与实践

本章知识考点:         第20课时主要学习通信系统架构设计的理论和工作中的实践。根据新版考试大纲,本课时知识点会涉及案例分析题(25分),而在历年考试中,案例题对该部分内容的考查并不多,虽在综合知识选择题目中经常考查,但分值也不高。本课时内容侧重于对知识点的记忆和理解,按照以往的出题规律,通信系统架构设计基础知识点多来源于教材内的基础网络设备、网络架构和教材外最新时事热点技术。本课时知识

计算机毕业设计 大学志愿填报系统 Java+SpringBoot+Vue 前后端分离 文档报告 代码讲解 安装调试

🍊作者:计算机编程-吉哥 🍊简介:专业从事JavaWeb程序开发,微信小程序开发,定制化项目、 源码、代码讲解、文档撰写、ppt制作。做自己喜欢的事,生活就是快乐的。 🍊心愿:点赞 👍 收藏 ⭐评论 📝 🍅 文末获取源码联系 👇🏻 精彩专栏推荐订阅 👇🏻 不然下次找不到哟~Java毕业设计项目~热门选题推荐《1000套》 目录 1.技术选型 2.开发工具 3.功能