匹配系统(下)

创建SpringCloud目录

目录

创建SpringCloud目录

创建我们的两个子项目

实现两个Interface

Config网关

放行+完事两个Api

封装后端逻辑

对接我们的匹配系统

修改数据库-天梯分

更改数据库对应的一些修改

实现我们匹配之后的逻辑的思路

具体实现过程

关于线程锁

来进行实现线程

写匹配函数

匹配完之后将已经用过的玩家删掉

更改数据库对应的一些修改


用户浏览器打开之后、两个client表示两个浏览器
Client向云端server发送请求、有http的servercontroller
还有ws的server、前端client会向websocket发请求
发完请求之后、本来应该用微服务实现匹配系统
向matchingsystem发送请求、表示我们加入了一个玩家
matchingsystem匹配完之后向server返回一个结果
当两名玩家匹配完之后、他们就会开一个线程
就会开始在云端执行一次游戏、Game第一步先去创建一个地图
创建完地图之后我们会把地图返回给前端、在前端画出来
通过ws返回、下一步读入两个玩家的操作
也可以用bot来执行、不一定非从前端读进来
如果没有得到某个玩家的输入可以直接结束返回结果
如果两名玩家的下一步操作都获取到的话我们就judge一下
判断一下操作是不是合法、如果不合法的话就结束了
如果合法的话无限循环到下一步

微服务
未来bot的执行也是一个微服务、微服务:其实就是这边会有一个独立的程序可以认为
又起了一个新的webserver,又起了一个新的springboot,两个springboot
当我们获取到两名玩家的匹配信息之后、它会向我们的matchingserver后端 发送一个http请求
当我们接收到这个请求之后类似与game它也会开一个单独的线程来做一个匹配
Matching、这个Matching的过程类似的游戏的过程、每隔一秒钟去扫描当前已有的所有玩家
然后判断一下这些玩家能不能相互之间匹配出来、如果可以匹配的话就把结果返回
返回的结果也是通过http、这个就称为一个微服务!我们微服务用的是springcloud
微服务实现方式、 我们用的是通过http进行操作两边都是webserver
thrift用的是普通的socket来通信、其他的地方没有任何区别 

每个服务是一个独立的程序,每个服务之间可以相互通信。此项目中,BackEnd 和 MatchSystem都是一个微服务!
当BackEnd接收到玩家开始匹配的请求,BackEnd会想MatchSystem发送http请求,通知MatchSystem开始匹配,
MatchSystem会单独开一个线程开始匹配,匹配逻辑为:每秒都会扫描匹配池中的所有玩家,判断玩家能否相互匹
配,若能,则发送http请求给BackEnd。此项目使用的微服务技术是Spring Cloud。Spring Cloud技术涉及的
很多不只是服务间的相关通信,还包括网关、负载均衡等优化配置,由于项目并发量没那么大,因此未使用这些技术。

创建SpringCloud目录 

首先我们先创建一个SpringCloud的目录
整个项目的结果会做一个变化
需要创建一个新的父项目用来装两个并列的子项目
加入springweb依赖,这个微服务其实就是两个单独的服务
它可以接收信息也可以发送信息,spring里面我们是用http操作的
也就是创建了两个后端服务器都是通过url来通信的、thrift是通过socket来通信的
会稍微快一些、创建完之后我们需要先配置一下
由于父级项目是没有逻辑的我们可以把src目录删掉、删完之后我们修改一下pom
另外加上springcloud的依赖

  <!-- https://mvnrepository.com/artifact/org.springframework.cloud/spring-cloud-dependencies -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-dependencies</artifactId>
            <version>2021.0.4</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>

整理父级项目

删除backendcloud的src目录,因为父级项目没有逻辑,只负责管理两个微服务。

配置pom,记得点开IDEA右侧Maven重新加载所有Maven项目

<artifactId>backendcloud</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>backendcloud</name>
    <description>backendcloud</description>
    <!-- 修改处1 -->
    <packaging>pom</packaging>
    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <!-- 修改处2 -->
        <!-- https://mvnrepository.com/artifact/org.springframework.cloud/spring-cloud-dependencies -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-dependencies</artifactId>
            <version>2021.0.3</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <!-- 修改处3 -->
                <version>2.7.2</version>
            </plugin>
        </plugins>
    </build>

