微服务可以理解为在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
注意,对于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
中的依赖也粘贴到下面这个位置
目前的项目结构如下:
首先要打通
需要将下面这段修改,目前还只是简单粗暴的进行从集合中取出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
发请求
此时能够在服务中看到,启动了两个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
就会加一,相应的匹配阈值就会变大。
注意,java中的break:跳出当前循环;但是如果是嵌套循环,则只能跳出当前的这一层循环,只有逐层break才能跳出所有循环。continue:终止当前循环,但是不跳出循环(在循环中continue后面的语句是不会执行了),继续往下根据循环条件执行循环。
以上用到的辅助函数
对于sendResult
,负责将匹配的两名玩家作为参数返回到backend
也就是这个过程
所以将backend.config.RestTemplateConfig
文件复制到matchingsystem.config.RestTemplateConfig
为了能让RestTemplateConfig
中的Bean
注入进来,添加@Component
注入之后,就可以使用RestTemplateConfig
来进行SpringBoot
服务之间的通信
注意要加端口号
为了能将匹配的a和b作为参数返回到backend
,我们需要在backend
写一个接收信息的方法
对于这样的一个方法而言,同样的一个流程
service
service.impl
controller
变动的文件如下,除了SecurityConfig
,其他的均为新增文件
其中的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()
之后测试两个用户
分差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