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

发布时间:2023年12月18日

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 Systembackend

注意: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 Systempom.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注解的方法,若存在则调用这个函数方法,把返回值赋过来。(似乎与函数名无关,如:getRestTemplatesetRestTemplate

开始匹配服务
首先要把之前的数据库也引入进现在的这个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.javaPlayer.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,因为每次循环的时候,ArrayListsize()都会改变,所以循环是有问题的,这样只能保证你删掉一个符合要求的元素,而不能实现循环删掉所有符合要求的元素,因此我们要从另一个角度思考问题,用一个新的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 ratingDelta<=min(waitingTimea,waitingTimeb)?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之前都要判断一下玩家信息是否为空,若不为空再进行下面的逻辑。

文章来源:https://blog.csdn.net/m0_51366201/article/details/135061583
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。