创建我们的两个子项目

两个模块,我们需要匹配一些依赖
matchingsystem本质上也是一个springboot、我们需要将springboot的一些依赖加进去
迁移一下Springweb、然后我们点开我们的src然后我们来实现一下我们的匹配系统
匹配系统中我们需要实现几个接口呢、我们需要实现两个接口
第一个是添加一个玩家addplayer、第二个是removeplayer
有两个springboot每一个springboot都需要占用一个独立的端口
我们需要先把matchingsystem这个服务器配置一下端口、接下里java里创建两个接口 

新建模块 


    <groupId>com.kob.matchingsystem</groupId>
    <artifactId>matchingsystem</artifactId>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>

    <!-- 修改处 -->
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

</project>

实现两个Interface

接下来在java里创建两个接口
创建接口的话我们就需要创建一个controller
定义两个接口、然后我们实现这两个接口、然后我们创建一个controller、注入接口
因为涉及到对玩家的修改、所以用post请求
MultiValueMap、是每一个关键字对应一个列表value、在这里需要加上一个权限控制 

配置端口
在matchingsystem -> src -> main -> resources右键新建文件, 文件名为:application.properties

内容:

server.port=3001

匹配服务的搭建

matchingsystem
    config
        SecurityConfig.java
    controller
        MatchingController.java
    service
        impl
            MatchingServiceImpl.java
        MatchingService.java
    MatchSystemApplication.java

 接口


public interface MatchingService {

    String addPlayer(Integer userId, Integer rating);

    String removePlayer(Integer userId);
}

接口实现

@Service
public class MatchingServiceImpl implements MatchingService {

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

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

控制器

参数不能使用普通map,MultiValueMap和普通map的区别,这个是一个键对应多个值

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

    // 参数不能使用普通map,MultiValueMap和普通map的区别时,这个是一个键对应多个值
    @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);
    }
}

权限控制

由于Spring Cloud是http请求,所以可能会接收到用户的伪请求,matchingsystem只能对于后端请求,因此需要防止外部请求,通过Spring Security来实现权限控制。 

导包

backendcloud/pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
   

    <groupId>com.kob.matchingsystem</groupId>
    <artifactId>matchingsystem</artifactId>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <!-- 此处修改 -->
        <!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-security -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
            <version>2.7.1</version>
        </dependency>
    </dependencies>

</project>

配置类


@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                // 放行这两个接口
                .antMatchers("/player/add/", "/player/remove/").hasIpAddress("127.0.0.1")
                .antMatchers(HttpMethod.OPTIONS).permitAll()
                .anyRequest().authenticated();
    }
}

 入口


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

测试

点击类左边的行号右边的启动图标运行项目,运行项目时,左下角会提示是否使用服务管理多个Spring Boot项目,点击使用。

将后端转化成一个微服务

右键backendcloud 新建 -> 模块

删掉backend的src目录

将之前项目的src目录复制到backend下,先在文件资源管理器赋值src目录,再右键idea的backend -> 粘贴 -> 确定

修改pom.xml

记得刷新Maven

backend/pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>backendcloud</artifactId>
        <groupId>com.kill</groupId>
        <version>0.0.1-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.kob.backend</groupId>
    <artifactId>backend</artifactId>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>

    <dependencies>
        <!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-jdbc -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
            <version>2.7.1</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/org.projectlombok/lombok -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.24</version>
            <scope>provided</scope>
        </dependency>
        <!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.29</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/com.baomidou/mybatis-plus-boot-starter -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.5.2</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/com.baomidou/mybatis-plus-generator -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-generator</artifactId>
            <version>3.5.3</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-security -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
            <version>2.7.1</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt-api -->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-api</artifactId>
            <version>0.11.5</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt-impl -->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-impl</artifactId>
            <version>0.11.5</version>
            <scope>runtime</scope>
        </dependency>
        <!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt-jackson -->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-jackson</artifactId>
            <version>0.11.5</version>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>com.github.penggle</groupId>
            <artifactId>kaptcha</artifactId>
            <exclusions>
                <exclusion>
                    <artifactId>javax.servlet-api</artifactId>
                    <groupId>javax.servlet</groupId>
                </exclusion>
            </exclusions>
            <version>2.3.2</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-websocket -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-websocket</artifactId>
            <version>2.7.2</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/com.alibaba/fastjson -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>2.0.11</version>
        </dependency>
    </dependencies>
