微服务可以理解为在SpringBoot之外的另外一个Server,负责处理一段独立的功能,与SpringBoot Server之间通过http通信。在游戏的匹配系统,之前是简单粗暴的放在一个集合上,当集合元素大于2时,取出两名玩家进行匹配,无法适应更加复杂的场景,因此现在要将这段程序独立出来。
微服务有多种实现方式,这里采用SpringCloud。SpringCloud和SpringBoot都相当于一个Web Server两者之间通过Http通信
由于SpringCloud实现的匹配系统和SpringBoot实现的游戏后端是并列的,因此项目结构需要改动。
在SpringCloud
项目中添加依赖: Maven仓库地址 spring-cloud-dependencies
创建SpringCloud
的子项目——matchingsystem
然后需要配置一些依赖。
matchingsystem
本质上也是一个springboot
,所以需要将父级目录backendcloud
中porm.xml
中的依赖,SpringWeb
依赖,直接复制到子项目matchingsystem
对应的porm.xml
创建application.properties
,配置端口,由于游戏后端backend
中是3000
,这里设置为3001
对于匹配系统而言,需要实现的接口中需要有两个函数,addPlayer
和removePlayer
用于添加和删除玩家
创建接口,就需要创建controller
负责调用接口,service
负责声明接口,service.impl
负责实现接口
为了方便调试,暂时不实现具体功能。
MatchingService.java
MatchingServiceImpl.java
MatchingController.java
注意,对于MultiValueMap
结构而言,运行一个key
对应的多个value
,并用数组保存
map.getFirst("user_id")
表示user_id
所对应的value
数组中,第一个值001
此时MatchingController
还有一个问题是,没有授权验证,这样就有外网通过其他请求来恶意攻击的风险。
与之前backend
一样,添加Spring Security
依赖
照着之前的代码,只用到configure
这一段代码,且不涉及Jwt-token
的验证,直接抄过来修改
我们期望的的是只能被后端服务器访问,而不能以其他方式访问。解决这个问题,可以根据IP地址来判断,也就是只能通过本地,而不能通过其他地方来访问。
如下图,对"/player/add/"
和"/player/remove/"
的请求放开
只有IP地址为本地(127.0.0.1)
的Server
发出的请求才是有效的。
将原来的Main
函数,重命名为MatchingSystemApplication
,作为Springboot
的入口
这样就能够跑起来(只不过此时由于是POST的类型的请求,不支持通过浏览器直接请求)
由于请求是游戏后端backend发起的,因此还需要将两部分对接起来。
现在需要创建一个新的子项目,将之前的逻辑装在backendclound下面
在backendcloud
下面创建SpringCloud
的子项目——Backend
然后,将之前的后端backend
项目的src
整个文件夹直接复制过来。
复制到backendcloud
下面的子模块backend
下面
然后将后端backend
项目下的porm.xml
中的依赖也粘贴到下面这个位置
其中的thymeleaf
用不到,删除即可。
目前的项目结构如下:
首先要打通
需要将下面这段修改,目前还只是简单粗暴的进行从集合中取出User
修改如下:
将与matchpool
相关的所有操作都删掉
然后在startMatching()
调用时向MatchingSystem
发请求,申请为玩家匹配对手,在stopMatching()
和onClose()
调用时向MatchingSystem
发请求,申请取消玩家的匹配,
配置RestTemplate
向MatchingSystem
发请求,需要借助Springboot
中的一个工具RestTemplate
,它可以在两个Spring进程之间进行通信。
先配置一下这个工具,如果希望在WebSocketServer.java
中使用RestTemplate
,就需要加Bean
注解,这样才能够取出来。
可以理解为,需要用到某个工具的时候,就定义一个它的Configuration
,加一个注解Bean
,返回一个它的实例。
这样在未来使用的时候,就可以通过@Autowired
将其注入
原理是,如果加了@Autowired
,就会看一下这个接口(或者Service)是否有一个唯一的注解为Bean
的函数和它对应,如果有的话就调用这个函数,将返回值赋过来。
现在看下怎么用。
在具体用之前,需要修改下数据库。将rating
字段,将bot
表中,移动到user
表中
同样,修改这两个表对应的pojo
并且在所用调用User
和Bot
构造函数的时候修改
然后进入正题。
使用RestTemplate
借助RestTemplate
向MatchingSystem
发请求
启动Backend模块对应的Spring服务
此时能够在服务中看到,启动了两个SpringBoot
Backend
对应的端口号为3000
MatchingSystem
对应的端口号为3001
用户登录之后,进行匹配,在MatchingSystem
对应的控制台下面add player1 1500
表示匹配系统接收到了来自后端的玩家ID为6的匹配请求。
当点击取消时,同样成功接收到了请求
控制台中看到的输出,是在匹配系统中实现的输出。
至此,游戏后端,向匹配系统发请求这一过程就完成。
接下来要实现匹配系统内部的逻辑.
匹配系统在接收到来自游戏后端的匹配请求之后,会将当前参与匹配的所有用户,放在一个池子(数组)里面。开辟额外新线程,每隔1s就扫描一遍整个数组,将能够匹配的玩家匹配到一起。我们期望匹配相近分值的玩家,随着时间的推移,可以逐步放宽分值要求,也就是允许两名匹配玩家的分值差距较大,直到所有玩家都可以在规定时间内匹配在一块为止。具体来说,第一秒,匹配分值差距10以内的玩家,第二秒,匹配分值差距20以内的玩家…直到匹配完成为止。
现在需要将之前关于线程的那部分再重复一遍。
创建service.impl.utils.MatchingPool
用于维护这样一个线程,同时创建service.impl.utils.Player
来存储玩家(需要提前将Lombok
依赖添加到匹配系统的porm.xml
中)
对于Player
类,需要考虑三个属性:用户名,积分值,等待时间
对于MatchingPool
,是一个多线程的类,需要继承Thread
players
保存玩家由于players
变量多个线程(匹配线程,传入参数线程)共用,因此这个变量涉及到读写冲突,因此就需要加锁。还要注意,从列表中删除元素的时候,要注意重新判断该位置。
对于匹配系统而言,由于全局只有一个匹配线程,因此将其定义成静态变量,放在MatchingServiceImpl
中。
同时在MatchingPool
开一个线程,需要重写Thread
的run()
。对于线程的执行,我们期望周期性的执行,判断当前所有玩家中有没有匹配的。写一个死循环,Thread.sleep(1000)
,每1秒中自动执行一遍。对于每一名玩家而言,每等待一秒,对应的waitingTime
就会加一,相应的匹配阈值就会变大。
在matchPlayers()
中,尝试匹配所有玩家
注意,java中的break:跳出当前循环;但是如果是嵌套循环,则只能跳出当前的这一层循环,只有逐层break才能跳出所有循环。continue:终止当前循环,但是不跳出循环(在循环中continue后面的语句是不会执行了),继续往下根据循环条件执行循环。
以上用到的辅助函数
对于sendResult
,负责将匹配的两名玩家作为参数返回到backend
也就是这个过程
因为也要用到RestTemplateConfig
所以将backend.config.RestTemplateConfig
文件复制到matchingsystem.config.RestTemplateConfig
为了能让RestTemplateConfig
中的Bean
注入进来,添加@Component
注入之后,就可以使用RestTemplateConfig
来进行SpringBoot
服务之间的通信
注意要加端口号
为了能将匹配的a和b作为参数返回到backend
,我们需要在backend
写一个接收信息的方法
对于这样的一个方法而言,同样的一个流程
service
service.impl
controller
变动的文件如下,除了SecurityConfig
,其他的均为新增文件
StartGameService.java
StartGameServiceImpl.java
其中的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
SecurityConfig.java
,对于"/pk/start/game/"
这样一个URL,只允许本地调用。
这样backend
端的接收函数就实现了
这样一个匹配池线程我们选择在Springboot启动之前随之启动
启动MatchingSystem
所对应的SpringBoot
服务,可以看到,每秒就会执行一次matchPlayers()
现在前端一个玩家点击“匹配”按钮
可以看到对应的waitingTime
每隔一秒加1
点击取消之后,就会删除。
之后测试两个用户
很快实现匹配
为了便于测试,将两名玩家的分值差距调大。
分差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<=waitingTime?10;(ratingDelta=100)
意味着
w a i t i n g T i m e > = 10 waitingTime >= 10 waitingTime>=10
因此,需要两名玩家的等待时间都>=10的时候,两者匹配。
测试结果如下:
有些时候玩家匹配成功之后,游戏过程中,突然老板进来了,然后此时立刻关闭网页。也就是不通过请求的方式想匹配系统发起取消匹配,而是直接断开连接。
也就是针对:玩家在匹配池,但是玩家已经断开连接
如上,报异常的原因是因为,userConnectionInfo.get(userA.getId())
返回的是一个空对象,然后空对象是没有game
属性的,所以会报错。
因此这里需要加一些判断。如果已经断开连接,还是将其匹配到一起,但是6秒之内没有接收到操作就会判输。
WebSocketServer.java
同时,consumer.utils.Game.java
也要添加判断
如果已经断开连接,还是将其匹配到一起(不报异常),但是6秒之内没有接收到操作就会判输。