第六章---匹配系统(下)

1.实现匹配系统的微服务

微服务可以理解为在SpringBoot之外的另外一个Server,负责处理一段独立的功能,与SpringBoot Server之间通过http通信。在游戏的匹配系统,之前是简单粗暴的放在一个集合上,当集合元素大于2时,取出两名玩家进行匹配,无法适应更加复杂的场景,因此现在要将这段程序独立出来。

第六章---匹配系统(下)_第1张图片
微服务有多种实现方式,这里采用SpringCloud。SpringCloud和SpringBoot都相当于一个Web Server两者之间通过Http通信

第六章---匹配系统(下)_第2张图片
由于SpringCloud实现的匹配系统和SpringBoot实现的游戏后端是并列的,因此项目结构需要改动。

1.1创建SpringCloud项目

第六章---匹配系统(下)_第3张图片
第六章---匹配系统(下)_第4张图片

1.2配置SpringCloud项目

第六章---匹配系统(下)_第5张图片
第六章---匹配系统(下)_第6张图片
SpringCloud项目中添加依赖: Maven仓库地址 spring-cloud-dependencies

第六章---匹配系统(下)_第7张图片

1)添加和配置

创建SpringCloud的子项目——matchingsystem

第六章---匹配系统(下)_第8张图片
第六章---匹配系统(下)_第9张图片
然后需要配置一些依赖。

matchingsystem本质上也是一个springboot,所以需要将父级目录backendcloudporm.xml中的依赖,SpringWeb依赖,直接复制到子项目matchingsystem对应的porm.xml

第六章---匹配系统(下)_第10张图片
创建application.properties,配置端口,由于游戏后端backend中是3000,这里设置为3001

第六章---匹配系统(下)_第11张图片

2)匹配系统的简单布局

对于匹配系统而言,需要实现的接口中需要有两个函数,addPlayerremovePlayer用于添加和删除玩家

创建接口,就需要创建controller负责调用接口,service负责声明接口,service.impl负责实现接口

为了方便调试,暂时不实现具体功能。

MatchingService.java

第六章---匹配系统(下)_第12张图片
MatchingServiceImpl.java

第六章---匹配系统(下)_第13张图片
MatchingController.java

第六章---匹配系统(下)_第14张图片
注意,对于MultiValueMap结构而言,运行一个key对应的多个value,并用数组保存

map.getFirst("user_id")表示user_id所对应的value数组中,第一个值001

第六章---匹配系统(下)_第15张图片
此时MatchingController还有一个问题是,没有授权验证,这样就有外网通过其他请求来恶意攻击的风险。

与之前backend一样,添加Spring Security依赖

第六章---匹配系统(下)_第16张图片
照着之前的代码,只用到configure这一段代码,且不涉及Jwt-token的验证,直接抄过来修改

第六章---匹配系统(下)_第17张图片
我们期望的的是只能被后端服务器访问,而不能以其他方式访问。解决这个问题,可以根据IP地址来判断,也就是只能通过本地,而不能通过其他地方来访问。

如下图,对"/player/add/""/player/remove/"的请求放开

只有IP地址为本地(127.0.0.1)Server发出的请求才是有效的。

第六章---匹配系统(下)_第18张图片
将原来的Main函数,重命名为MatchingSystemApplication,作为Springboot的入口

第六章---匹配系统(下)_第19张图片
这样就能够跑起来(只不过此时由于是POST的类型的请求,不支持通过浏览器直接请求)

第六章---匹配系统(下)_第20张图片
由于请求是游戏后端backend发起的,因此还需要将两部分对接起来。

现在需要创建一个新的子项目,将之前的逻辑装在backendclound下面

1.4添加子项目—Backend
1)添加和配置

backendcloud下面创建SpringCloud的子项目——Backend

第六章---匹配系统(下)_第21张图片
第六章---匹配系统(下)_第22张图片
然后,将之前的后端backend项目的src整个文件夹直接复制过来。

第六章---匹配系统(下)_第23张图片

复制到backendcloud下面的子模块backend下面

第六章---匹配系统(下)_第24张图片
第六章---匹配系统(下)_第25张图片
然后将后端backend项目下的porm.xml中的依赖也粘贴到下面这个位置

第六章---匹配系统(下)_第26张图片
第六章---匹配系统(下)_第27张图片
其中的thymeleaf用不到,删除即可。

目前的项目结构如下:

第六章---匹配系统(下)_第28张图片

2)连通匹配系统

首先要打通

第六章---匹配系统(下)_第29张图片
需要将下面这段修改,目前还只是简单粗暴的进行从集合中取出User

第六章---匹配系统(下)_第30张图片
修改如下:

将与matchpool相关的所有操作都删掉

然后在startMatching()调用时向MatchingSystem发请求,申请为玩家匹配对手,在stopMatching()onClose() 调用时向MatchingSystem发请求,申请取消玩家的匹配,

