第六章--- 实现微服务:匹配系统(下)

2023-12-19

0.写在前面

这一章终于完了,但是收尾工作真的好难呀QAQ,可能是我初学的缘故,有些JAVA方面的特性不是很清楚,只能依葫芦画瓢地模仿着用。特别是JAVA的注解,感觉好多但又不是很懂其中的原理,只知道要在某个时候用某个注解,我真是有够菜的()

以我拙见,JAVA注解大概分为两类

  1. 一类是使用Bean,即是把已经在xml文件中配置好的Bean拿来用,完成属性、方法的组装;比如@Autowired , @Resource,可以通过byTYPE(@Autowired)、byNAME(@Resource)的方式获取Bean;

  2. 一类是注册Bean,@Component , @Repository , @ Controller , @Service , @Configration这些注解都是把你要实例化的对象转化成一个Bean,放在IoC容器中,等你要用的时候,它会和上面的@Autowired , @Resource配合到一起,把对象、属性、方法完美组装。

我感觉注册类的功能都是差不多的,可能只是由于写程序的时候业务逻辑的不同,而把它定义为不同的名字(这里我不太了解,可能说的不太严谨)。
具体业务逻辑大致可以归类如下:

  • @controller :标注控制层,也可以理解为接收请求处理请求的类。

  • @service :标注服务层,也就是内部逻辑处理层。

  • @repository :标注数据访问层,也就是用于数据获取访问的类(组件)。

  • @component 其他不属于以上三类的类,但是会同样注入spring容器以被获取使用。 它的作用就是实现bean的注入

  • @AutoWired 就是在你声明了注册类后,可以用该注解注入进当前写的类中。

凡是子类及带属性、方法的类都注册Bean到Spring中,交给它管理; @Bean 用在方法上,告诉Spring容器,你可以从下面这个方法中拿到一个Bean。调用的时候和 @Component 一样,用 @Autowired 调用有 @Bean 注解的方法,多用于第三方类无法写 @Component 的情况。

1.微服务实现匹配系统

根据上一part的设计逻辑,我们可以用微服务去代替之前调试用的匹配系统,使匹配系统功能更加完善。
微服务 :是一个独立的程序,可以认为是另起了一个新的springboot。
我们把这个新的springboot叫做 Matching System 作为我们的匹配系统,与之对应的是 Matching Server ,即匹配的服务器后端。

当我们之前的springboot也就是游戏对战的服务器后端 backend Server 获取了两个匹配的玩家信息后,会向 Matching Server 服务器后端发送一个 http 请求,而当 Matching Server 接收到了请求后,会开一个独立的线程 Matching 开始进行玩家匹配。
匹配逻辑也非常简单,即每隔1s会扫描当前已有的所有玩家,判断当前玩家的 rating 是否相近,能否匹配出来,若能匹配出来则将结果返回给 backend Server (通过 http 返回)

实现手法: Spring Cloud

2.创建 backendcloud

我们项目的结构会出现变化,要先创建一个新的springboot项目 backendcloud 作为父项目,包含两个并列的子项目 Matching System backend

注意: backendcloud 创建时要引入 Spring Web 依赖,不然的话后面自己要在 pom.xml 里手动添加!

因为父级项目是不用写逻辑的,可以把他的整个src文件删掉。

配置 pom.xml

<packaging>pom</packaging>

加上 Spring Cloud 依赖

 <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-dependencies</artifactId>
            <version>2021.0.3</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>

backendcloud 项目文件夹下创建两个 模块 MatchingSystem , backend ,相当于两个并列的springboot项目。

3.Matching System

配置 pom.xml
将父项目里的 spring web 依赖转移到 Matching System pom.xml

配置端口
resources 文件夹里创建文件 application.properties

server.port = 3001

这样 Matching System 的端口就是 3001

匹配服务的实现
和之前写的业务逻辑一样,先写个匹配的服务接口MatchingService,然后在 Impl 里实现对应的接口
这里提供参考逻辑:
matchingsystem\service\impl\MatchingServiceImpl.java

