第六章--- 实现微服务:匹配系统(下)
0.写在前面
这一章终于完了,但是收尾工作真的好难呀QAQ,可能是我初学的缘故,有些JAVA方面的特性不是很清楚,只能依葫芦画瓢地模仿着用。特别是JAVA的注解,感觉好多但又不是很懂其中的原理,只知道要在某个时候用某个注解,我真是有够菜的()
以我拙见,JAVA注解大概分为两类
-  一类是使用Bean,即是把已经在xml文件中配置好的Bean拿来用,完成属性、方法的组装;比如@Autowired , @Resource,可以通过byTYPE(@Autowired)、byNAME(@Resource)的方式获取Bean; 
-  一类是注册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 
       
      
    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之前都要判断一下玩家信息是否为空,若不为空再进行下面的逻辑。
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。 如若内容造成侵权/违法违规/事实不符,请联系我的编程经验分享网邮箱:veading@qq.com进行投诉反馈,一经查实,立即删除!