本阶段是将整个Spring项目的匹配系统的微服务收尾,
如下图所示是之前所提到的匹配系统的一整个通信逻辑
这次要实现的是一个完整的通信服务。
前章:介绍微服务
首先先来介绍一下什么是微服务:微服务是一个独立的程序,可以认为是又另起了一个WebSocketServer/SpringBoot
,当获取到两名玩家的匹配信息之后,会向Matching Server
的后台服务器发送一个http
请求。当接收到http
请求的时候会到哪都开一个线程。matching
的过程其实是类似于整个游戏的处理过程。每隔一秒钟会去扫描当前的已有的所有玩家,并且去判断这些玩家能否相互之间匹配出来,如果能够匹配,就把结果返回,返回结果的工具也是通过http
,这样的就实现了一个微服务。实现微服务用到的工具是SpringCloud
但是由于整个项目的负载量并没有那么大,因此很多复杂逻辑都是没有必要用的,因此其实和SpringCloud
没有特别大的关系`
项目地址
一。创建SpringCloud
服务
要想创建SpringCloud
服务,因此整个项目的结构都会发生变化。
整个后端项目里有两个:匹配项和web项
那么这样就需要创建一个新的父级项目用来装两个并列的子项目
首先先创建一个新的项目,名为backendcloud
,组名为com.kob
1.首先先添加配置
修改pom.xml
添加
<package>pom<package>
然后添加SpringCloud
依赖。
然后在SpringCloud
下创建两个子系统(模块)第一个名为matchingSystem
组ID为com.kob.matchingSystem
1.1 配置模块matchingSystem
添加模块方法如下所示:
首先将这个模块的pom添加一些依赖
注:这个pom.xml
本质上还是一个SpringBoot
也就是说微服务本质上就是一个新的SpringBoot
因此要将Spring web
的依赖剪切出来,放到子目录的pom.xml
里面,让子目录拥有Spring
的依赖
Maven
点击并且刷新。
如果同步时始终不成功,可以查看下面的方法:
解决方法
二。创建匹配系统
匹配系统需要实现两个接口:addPlayer
添加玩家,removePlayer
删除玩家
有两个SpringBoot
的话,那么就要要求每一个SpringBoot
都要有一个独立的端口
将MatchingSystem
设置为端口为3001,然后就是创建controller
、service
、impl
定义接口的全套流程
1.定义接口的全套流程
1.1 定义接口MatchingSystem
接口中有两个函数,一个是往匹配池里添加一名玩家:addPlayers
其中要传入两个东西一个是用户的Id,一个是分值rating,按照冉婷作为匹配原则
另一个函数接口是从匹配池中删除一名玩家:removePlayer(Integer userId)
1.2 实现接口(impl
层)
在impl
文件夹中定义MatchingServiceImpl
添加@Service
注解,并且继承接口以实现接口
快捷键添加实现方法
然后给函数添加一个用作返回的调试语句。
1.3 实现controller
在Controller
软件包中创建类为Matchingcontroller
首先添加注解:@RestController
然后将刚刚的接口注入进来:@Autowired
由于涉及到对于数据的修改,因此需要用到post
请求。@PostMapping('\player\add\')
;
对于addplayer
需要传入两个参数@RequestParam
其中数据的参数格式为MultiValueMap<String,String>
map
这个格式的意思是每个关键字对应一个列表value
那么为什么不用map
原因是因为map
识别不了
从map
将userId
中取出来
userId=Integer.(data.getfirst("userId"));
同样将rating
取出来
rating =Integer.parseInt(data.getFirst("rating"));
再写第二个函数,以实现删除一名玩家的操作
@postMapping("/player/remove/")
在这个函数里取出userId
然后就直接返回
1.4实现config
MatchingServer
按理说应该只能接受外网的请求而在浏览器中去访问是不可行的,这样可能会有一些用户来进行伪操作,来攻击服务器
这样就需要用Spring Security
来添加权限
先添加相关的依赖并且刷新Maven
将之前的backend
文件夹下的SecurityConfig
将里面的内容做下修改,移植到matchingSystem
里的config
里,只保留copfig
函数,因为对于每一个链接的权限控制是在这个函数里面
添加权限控制,由于链接只能允许被后端服务器访问。这样就需要通过Ip地址来判断
.antMatchers("/player/add/","/player/remove/").hasIpAddress("127.0.0.1")
可以先运行一下,重启main
,为了防止冲突更名为MatchingSystemApplication
添加@SpringBotApplication
入口
SpringApplication.run(MatchingSystemApplication.class,args);
输入链接查看是否能够访问到。其中由于/player/add/
的链接方法是post
类型,浏览器是Get
类型,访问不了,因此报错405,/player/remove/
如果没放行报错403,如下图所示:
2.对接前面服务
创建新子项,把前面backend
的逻辑装起来。
根目录下创建新的模块名为bakcend
,修改组ID为com.kob.backend
将src
替换成之前backend
里面的src
pom.xml
也配置成之前的pom.xml
之后Maven
重新刷新
接下来的操作就是要打通http
,然后能够访问回去
3.改后端:打通http
以响应过去
首先为了方便,把匹配逻辑封装成函数.
startGame(Integer a,Integer b)
首先先把a和b取出来,然后将基本逻辑进行一些封装
User a=userMapper.selectById(aId)
匹配池会放到匹配系统模块中。
然后再负责开始匹配的函数:来自MatchServer
的startMatching
中发送一个请求。以表示传一个玩家过去。
在负责取消匹配的函数:stopMatching
发送请求一表示取消匹配。
3.1 定义向后端发送请求的工具—Restemplate
以及使用
首先先建一个config
类RestTemplateConfig
添加注解:@Configuration
对于这个工具,想要取得谁,就添加一个@Bean
注解,返回RestTemplate
public class RestTemplateConfig {
@Bean
public RestTemplate getRestTemplate(){
return new RestTemplate();
}
}
综上所述:这个工具用到时候一般是定义一个@Configuration
再定义一个@Bean
再返回实例。
未来要是用到这个工具类的时候,用一个Autowired
将其注入进来。
回到WebSocketServer
首先先定义一个静态变量
public static RestTemplate restTemplate 这个的作用是发送请求
然后添加注解注入的工具类
public void setRestTemplate(RestTemplate restTemplate){
WebSocketServer.restTemplate=restTemplate;
}
这个的处理原理是什么:@Autowired
看接口是否有唯一的@Bean
函数,如果有就返回函数结果
如何用:
StartMatching()
中,首先要先定下请求类型:MyltiValueMap
添加参数以传过去(参数的名称取决于MatchingController
)
关于rating
的设置,最好是放到个人,这样rating代表个人的战斗力,不断更换bot
可以提升战斗力。
因此更新下数据库,在user
的数据库中添加rating
列,删除掉bot
的rating列,并且设置默认值为1500
然后Pojo
改一下配置
通过发送请求,将天梯积分传给工具类
restTemplate.postForObject(url,参数,返回值class:String.class)
url通过定义一个常量来表达:addPlayerUrl
、removeplayerUrl
删除的方法与之类似
两个模块都启动并且调试:
但是出现报错:
原因在于1用户的rating值为空,因此导致的报错。修改后并且重启,问题解决。
关于匹配规则:要匹配分值比较接近的两名玩家,随着匹配时间的推移,允许分值之差可以越来越大,来让两名玩家匹配。
4.重复创建线程
在impl
中添加utils
来给matchingSystem
模块创建线程。创建类为MatchingPool
.还再创建类去存储玩家Player
给这个模块添加lombok
依赖加进来这样才可以用注解。
4.1添加Player
类
首先注入注解:
@Data
@AllArgsConstructor
@NoArgsConstructor
4.2添加matchpool
匹配池类
由于匹配池是一个多线程的类,因此需要继承自Thread
重写run
方法。
这个类加到MatchingServiceImpl
中
因为全局只有一个线程,所以定义成全局静态变量
private final static MatchingPool matchingPool =new MatchingPool()
线程选择启动的位置一定是一个相对比较合适的位置,比如说在SpringBoot
前启动
MatchingServiceImpl.matchingPool.start()
因为需要把所以的用户存下来,存下来所有的用户需要用到List
链表
List<Player>players =new ArrayList<>()
然后定义两个锁,另外也定义两个函数,一个是添加一名玩家:addPlayer
一个是删除一名玩家:removePlayer
这两个函数由于会出现读写冲突,因此需要加锁。
4.2.1实现addPlayer
函数
先加个锁,然后给用户链接加上userId
与rating
以及等待时间。
4.2.2删除一名玩家
删除相对来说较麻烦。首先需要建一个新的列表,将没有删掉的用户都存下来
List<Player>newPlayers =new ArrayList<>()
枚举所有的Player
,如果要删除的用户的Id与当前枚举到的用户的Id不是一个的话,那么就不用被删。
最后将players
赋值为列表中的元素,更新列表方便后续枚举。
5.实现线程:
线程的执行以周期性的执行方式比较合适
用sleep
函数,保证每一秒中自动执行一遍,每次执行判断能不能配对,如果能配对就配对,不能配对就下一秒进行探索。
每等1秒,使得waiting
时间加1
在构造函数run
中写一个死循环。每一次死循环就先sleep
1秒
Thread.sleep(1000)
这样的sleep函数记得加一个try...catch
实现线程需要加几个辅助函数:
5.1increaseWaitingtime
需要将所有等待的人,如果这些等待的人还没有匹配上,就把等待时间+1
private void increateWaitingTime
在run函数中,每隔1秒中,就去调用,把那些玩家的等待时间+1
5.2 匹配函数matchPlayers
也是在sleep的时间里调用。因为这两个函数都会去操作players
变量,造成读写冲突,因此需要加锁。
开一个boolean
,用来表示当前还剩下哪些人,
boolean[] used =new boolean[players.size()];
为了能够有更好的用户体验,去枚举那些等待时间更长的玩家。因为用户进入匹配池的方法是调用add()
,队列里面越往后的元素时间越新,所以需要从前往后枚举。如果该玩家有能够匹配上的人,那就先匹配,用到两层循环,对于第二层循环,由于前面已经枚举确定好,所以从i+1
开始枚举。取出两名没有匹配的玩家调用check函数,如果可以匹配的话,标注这两个人,然后返回结果并且break
;
具体写法如下:
5.3辅助函数:用来判断两名玩家是否匹配— checkMatched()
public boolean checkMatched(Player a,Player b)
先计算下两名玩家的分差
int ratingDelta=Math.abs(a.getRating()-b.getRating());
再计算下两名玩家的等待时间
int waitingTime =Math.min(a.getWaitingTime(),b.getWaitingTime());
匹配时的原则要满足:a和b的接受程度为:能被a接受的范围和能被b接受的范围
a的范围为:分差<=a的等待时间10
b=范围为:分差<=b的等待时间10
因此只需要判断分差是不是小于等于a和b的等待时间最小值*10
注意:匹配完之后需要将已经用过的玩家删掉
5.4 辅助函数:sendResult
,当能够匹配的时候,用来返回函数结果
WebSockeServer
中需要有一个函数能够接收到信息
5.4.1用方法来接收信息
需要一个方法来能够接收信息
同样的也是Service
、Impl
、Controller
都写一遍
service
中创建一个新的软件包为pk
,impl
中创建一个新的包为pk
在pk
中创建一个接口StartGameService
,其中参数aId
与bId
实现这个接口StartGameServiceImpl
添加@Service
注解,并且继承自接口
加上实现方法,首先先输出一个调试信息。
因为要开始游戏,一开始游戏的api其实在WebSockerServer
中已经写好了,即为WebSocketServer
的startGame
.由于这个函数需要外部调用,因此将其改为public
且全局类型.
在impl
中去调用这个函数。
创建调用接口的controller
,即startGameController
添加注解@RestController
把刚刚写好的接口注入进来。链接传递用到post
方法。定义一个与接口同名的函数,其中传递参数
@RequestParam MultiValueMap<String,String>data
需要把aId
与bId
取出来最后直接返回。
注:要把相关的链接去SecurityConfig
加上对接口链接/pk/start/game/
的放行。而且这里只允许对本地的Id放行,因为要求在未来放到云端之后,只允许本地相互调用。
既然接口已经写完了,那就可以去实现调用。
在MatchPool
类中的sendResult
函数里调用。
调用的话需要用到Resttemplate
来发送请求
为了能够把@Bean
注入进来,将MatchingPool
中的类添加注解@Component
并且添加一个RestTemplate
类
private static RestTemplate restTemplate;
这样就实现了请求的发送
5.4.2 发送请求
发送请求之前首先先构造函数,然后传过去Id
之后结果调用:restTemplate
,传回去url
、data
、String
、class
写法如下所示:
在这之前先定义一个url常量
private final static String startGameUrl ="http://127.0.0.1:3000/pk/start/game/"
运行并且调试,当一名用户进了匹配池:
此时会发现线程每秒钟都会在匹配一遍所有玩家
当添加第一名玩家进入匹配池中,此时会先多出一名玩家
当加入第二个人的时候会瞬间匹配成功
当提升一个人的rating
后会极大的拉大等待时间
6.优化
在匹配的过程中,等待的时间里,当用户选择取消匹配,但是没有向匹配池里发送命令直接断开连接。也就是说匹配池中的两名玩家有可能一名玩家已经不存在了。那么需要做到的是,如果一名玩家不存在,去通知他并且直接判为输。对于这样的优化需要加一些判断,如果不加判断会报异常。
报异常的原因在于因为WebSocektServer
中的玩家已经不存在,去get
一个空对象,如果空对象不存在Game
属性,那么就一定会报错。
这样就需要加一下get
判断,来判断get
获取是否为空,如果获取不为空,则执行下一步操作
当已经有一名用户断开连接。暂且先绑定在一起然后在规定时间内直接判输,以达到相应的效果
所有的WebSocketServer
、Game
类里面都加上相应的判断以执行下一步操作
特例说明
对于一个用户的链接来说,刷新链接会断开连接,但是同样也会去建立一个新的连接,新的连接会重新加到user里面,匹配池重新得到用户并且重新得到请求并且开始和玩家匹配。
但是当换一名玩家,由于匹配依据的关键字是userId
根据rating
得分匹配,因此这原来的两个玩家就不会能够匹配的上了。
举一个例子来说吧,比如bb与ccc用户先行匹配,匹配之后将ccc退掉,ccc换成void用户,这个用户的rating值很高。bb与void再匹配,匹配出来的结果是bb与ccc匹配。原因就在于两者的分差的接近。会先行匹配,如下图所示
那么是否应该实现说完全取消匹配的方法,其实按理说是应该,但是对于一个程序来说也许会有一些特殊情况,需要一个容错空间,比如说某个用户突然断网。在这种情况下不可能说能够去事先感知,那么就需要提供一个容错的空间。以处理突发问题
大佬您好,想问下,spring-cloud-dependencies这个依赖添加了之后,后续在哪里应用到了呢,我去掉这个依赖还是可以启动并进行匹配呀
这个匹配的时候加的依赖主要是进行前后端通信用的,至于为啥去掉还能匹配,个人猜测是backend模块的
WebSocketServer
实现了一个简单的傻瓜式的实现逻辑,从而能够实现匹配吧所以这个依赖的作用是实现matchingsystem和backend两者之间的通信嘛
可以这么说吧
嗯嗯,谢谢大佬了