</project>

Backend 和 Match System 的通信

1.文件结构

backend
    config
        RestTemplateConfig.java

2.代码

封装StartGame逻辑

// 1.删除以下两个包
import java.util.Iterator;
import java.util.concurrent.CopyOnWriteArraySet;

@Component
// url链接:ws://127.0.0.1:3000/websocket/**
@ServerEndpoint("/websocket/{token}")  // 注意不要以'/'结尾
public class WebSocketServer {
    ...

    // 2.删除matchingPool变量

    ...

    @OnClose
    public void onClose() {
        // 关闭链接
        System.out.println("disconnected!");
        if(this.user != null) {
            users.remove(this.user.getId());
            // 3. 删除取消匹配逻辑
        }
    }

    // 4.抽取匹配成功后的逻辑为一个函数
    public static void startGame(Integer aId, Integer bId) {
        User a = userMapper.selectById(aId), b = userMapper.selectById(bId);
        Game game = new Game(13, 14, 20, a.getId(), b.getId());
        game.createMap();
        // 一局游戏一个线程,会执行game类的run方法
        game.start();

        users.get(a.getId()).game = game;
        users.get(b .getId()).game = game;

        JSONObject respGame = new JSONObject();
        // 玩家的id以及横纵信息
        respGame.put("a_id", game.getPlayerA().getId());
        respGame.put("a_sx", game.getPlayerA().getSx());
        respGame.put("a_sy", game.getPlayerA().getSy());
        respGame.put("b_id", game.getPlayerB().getId());
        respGame.put("b_sx", game.getPlayerB().getSx());
        respGame.put("b_sy", game.getPlayerB().getSy());
        respGame.put("map", game.getG());

        // 发送给A的信息
        JSONObject respA = new JSONObject();
        respA.put("event", "start-matching");
        respA.put("opponent_username", b.getUsername());
        respA.put("opponent_photo", b.getPhoto());
        respA.put("game", respGame);
        // 通过userId取出a的连接,给A发送respA
        users.get(a.getId()).sendMessage(respA.toJSONString());

        // 发送给B的信息
        JSONObject respB = new JSONObject();
        respB.put("event", "start-matching");
        respB.put("opponent_username", a.getUsername());
        respB.put("opponent_photo", a.getPhoto());
        respB.put("game", respGame);
        // 通过userId取出b的连接,给B发送respB
        users.get(b.getId()).sendMessage(respB.toJSONString());
    }

    // 5.删除原先匹配逻辑,把匹配的逻辑交给另一个服务,服务成功后再调用startGame函数开始游戏逻辑
    private void startMatching() {
        System.out.println("start matching!");
    }

    // 6.删除取消匹配逻辑
    private void stopMatching() {
        System.out.println("stop matching");
    }

    ...
}

构造Spring Cloud服务之间通信需要的RestTemplate

RestTemplate是Spring提供的用于访问Rest服务的客户端,RestTemplate提供了多种便捷访问远程Http服务的方法,能够大大提高客户端的编写效率。

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;

@Configuration
public class RestTemplateConfig {

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

 修改数据库,将排名移到玩家身上

修改涉及到User构造和Bot构造的代码

@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {
    @TableId(type = IdType.AUTO)
    private Integer id;
    private String username;
    private String password;
    private String photo;
    private Integer rating;
}

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Bot {
    @TableId(type = IdType.AUTO)
    private Integer id;
    private Integer userId;
    private String title;
    private String description;
    private String content;
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone="Asia/Shanghai")
    private Date createtime;
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone="Asia/Shanghai")
    private Date modifytime;
}

