实现匹配系统(下)

创建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技术涉及的
很多不只是服务间的相关通信,还包括网关、负载均衡等优化配置,由于项目并发量没那么大,因此未使用这些技术。

实现匹配系统(下)_第1张图片

创建SpringCloud目录 

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

  
        
            org.springframework.cloud
            spring-cloud-dependencies
            2021.0.4
            pom
            import
        

整理父级项目

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

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

backendcloud
    0.0.1-SNAPSHOT
    backendcloud
    backendcloud
    
    pom
    
        1.8
    
    
        
            org.springframework.boot
            spring-boot-starter-web
        

        
            org.springframework.boot
            spring-boot-starter-test
            test
        

        
        
        
            org.springframework.cloud
            spring-cloud-dependencies
            2021.0.3
            pom
            import
        
    

    
        
            
                org.springframework.boot
                spring-boot-maven-plugin
                
                2.7.2
            
        
    

创建我们的两个子项目

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

新建模块 实现匹配系统(下)_第2张图片


    com.kob.matchingsystem
    matchingsystem

    
        8
        8
    

    
    
        
            org.springframework.boot
            spring-boot-starter-web
        

        
            org.springframework.boot
            spring-boot-starter-test
            test
        
    


实现两个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 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 data) {
        Integer userId = Integer.parseInt(Objects.requireNonNull(data.getFirst("user_id")));
        return matchingService.removePlayer(userId);
    }
}

权限控制

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

导包

backendcloud/pom.xml



   

    com.kob.matchingsystem
    matchingsystem

    
        8
        8
    

    
        
            org.springframework.boot
            spring-boot-starter-web
        

        
            org.springframework.boot
            spring-boot-starter-test
            test
        

        
        
        
            org.springframework.boot
            spring-boot-starter-security
            2.7.1
        
    



配置类


@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



    
        backendcloud
        com.kill
        0.0.1-SNAPSHOT
    
    4.0.0

    com.kob.backend
    backend

    
        8
        8
    

    
        
        
            org.springframework.boot
            spring-boot-starter-jdbc
            2.7.1
        
        
        
            org.projectlombok
            lombok
            1.18.24
            provided
        
        
        
            mysql
            mysql-connector-java
            8.0.29
        
        
        
            com.baomidou
            mybatis-plus-boot-starter
            3.5.2
        
        
        
            com.baomidou
            mybatis-plus-generator
            3.5.3
        
        
        
            org.springframework.boot
            spring-boot-starter-security
            2.7.1
        
        
        
            io.jsonwebtoken
            jjwt-api
            0.11.5
        
        
        
            io.jsonwebtoken
            jjwt-impl
            0.11.5
            runtime
        
        
        
            io.jsonwebtoken
            jjwt-jackson
            0.11.5
            runtime
        
        
            org.springframework.boot
            spring-boot-starter-web
        

        
            org.springframework.boot
            spring-boot-starter-test
            test
        
        
            com.github.penggle
            kaptcha
            
                
                    javax.servlet-api
                    javax.servlet
                
            
            2.3.2
        
        
            org.springframework.boot
            spring-boot-starter-data-redis
        
        
        
            org.springframework.boot
            spring-boot-starter-websocket
            2.7.2
        
        
        
            com.alibaba
            fastjson
            2.0.11
        
    

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 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 add(Map 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 update(Map 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 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 data = new LinkedMultiValueMap<>();
        data.add("user_id", this.user.getId().toString());
        restTemplate.postForObject(removePlayerUrl, data, String.class);
    }

    ...
}

实现匹配系统(下)_第3张图片

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

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

Match System的实现

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

引入lombok maven


        
        
            org.projectlombok
            lombok
            1.18.24
            provided
        

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

    ...
}

效果展示: 

实现匹配系统(下)_第4张图片

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

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

    ...
}

你可能感兴趣的:(springboot,vue,java,微服务,spring,cloud)