配置RestTemplate
MatchingSystem发请求,需要借助Springboot中的一个工具RestTemplate,它可以在两个Spring进程之间进行通信。

先配置一下这个工具,如果希望在WebSocketServer.java中使用RestTemplate,就需要加Bean注解,这样才能够取出来。

可以理解为,需要用到某个工具的时候,就定义一个它的Configuration,加一个注解Bean,返回一个它的实例。

第六章---匹配系统(下)_第31张图片
这样在未来使用的时候,就可以通过@Autowired将其注入

第六章---匹配系统(下)_第32张图片
原理是,如果加了@Autowired,就会看一下这个接口(或者Service)是否有一个唯一的注解为Bean的函数和它对应,如果有的话就调用这个函数,将返回值赋过来。

现在看下怎么用。

在具体用之前,需要修改下数据库。将rating字段,将bot表中,移动到user表中

同样,修改这两个表对应的pojo

并且在所用调用UserBot构造函数的时候修改

第六章---匹配系统(下)_第33张图片
第六章---匹配系统(下)_第34张图片
然后进入正题。

使用RestTemplate
借助RestTemplateMatchingSystem发请求

第六章---匹配系统(下)_第35张图片
启动Backend模块对应的Spring服务

此时能够在服务中看到,启动了两个SpringBoot

Backend对应的端口号为3000

MatchingSystem对应的端口号为3001

第六章---匹配系统(下)_第36张图片
用户登录之后,进行匹配,在MatchingSystem对应的控制台下面add player1 1500表示匹配系统接收到了来自后端的玩家ID为6的匹配请求。

第六章---匹配系统(下)_第37张图片
当点击取消时,同样成功接收到了请求

第六章---匹配系统(下)_第38张图片
控制台中看到的输出,是在匹配系统中实现的输出。

至此,游戏后端,向匹配系统发请求这一过程就完成。

在这里插入图片描述
接下来要实现匹配系统内部的逻辑.

1.5匹配系统

匹配系统在接收到来自游戏后端的匹配请求之后,会将当前参与匹配的所有用户,放在一个池子(数组)里面。开辟额外新线程,每隔1s就扫描一遍整个数组,将能够匹配的玩家匹配到一起。我们期望匹配相近分值的玩家,随着时间的推移,可以逐步放宽分值要求,也就是允许两名匹配玩家的分值差距较大,直到所有玩家都可以在规定时间内匹配在一块为止。具体来说,第一秒,匹配分值差距10以内的玩家,第二秒,匹配分值差距20以内的玩家…直到匹配完成为止。

现在需要将之前关于线程的那部分再重复一遍。

创建service.impl.utils.MatchingPool用于维护这样一个线程,同时创建service.impl.utils.Player来存储玩家(需要提前将Lombok依赖添加到匹配系统的porm.xml中)

第六章---匹配系统(下)_第39张图片
对于Player类,需要考虑三个属性:用户名,积分值,等待时间

第六章---匹配系统(下)_第40张图片

1)MatchingPool线程

对于MatchingPool,是一个多线程的类,需要继承Thread

  • 创建一个列表players保存玩家
  • 添加玩家的函数
  • 删除玩家的函数

由于players变量多个线程(匹配线程,传入参数线程)共用,因此这个变量涉及到读写冲突,因此就需要加锁。还要注意,从列表中删除元素的时候,要注意重新判断该位置。

第六章---匹配系统(下)_第41张图片
对于匹配系统而言,由于全局只有一个匹配线程,因此将其定义成静态变量,放在MatchingServiceImpl中。

第六章---匹配系统(下)_第42张图片
同时在MatchingPool开一个线程,需要重写Threadrun()。对于线程的执行,我们期望周期性的执行,判断当前所有玩家中有没有匹配的。写一个死循环,Thread.sleep(1000),每1秒中自动执行一遍。对于每一名玩家而言,每等待一秒,对应的waitingTime就会加一,相应的匹配阈值就会变大。

第六章---匹配系统(下)_第43张图片
matchPlayers()中,尝试匹配所有玩家

第六章---匹配系统(下)_第44张图片

注意,java中的break:跳出当前循环;但是如果是嵌套循环,则只能跳出当前的这一层循环,只有逐层break才能跳出所有循环。continue:终止当前循环,但是不跳出循环(在循环中continue后面的语句是不会执行了),继续往下根据循环条件执行循环。

以上用到的辅助函数

第六章---匹配系统(下)_第45张图片

2)MatchingSystem发送请求

对于sendResult,负责将匹配的两名玩家作为参数返回到backend

也就是这个过程

第六章---匹配系统(下)_第46张图片
因为也要用到RestTemplateConfig

所以将backend.config.RestTemplateConfig文件复制到matchingsystem.config.RestTemplateConfig

为了能让RestTemplateConfig中的Bean注入进来,添加@Component