@Service

public class MatchingServiceImpl implements MatchingService {
    @Override
    public String addPlayer(Integer userId, Integer rating) {
        System.out.println("add player: " + userId + " " + rating);
        return "add player successfully";
    }

    @Override
    public String removePlayer(Integer userId) {
        System.out.println("remove player: " + userId);
        return "remove player successfully";
    }
}

实现匹配的Controller
matchingsystem\controller\MatchingController.java

@RestController
public class MatchingController {
    @Autowired
    private MatchingService matchingService;

    @PostMapping("/player/add/")
    public String addPlayer(@RequestParam MultiValueMap<String, String> data) {
        Integer userId = Integer.parseInt(Objects.requireNonNull(data.getFirst("user_id")));
        Integer rating = Integer.parseInt(Objects.requireNonNull(data.getFirst("rating")));
        return matchingService.addPlayer(userId, rating);
    }

    @PostMapping("/player/remove/")
    public String removePlayer(@RequestParam MultiValueMap<String, String> data) {
        Integer userId = Integer.parseInt(Objects.requireNonNull(data.getFirst("user_id")));
        return matchingService.removePlayer(userId);
    }
}

注意:这里用的是 MultiValueMap ,即一个键值 key 可以对应多个 value 值,一个key对应一个列表list
定义: MultiValueMap<String, String> valueMap = new LinkedMultiValueMap<>();
这里如果用 @Requestparam + map 接收所有参数的话会不严谨,因为若 url 返回的是多个参数的话, map 只能接受一个参数,即一个 value ,有时候匹配的会返回多个 rating 相近的人的结果,这时候如果用 map 接收可能会产生一些蜜汁错误,因此用 MultiValueMap 的话可以省事点。。。
用到的api:
MultiValueMap.getFirst(key) 返回对应key的 value 列表的第一个值。

设置网关
为了防止用户破坏系统,我们应该设置一定的访问权限,让自己的系统更加安全

这里可以仿照之前写过的 SecurityConfig

添加 spring security 依赖
pom.xml

  <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
            <version>2.7.1</version>
        </dependency>

配置 SecurityConfig

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
  ...
                .antMatchers("/player/add/","/player/remove/").hasIpAddress("127.0.0.1") //只允许本地访问
  ...
}

设置Matching System项目的启动入口
MatchingSystemApplication.java

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication

public class MatchingSystemApplication {
    public static void main(String[] args){
        SpringApplication.run(MatchingSystemApplication.class,args);
    }
}

4.backend

准备工作
将之前写的springboot项目 backend 引入进现在的 backendcloud

把之前 backend 里的 src 文件夹粘贴进 backendcloud 里的 backend 模块中

注意:要同时配置相应的 pom.xml

将匹配链接对接到Matching System
向后端发请求

工具:RestTemplate ,可以在两个 springboot 之间进行通信
为了将RestTemplate取出来,我们要先建立一个config类 用 @Configuration 注解
我们想取得谁就要加一个 @Bean 注解(前面有提到过)
后面如果要用到这个类的时候,就直接 @Autowired 注入进去

backend\config\RestTemplateConfig.java

@Configuration

public class RestTemplateConfig {
    @Bean
    public RestTemplate getRestTemplate() {
        return new RestTemplate();
    }
}

Spring的@Bean注解用于告诉方法,产生一个Bean对象,然后这个Bean对象交给Spring管理。 产生这个 Bean 对象的方法Spring只会调用一次,随后这个Spring将会将这个Bean对象放在自己的IOC容器中。@Bean明确地指示了一种方法,什么方法呢?产生一个 bean 的方法,并且交给Spring容器管理;从这我们就明白了为啥@Bean是放在方法的注释上了,因为它很明确地告诉被注释的方法,你给我产生一个Bean,然后交给Spring容器,剩下的你就别管了。记住,@Bean就放在方法上,就是让方法去产生一个Bean,然后交给Spring容器。
如上面 getRestTemplate() 生成了一个 RestTemplate 对象,然后这个 RestTemplate 对象交给Spring管理,后面就可以直接 @Autowired 注入这个对象了。