@Service
public class RegisterServiceImpl implements RegisterService {
    ...

    @Override
    public Map<String, String> register(String username, String password, String confirmedPassword) {
        ...

        // 修改处:增加rating为1500的分数
        User user = new User(null, username, encodedPassword, photo, 1500);

        ...
    }
}


@Service
public class AddServiceImpl implements AddService {
    @Autowired
    private BotMapper botMapper;

    @Override
    public Map<String, String> add(Map<String, String> data) {
        ...

        Bot bot = new Bot(null, user.getId(), title, description, content, now, now);

        ...
    }
}

@Service
public class UpdateServiceImpl implements UpdateService {
    @Autowired
    private BotMapper botMapper;

    @Override
    public Map<String, String> update(Map<String, String> data) {
        ...

        Bot new_bot = new Bot(
                bot.getId(),
                user.getId(),
                title,
                description,
                content,
                bot.getCreatetime(),
                new Date()
        );

        ...
    }
}

加上BackEnd发送给matchingsystem服务的添加玩家和删除玩家的请求


import org.springframework.web.client.RestTemplate;
import org.springframework.util.MultiValueMap;
import org.springframework.util.LinkedMultiValueMap;

@Component
// url链接:ws://127.0.0.1:3000/websocket/**
@ServerEndpoint("/websocket/{token}")  // 注意不要以'/'结尾
public class WebSocketServer {
    private static RestTemplate restTemplate;
    private final static String addPlayerUrl = "http://127.0.0.1:3001/player/add/";
    private final static String removePlayerUrl = "http://127.0.0.1:3001/player/remove/";

    ...

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

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

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

    ...
}

实现我们匹配之后的逻辑的思路

当前已经实现浏览器端向云端发送了一个请求
我们在websocket当中接收到这个请求之后,它会把我们的请求再发送给我们的匹配系统
我们刚才看到的输出是在匹配系统中看到的输出、一个添加一个取消两个操作
下一步就是要实现我们匹配之后的逻辑了、收到请求之后的逻辑
接收到一个请求之后会将当前匹配的所有用户放到一个池子
然后我们开一个额外的新线程、是每隔一秒去扫描一遍数组
将能够匹配的人匹配到一起、匹配的时候、匹配两名分值比较相近的玩家
随着时间的推移、两名玩家允许的分差可以越来越大可能一开始a和b分值比较大
这两个人是不能匹配到一块的,但是随着时间推移我们发现没有其他玩家可以跟这两名玩家匹配
我们就可以逐步放大两名玩家的一个匹配范围、直到两名玩家都可以匹配到一块为止
具体就是、每位玩家有一个分值rating
第0秒的时候这个玩家只能匹配分差在十以内的玩家、第二秒的时候就可以匹配分差在20以内的玩家

Match System的实现

matchingsystem
    config
        RestTemplateConfig.java
    service
        impl
            utils
                MatchingPool.java
                Player.java

引入lombok maven

<!-- 修改处 -->
        <!-- https://mvnrepository.com/artifact/org.projectlombok/lombok -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.24</version>
            <scope>provided</scope>
        </dependency>

Player.java

匹配服务也有玩家的概念


@Data
@AllArgsConstructor
@NoArgsConstructor
public class Player {
    private Integer userId;
    private Integer rating;
    private Integer waitingTime;
}

MatchingServiceImpl.java

接收到backend的请求,添加玩家或删除玩家


@Service
public class MatchingServiceImpl implements MatchingService {
    public static final 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 success";
    }

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

发送给BackEnd消息辅助类:RestTemplate.java


@Configuration
public class RestTemplateConfig {

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

MatchingPool.java

匹配池:匹配分数最接近的玩家,根据匹配时间增长,匹配范围逐渐增大。
操作:包括添加玩家,删除玩家,匹配玩家,发送给后端匹配成功的结果。
策略:为了防止匹配时间过长,优先将先匹配的玩家优先匹配,防止用户流失。

// 匹配池是多线程的
@Component
public class MatchingPool extends Thread {
    private static List<Player> players = new ArrayList<>();
    private ReentrantLock lock = new ReentrantLock();
    private static RestTemplate restTemplate;
    private static final String startGameUrl = "http://127.0.0.1:3000/pk/start/game/";

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