第六章---匹配系统(下)_第47张图片
注入之后,就可以使用RestTemplateConfig来进行SpringBoot服务之间的通信

注意要加端口号

第六章---匹配系统(下)_第48张图片

3)Backend接收请求

为了能将匹配的a和b作为参数返回到backend,我们需要在backend写一个接收信息的方法

对于这样的一个方法而言,同样的一个流程

  • service
  • service.impl
  • controller

变动的文件如下,除了SecurityConfig,其他的均为新增文件

第六章---匹配系统(下)_第49张图片
StartGameService.java

第六章---匹配系统(下)_第50张图片
StartGameServiceImpl.java

第六章---匹配系统(下)_第51张图片
其中的WebSocketServer.startGame(aId, bId)

内容为:

public static void startGame(Integer aId, Integer bId){
    User userA = userMapper.selectById(aId);
    User userB = userMapper.selectById(bId);
    Game game = new Game(13,14,20, userA.getId(), userB.getId());
    game.createMap();

    //game是属于A和B两个玩家 因此需要赋值给A和B两名玩家对应的连接上
    userConnectionInfo.get(userA.getId()).game = game;
    userConnectionInfo.get(userB.getId()).game = game;

    game.start();//开辟一个新的线程
    JSONObject respGame = new JSONObject();
    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());//两名玩家的地图一致

    //分别给userA和userB传送消息告诉他们匹配成功了
    //通过userA的连接向userA发消息
    JSONObject respA = new JSONObject();
    respA.put("event","start-matching");
    respA.put("opponent_username",userB.getUsername());
    respA.put("opponent_photo",userB.getPhoto());
    respA.put("game",respGame);
    WebSocketServer webSocketServer1 = userConnectionInfo.get(userA.getId());//获取user1的连接
    webSocketServer1.sendMessage(respA.toJSONString());

    //通过userB的连接向userB发消息
    JSONObject respB = new JSONObject();
    respB.put("event","start-matching");
    respB.put("opponent_username",userA.getUsername());
    respB.put("opponent_photo",userA.getPhoto());
    respB.put("game",respGame);
    WebSocketServer webSocketServer2 = userConnectionInfo.get(userB.getId());
    webSocketServer2.sendMessage(respB.toJSONString());
}

StartGameController.java

第六章---匹配系统(下)_第52张图片
SecurityConfig.java,对于"/pk/start/game/"这样一个URL,只允许本地调用。
第六章---匹配系统(下)_第53张图片
这样backend端的接收函数就实现了

4)匹配池启动

这样一个匹配池线程我们选择在Springboot启动之前随之启动

第六章---匹配系统(下)_第54张图片

5)Debug调试

启动MatchingSystem所对应的SpringBoot服务,可以看到,每秒就会执行一次matchPlayers()

第六章---匹配系统(下)_第55张图片
现在前端一个玩家点击“匹配”按钮

第六章---匹配系统(下)_第56张图片
可以看到对应的waitingTime每隔一秒加1

第六章---匹配系统(下)_第57张图片
点击取消之后,就会删除。

之后测试两个用户

第六章---匹配系统(下)_第58张图片
很快实现匹配

第六章---匹配系统(下)_第59张图片
为了便于测试,将两名玩家的分值差距调大。

第六章---匹配系统(下)_第60张图片
分差100,根据匹配规则,需要满足与自己的分值差距,小于自己的等待时间*10,

r a t i n g D e l t a < = w a i t i n g T i m e ∗ 10 ; ( r a t i n g D e l t a = 100 ) ratingDelta <= waitingTime * 10; (ratingDelta = 100) ratingDelta<=waitingTime10;(ratingDelta=100)

意味着

w a i t i n g T i m e > = 10 waitingTime >= 10 waitingTime>=10

因此,需要两名玩家的等待时间都>=10的时候,两者匹配。

在这里插入图片描述
测试结果如下:

第六章---匹配系统(下)_第61张图片

6)老板模式

有些时候玩家匹配成功之后,游戏过程中,突然老板进来了,然后此时立刻关闭网页。也就是不通过请求的方式想匹配系统发起取消匹配,而是直接断开连接。

也就是针对:玩家在匹配池,但是玩家已经断开连接

第六章---匹配系统(下)_第62张图片
第六章---匹配系统(下)_第63张图片
如上,报异常的原因是因为,userConnectionInfo.get(userA.getId())返回的是一个空对象,然后空对象是没有game属性的,所以会报错。

因此这里需要加一些判断。如果已经断开连接,还是将其匹配到一起,但是6秒之内没有接收到操作就会判输。

WebSocketServer.java

第六章---匹配系统(下)_第64张图片
第六章---匹配系统(下)_第65张图片
同时,consumer.utils.Game.java也要添加判断

第六章---匹配系统(下)_第66张图片
如果已经断开连接,还是将其匹配到一起(不报异常),但是6秒之内没有接收到操作就会判输。

你可能感兴趣的:(SpringBoot,spring)