backend\consumer\utils\WebSocketServer.java

将之前调试用的 matchpoll 删掉
并编写新的匹配逻辑

先将上面写的 RestTemplate 类注入进来

 private static RestTemplate restTemplate;
  @Autowired
  public void setRestTemplate(RestTemplate restTemplate) {
        WebSocketServer.restTemplate = restTemplate;
  }

一些比较 感性 的理解:当你注入 @Autowired 的时候,springboot会调查相应的带有 @Configuration 的接口/类,看看是否有对应的带有 @Bean 注解的方法,若存在则调用这个函数方法,把返回值赋过来。(似乎与函数名无关,如: getRestTemplate setRestTemplate

开始匹配服务
首先要把之前的数据库也引入进现在的这个springboot项目中

    private void startMatching() {
        System.out.println("start matching!");
        //向后端发请求
        MultiValueMap<String,String> data = new LinkedMultiValueMap<>();
        data.add("user_id",this.user.getId().toString());
        data.add("rating",this.user.getRating().toString());
        restTemplate.postForObject(addPlayerUrl,data,String.class);//发送请求
        //(url,数据,返回值类型的class) 反射机制?
    }

注: restTemplate.postForObject(addPlayerUrl,data,String.class); 发送请求给Matchin System里的 MatchingController ,里面用 @RequestParam MultiValueMap<String, String> data 接收传过来的数据data。

删除匹配服务

 private void stopMatching() {
        System.out.println("stop matching!");
        MultiValueMap<String,String> data = new LinkedMultiValueMap<>();
        data.add("user_id",this.user.getId().toString());
        restTemplate.postForObject(removePlayerUrl,data,String.class);

    }

现在我们实现了浏览器向ws端(backend)发送匹配请求,ws端再发送请求给Matching System端

5.实现收到请求后的匹配具体逻辑

思路:把所有当前匹配的用户放在一个数组(matchinPool)里,每隔1s扫描一遍数组,把rating较接近的两名用户匹配在一起,随着时间的推移,两名用户允许的rating差可以不断扩大,保证了所有用户都可以匹配在一起。

Impl 文件夹里新建一个 utils 工具包,编写 MatchingPool.java Player.java 类(对应于上面的数组和用户信息)

MatchingPool.java 是一个多线程的类,要继承自 Thread

public class MatchingPool extends Thread {

    private static List<Player> players = new ArrayList<>(); //多个线程公用的,要上锁
    //这里不用线程安全的类,因为我们自己会手动加锁把不安全的变为安全的
    private final ReentrantLock lock = new ReentrantLock();

    public void addPlayer(Integer userId, Integer rating) {
        lock.lock();
        try {
            players.add(new Player(userId, rating, 0));

        } finally {
            lock.unlock();
        }

    }

    public void removePlayer(Integer userId) {
        lock.lock();
        try {
            players.removeIf(player -> player.getUserId().equals(userId));
        } finally {
            lock.unlock();
        }

    }

    @Override
    public void run() {

    }
}

在匹配服务里把实现添加与删除用户的逻辑
MatchingSystem\service\Impl\MatchingServiceImpl.java

public class MatchingServiceImpl implements MatchingService {

    public final static MatchingPool matchingPool = new MatchingPool();

    @Override
    public String addPlayer(Integer userId, Integer rating) {
        System.out.println("add player: " + userId + " " + rating);
        matchingPool.addPlayer(userId, rating);
        return "add player successfully";
    }

    @Override
    public String removePlayer(Integer userId) {
        System.out.println("remove player: " + userId);
        matchingPool.removePlayer(userId);
        return "remove player successfully";
    }
}

匹配逻辑 :搞个无限循环,周期性执行,每次sleep(1000),若没有匹配的人选,则等待时间++,若有匹配的人选则进行匹配。匹配的rating差会随着等待时间而增加(rating差每等待1s则*10)。

匹配原则 :为了提高用户体验,等待时间越长的玩家越优先匹配。

即列表players从前往后匹配。用一个标记数组标记有没有匹配过即可, checkMatched() 是判断这两个玩家是否能成功匹配在一起。 sendResult() 是发送匹配结果。

private void matchPlayers() { //尝试匹配所有玩家
        boolean[] used = new boolean[players.size()];
        for (int i = 0; i < players.size(); i++) {
            if (used[i]) continue;
            for (int j = i + 1; j < players.size(); j++) {
                if (used[j]) continue;
                Player a = players.get(i), b = players.get(j);
                if (checkMatched(a, b)) {
                    used[i] = used[j] = true;
                    sendResult(a, b);
                    break;
                }
            }
        }
         List<Player> newPlayers = new ArrayList<>();
        for (int i = 0; i < players.size(); i++) {
            if (!used[i]) {
                newPlayers.add(players.get(i));
            }
        }
        players = newPlayers;
       /* for (int i = 0; i < players.size(); i++) { 错误示范
            if (used[i]) players.remove(players.get(i));
        }*/

    }

TIPS :这里标注一下我初学遇到的 坑点 ArrayList 循环删除某个元素不能直接循环一遍然后 remove ,因为每次循环的时候, ArrayList size() 都会改变,所以循环是有问题的,这样只能保证你删掉一个符合要求的元素,而不能实现循环删掉所有符合要求的元素,因此我们要从另一个角度思考问题,用一个新的 ArrayList 存放每一个不需要删除的元素,然后原来的引用直接指向新的List即可。
这里也提供另一种实现循环remove的方法:用 迭代器 Iterator
eg:

 Iterator<Player> iterator = players.iterator();
        while (iterator.hasNext()) {
            if (要删除的条件) iterator.remove();
        }

但是我们上面的删除还涉及到used数组,所以迭代器删除法并不适合,所以要用新列表赋值法!!

对于 checkMatch 判断两个玩家是否能成功匹配,还要考虑其等待时间,要判断分差能不能小于等于a与b的等待时间的最小值*10即 r a t i n g D e l t a < = m i n ( w a i t i n g T i m e a , w a i t i n g T i m e b ) ∗ 10 ratingDelta<=min(waitingTimea,waitingTimeb)∗10 r a t in g De lt a <= min ( w ai t in g T im e a , w ai t in g T im e b ) 10

 private boolean checkMatched(Player a, Player b) { //判断两名玩家是否匹配
        int ratingDelta = Math.abs(a.getRating() - b.getRating());
        int waitingTime = Math.min(a.getWaitingTime(), b.getWaitingTime());
        return ratingDelta <= waitingTime * 10;
    }

6.接收匹配成功的信息

我们要在 backend 端再写一个接受 MatchingSystem 端匹配成功的信息的 Service 和相应的 Controller
GameStartController.java

@RestController

public class StartGameController {
    @Autowired
    private StartGameService startGameService;

    @PostMapping("/pk/start/game/")
    public String startGame(@RequestParam MultiValueMap<String, String> data) {
        Integer aId = Integer.parseInt(Objects.requireNonNull(data.getFirst("a_id")));
        Integer bId = Integer.parseInt(Objects.requireNonNull(data.getFirst("b_id")));
        return startGameService.startGame(aId, bId);
    }
}

GameStartServiceImpl.java

@Service

public class StartGameServiceImpl implements StartGameService {
    @Override
    public String startGame(Integer aId, Integer bId) {
        System.out.println("start game: " + aId + " " + bId);
        WebSocketServer.startGame(aId, bId);
        return "start game successfully";
    }
}

注意:要把上面的路由 /pk/start/game/ 放行,只能本地访问
SecurityConfig.java

...
 .antMatchers("/pk/start/game/").hasIpAddress("127.0.0.1")
...

7.Matching System调用ws端的接口

为了实现springboot之间的通信,我们要像前文一样使用一个Bean类,方法为调用RestTemplate类。即上文的 RestTemplateConfig.java

为了能让Spring里面的Bean注入进来,需要在 MatchingPool.java 里加上 @Component

@Component
...
 private static RestTemplate restTemplate;

    @Autowired
    public void setRestTemplate(RestTemplate restTemplate) {
        MatchingPool.restTemplate = restTemplate;
    }

    ...

    private void sendResult(Player a, Player b) { // 返回匹配结果给ws端
        MultiValueMap<String, String> data = new LinkedMultiValueMap<>();
        data.add("a_id", a.getUserId().toString());
        data.add("b_id", b.getUserId().toString());
        restTemplate.postForObject(startGameURL, data, String.class);
    }

...

8.对于匹配时断开连接的处理

如果一名玩家开始匹配后断开了连接,按照我们上面的做法,断开连接后的玩家会一直处于匹配池中,这样我们的 Matching System 后端会报错,因为我们凡是要获取玩家信息的时候,该玩家已经掉线了,不存在了,会get一个空玩家信息,空信息是没有属性的,而我们后面会调用玩家属性,这是不合理的,肯定会报错的,我们需要修改这个bug:在每次get之前都要判断一下玩家信息是否为空,若不为空再进行下面的逻辑。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

第六章--- 实现微服务:匹配系统(下) 的相关文章

随机推荐

  • 图像配准 CVPRW21 - 深度特征匹配 DFM

    本文转自 图像配准 CVPRW21 深度特征匹配 DFM 深度学习图像配准 CSDN博客 github地址 https github com ufukefe DFM 相识 图像配准 Image Registration 是计算机视觉领域中的
  • 性能测试:如何使用window系统资源监控!

    系统资源监控通常分为两类系统 window和类unix unix linux aix window系统资源监控 window系统资源监控有两种方法 一是使用loadrunner工具进行监控 二是使用windows自带的性能工具perfmon
  • 用一个简单的例子教你如何 自定义ASP.NET Core 中间件(二)

    上一章已经说过了 中间件是一种装配到应用管道以处理请求和响应的软件 每个组件 选择是否将请求传递到管道中的下一个组件 可在管道中的下一个组件前后执行工作 请求委托用于生成请求管道 请求委托处理每个 HTTP 请求 一句话总结 中间件是比筛选
  • git断开原来远程仓库连接并连接新的远程仓库

    背景 先开发了基础框架 后续实际项目基于基础框架开发 需要拉去基础框架 如果直接开发还是在基础框架上进行的 这是不允许的 就需要修改远程地址连接 1 查看远程连接 会返回当前的远程连接地址 git remote v 2 断开与远程仓库的连接
  • SHT10温湿度传感器——STM32驱动

    实验效果 硬件外观 接线 3 3V供电 IIC通讯 代码获取 查看下方 END
  • Java版工程行业管理系统源码-专业的工程管理软件- 工程项目各模块及其功能点清单

    鸿鹄工程项目管理系统 Spring Cloud Spring Boot Mybatis Vue ElementUI 前后端分离构建工程项目管理系统 项目背景 随着公司的快速发展 企业人员和经营规模不断壮大 为了提高工程管理效率 减轻劳动强度
  • Python编程:从入门到实践(基础知识)

    第一章 起步 计算机执行源程序的两种方式 编译 一次性执行源代码 生成目标代码 解释 随时需要执行源代码 源代码 采用某种编程语言编写的计算机程序 目标代码 计算机可执行 101010 编程语言分为两类 静态语言 使用编译执行的编程语言 C
  • DEVOPS 持续部署的例子:IMVU

    持续部署的例子 IMVU IMVU是一家社交娱乐公司 它的产品允许用户以一种3D阿凡达式的体验互相连接起来 本节内容改编自一位IMVU工程师所写的博客 IMVU采用了持续集成 开发人员尽早提交并经常提交 每次提交都触发测试套件的执行 IMV
  • 面向对象编程---基于java控制台的高校教材管理系统课设

    功能要求 1 实现出版社 教材类型的管理 2 实现教材的订购管理 3 实现教材的入库管理 4 创建规则 实现教材的书号必须满足以ISBN开头 后跟10个数字 比如ISBN7302120363 5 创建触发器 实现教材入库和出库时自动修改库存
  • 为什么云监控、云产品流量监控中的流量数据和DDoS防护的流量监控数据有差异?

    一般情况下 DDoS防护的流量监控数据大于您在云监控或具体云产品数据页面看到的流量数据 示例 假设您的ECS实例遭受了DDoS攻击 触发流量清洗 您收到DDoS原生防护基础版的清洗通知 触发清洗时的流量为2 5 Gbps 但是 您在云监控中
  • 总结 BurpSuite 插件 HaE 与 Authz 用法!!!

    HaE与Authz均为BurpSuite插件生态的一员 两者搭配可以避免 越权 未授权 两类漏洞的重复测试行为 适用于业务繁杂 系统模块功能多的场景 两个插件都可以在store里安装 安装完后 点击Filter Settings勾选Show
  • 【工作流Activiti】了解工作流

    1 什么是工作流 工作流 Workflow 就是通过计算机对业务流程自动化执行管理 它主要解决的是 使在多个参与者之间按照某种预定义的规则自动进行传递文档 信息或任务的过程 从而实现某个预期的业务目标 或者促使此目标的实现 通俗来讲 就是业
  • 【工作流Activiti】Activiti的使用

    1 数据库支持 Activiti 运行必须要有数据库的支持 支持的数据库有 mysql oracle postgres mssql db2 h2 2 Activiti环境 我们直接在当前项目 guigu oa parent做Activiti
  • python 基础

    Python 基础 部分信息参考 菜鸟教程 文章目录 Python 基础 介绍背景 环境搭建 下载 win下 Linux下
  • 超星学习通《大学生创新基础》 答案

    1 1 1 多选题 具有高创造性个体的人格特征是 ACBD A 独立性 B 自信 C 对复杂问题感兴趣 D 冒险精神 2 多选题 创新型人才的特点是 ABD A 具有创新精神和创新能力 B 个性灵活 开放 C 力求稳妥 拒绝冒险 D 精力充
  • 绝对干货!自动化测试PO设计模式的进阶实现(附源码),适合想提升编码能力的测试同学

    本文阅读的前提 同学们知道如何进行Selenium自动化测试并了解什么是PO设计模式 对于代码基础较弱的测试同学希望多读几次文章并上手操作 这篇文章看懂后 大家的编码能力就会进步了 代码哪里不懂欢迎留言 PO模式的设计问题 在前面的文章 无
  • [MM32硬件]搭建灵动微MM32G0001A6T的简易开发环境

    作为学习单片机的经典 自然是通过GPIO点亮LED 或者是响应按钮的外部中断例程 这我们看看SOP8封装的芯片MM32G0001A6T得引脚 除了VDD和GND固定外 我们可以使用PA14 PA1 PA13 PA15 PA2 PA3这六个G
  • SQL语句整理二--Mysql

    文章目录 知识点梳理 1 mysql 中 in 和 exists 区别 2 varchar 与 char 的区别
  • 有没有免费好用的进销存管理系统?

    有没有免费好用的进销存管理系统 要 好用 还要 免费 这样的进销存系统选型策略 到最后是 一箭双雕 还是 鱼与熊掌不可兼得 呢 让我们从好用和免费开始 慢慢分析 免费和好用能共存吗 怎么才算是好用的进销存系统 推荐一款好用的 订阅制付费的系
  • 第六章--- 实现微服务:匹配系统(下)

    0 写在前面 这一章终于完了 但是收尾工作真的好难呀QAQ 可能是我初学的缘故 有些JAVA方面的特性不是很清楚 只能依葫芦画瓢地模仿着用 特别是JAVA的注解 感觉好多但又不是很懂其中的原理 只知道要在某个时候用某个注解 我真是有够菜的