    public void addPlayer(Integer userId, Integer rating) {
        // 在多个线程(匹配线程遍历players时,主线程调用方法时)会操作players变量,因此加锁
        lock.lock();
        try {
            players.add(new Player(userId, rating, 0));
        } finally {
            lock.unlock();
        }
    }

    public void removePlayer(Integer userId) {

        // 在多个线程(匹配线程遍历players时,主线程调用方法时)会操作players变量,因此加锁
        lock.lock();
        try {
            List<Player> newPlayers = new ArrayList<>();
            for(Player player : players) {
                if(!player.getUserId().equals(userId)) {
                    newPlayers.add(player);
                }
            }
            players = newPlayers;
        } finally {
            lock.unlock();
        }
    }

    private void increaseWaitingTime() {    // 将所有当前玩家的等待时间 + 1
        for(Player player : players) {
            player.setWaitingTime(player.getWaitingTime() + 1);
        }
    }

    private boolean checkMatched(Player a, Player b) {  // 判断两名玩家是否匹配
        // 获取两名分差
        int ratingDelta = Math.abs(a.getRating() - b.getRating());
        // min: 若取min则代表两者分差都小于 等待时间 * 10,实力差距最接近
        // max: 若取max则代表有一方分差小于 等待时间 * 10,实力差距可能会大
        int waitingTime = Math.min(a.getWaitingTime(), b.getWaitingTime());
        return ratingDelta <= waitingTime * 10;
    }

    private void sendResult(Player a, Player b) {   // 返回匹配结果
        System.out.println("send result: " + a + " " + b);
        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);
    }

    private void matchPlayers() {   // 尝试匹配所有玩家
        System.out.println("match players: " + players.toString());
        // 标记是否被匹配
        boolean[] used = new boolean[players.size()];

        // 先枚举等待最久的玩家,恰好是players前面的玩家等待的的久
        for(int i = 0; i < players.size(); i++) {
            if(used[i]) continue;
            // i只要和比i大的匹配,就正好所有玩家匹配一次
            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;
    }

    @Override
    public void run() {
        while(true) {
            try {
                Thread.sleep(1000);
                // 涉及到操作players变量,加锁;
                lock.lock();
                try {
                    increaseWaitingTime();
                    matchPlayers();
                } finally {
                    lock.unlock();
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

MatchSystemApplication.java
启动匹配线程

这里不需要用线程安全的列表
因为这里会手动加锁、不管线程安全不安全
我们会把它变成安全、异步就是不是按照顺序执行的
会切换线程、顺序是乱的、我们需要两个线程范围我们的player
一个是匹配线程一个是我们传入参数的线程
这是我们添加一名玩家以及删除一名玩家

来进行实现线程

线程的逻辑、周期性的执行
每次执行的时候每隔一段时间判断一下当前玩家有没有匹配的
这里可以写一个死循环、有匹配的返回没有就等待
写一个sleep函数每次sleep一秒、每一秒钟自动执行一遍
每位玩家每等待一秒就在waitingtime里加一、他未来匹配的阈值就可以变宽 

写匹配函数
我们需要写几个匹配函数
如何去匹配玩家呢?
我们可以开一个bool数组用来判断当前还剩下哪些人、从前往后去枚举 每一个玩家
对于这个玩家我们需要去看一下哪个玩家和他匹配、如果有人可以和他匹配
我们就不匹配了、越老的玩家越具有优先选择权
分差能被ab接受\我们只需要判断我们的分差能不能被a和b等待时间的最小值*10就可以了 

匹配完之后将已经用过的玩家删掉
我们需要是调用sendresult函数的话
已经在匹配池匹配好了、server里需要能接收这个消息
写一个方法能够接收这个信息、写完之后对接口放行
就可以在sendresult中调用、用到RestTemplate 

import com.kob.matchingsystem.service.impl.MatchingServiceImpl;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class MatchSystemApplication {
    public static void main(String[] args) {
        MatchingServiceImpl.matchingPool.start();   // 启动匹配线程
        SpringApplication.run(MatchSystemApplication.class, args);
    }
}

Backend接收Match System的消息

删除backend/controller/pk下的BotInfoController.java和IndexController

文件结构

backend
    controller
        pk
            StartGameController.java
    service
        impl
            pk
                StartGameServiceImpl.java
        pk
            StartGameService.java

接口

package com.kob.backend.service.pk;

public interface StartGameService {

    String startGame(Integer aId, Integer bId);
}

接口实现

import org.springframework.stereotype.Service;

@Service
public class StartGameServiceImpl implements StartGameService {

    @Override
    public String startGame(Integer aId, Integer bId) {
        System.out.println("start gameL: " + aId + " " + bId);
        WebSocketServer.startGame(aId, bId);
        return "start game success";
    }
}

控制器

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.MultiValueMap;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.util.Objects;

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

权限控制
对Match System服务放行接口

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    ...

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                // 放行这两个接口
                .antMatchers("/user/account/token/", "/user/account/register/", "/getKaptcha").permitAll()
                .antMatchers("/pk/start/game/").hasIpAddress("127.0.0.1")   // 增加此行
                .antMatchers(HttpMethod.OPTIONS).permitAll()
                .anyRequest().authenticated();

        http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
    }

    ...
}

效果展示: 

增加程序鲁棒性:若玩家在匹配中突然停电,则会爆异常,需要特判

@Component
// url链接:ws://127.0.0.1:3000/websocket/**
@ServerEndpoint("/websocket/{token}")  // 注意不要以'/'结尾
public class WebSocketServer {
    ...

    public static void startGame(Integer aId, Integer bId) {
        User a = userMapper.selectById(aId), b = userMapper.selectById(bId);
        Game game = new Game(13, 14, 20, a.getId(), b.getId());
        game.createMap();
        // 一局游戏一个线程,会执行game类的run方法
        game.start();

        if(users.get(a.getId()) != null)
            users.get(a.getId()).game = game;

        if(users.get(b.getId()) != null)
            users.get(b .getId()).game = game;

        JSONObject respGame = new JSONObject();
        // 玩家的id以及横纵信息
        respGame.put("a_id", game.getPlayerA().getId());
        respGame.put("a_sx", game.getPlayerA().getSx());
        respGame.put("a_sy", game.getPlayerA().getSy());
        respGame.put("b_id", game.getPlayerB().getId());
        respGame.put("b_sx", game.getPlayerB().getSx());
        respGame.put("b_sy", game.getPlayerB().getSy());
        respGame.put("map", game.getG());

        // 发送给A的信息
        JSONObject respA = new JSONObject();
        respA.put("event", "start-matching");
        respA.put("opponent_username", b.getUsername());
        respA.put("opponent_photo", b.getPhoto());
        respA.put("game", respGame);
        // 通过userId取出a的连接,给A发送respA
        if(users.get(a.getId()) != null)
            users.get(a.getId()).sendMessage(respA.toJSONString());

        // 发送给B的信息
        JSONObject respB = new JSONObject();
        respB.put("event", "start-matching");
        respB.put("opponent_username", a.getUsername());
        respB.put("opponent_photo", a.getPhoto());
        respB.put("game", respGame);
        // 通过userId取出b的连接,给B发送respB
        if(users.get(b.getId()) != null)
            users.get(b.getId()).sendMessage(respB.toJSONString());
    }

    ...
}

...

public class Game extends Thread {
    ...

    private void senAllMessage(String message) {
        if(WebSocketServer.users.get(playerA.getId()) != null)
            WebSocketServer.users.get(playerA.getId()).sendMessage(message);
        if(WebSocketServer.users.get(playerB.getId()) != null)
            WebSocketServer.users.get(playerB.getId()).sendMessage(message);
    }

    ...
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值