这一章终于完了,但是收尾工作真的好难呀QAQ,可能是我初学的缘故,有些JAVA方面的特性不是很清楚,只能依葫芦画瓢地模仿着用。特别是JAVA的注解,感觉好多但又不是很懂其中的原理,只知道要在某个时候用某个注解,我真是有够菜的()
以我拙见,JAVA注解大概分为两类
一类是使用Bean,即是把已经在xml文件中配置好的Bean拿来用,完成属性、方法的组装;比如@Autowired , @Resource,可以通过byTYPE(@Autowired)、byNAME(@Resource)的方式获取Bean;
一类是注册Bean,@Component , @Repository , @ Controller , @Service , @Configration这些注解都是把你要实例化的对象转化成一个Bean,放在IoC容器中,等你要用的时候,它会和上面的@Autowired , @Resource配合到一起,把对象、属性、方法完美组装。
我感觉注册类的功能都是差不多的,可能只是由于写程序的时候业务逻辑的不同,而把它定义为不同的名字(这里我不太了解,可能说的不太严谨)。
具体业务逻辑大致可以归类如下:
@controller
:标注控制层,也可以理解为接收请求处理请求的类。
@service
:标注服务层,也就是内部逻辑处理层。
@repository
:标注数据访问层,也就是用于数据获取访问的类(组件)。
@component
其他不属于以上三类的类,但是会同样注入spring容器以被获取使用。它的作用就是实现bean的注入
@AutoWired
就是在你声明了注册类后,可以用该注解注入进当前写的类中。
凡是子类及带属性、方法的类都注册Bean到Spring中,交给它管理;@Bean
用在方法上,告诉Spring容器,你可以从下面这个方法中拿到一个Bean。调用的时候和@Component
一样,用@Autowired
调用有@Bean
注解的方法,多用于第三方类无法写@Component
的情况。
根据上一part的设计逻辑,我们可以用微服务去代替之前调试用的匹配系统,使匹配系统功能更加完善。
微服务:是一个独立的程序,可以认为是另起了一个新的springboot。
我们把这个新的springboot叫做Matching System
作为我们的匹配系统,与之对应的是Matching Server
,即匹配的服务器后端。
当我们之前的springboot也就是游戏对战的服务器后端backend Server
获取了两个匹配的玩家信息后,会向Matching Server
服务器后端发送一个http
请求,而当Matching Server
接收到了请求后,会开一个独立的线程Matching
开始进行玩家匹配。
匹配逻辑也非常简单,即每隔1s会扫描当前已有的所有玩家,判断当前玩家的rating
是否相近,能否匹配出来,若能匹配出来则将结果返回给backend Server
(通过http
返回)
实现手法:Spring Cloud
backendcloud
我们项目的结构会出现变化,要先创建一个新的springboot项目backendcloud
作为父项目,包含两个并列的子项目Matching System
和backend
。
注意:backendcloud
创建时要引入Spring Web
依赖,不然的话后面自己要在pom.xml
里手动添加!
因为父级项目是不用写逻辑的,可以把他的整个src文件删掉。
配置pom.xml
<packaging>pom</packaging>
加上Spring Cloud
依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>2021.0.3</version>
<type>pom</type>
<scope>import</scope>
</dependency>
在backendcloud
项目文件夹下创建两个模块:MatchingSystem
, backend
,相当于两个并列的springboot项目。
配置pom.xml
将父项目里的spring web
依赖转移到Matching System
的pom.xml
里
配置端口
在resources
文件夹里创建文件application.properties
server.port = 3001
这样Matching System
的端口就是3001
了
匹配服务的实现
和之前写的业务逻辑一样,先写个匹配的服务接口MatchingService,然后在Impl
里实现对应的接口
这里提供参考逻辑:
matchingsystem\service\impl\MatchingServiceImpl.java
@Service
public class MatchingServiceImpl implements MatchingService {
@Override
public String addPlayer(Integer userId, Integer rating) {
System.out.println("add player: " + userId + " " + rating);
return "add player successfully";
}
@Override
public String removePlayer(Integer userId) {
System.out.println("remove player: " + userId);
return "remove player successfully";
}
}
实现匹配的Controller
matchingsystem\controller\MatchingController.java
@RestController
public class MatchingController {
@Autowired
private MatchingService matchingService;
@PostMapping("/player/add/")
public String addPlayer(@RequestParam MultiValueMap<String, String> 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<String, String> data) {
Integer userId = Integer.parseInt(Objects.requireNonNull(data.getFirst("user_id")));
return matchingService.removePlayer(userId);
}
}
注意:这里用的是MultiValueMap
,即一个键值key
可以对应多个value
值,一个key对应一个列表list
定义:MultiValueMap<String, String> valueMap = new LinkedMultiValueMap<>();
这里如果用@Requestparam + map
接收所有参数的话会不严谨,因为若url
返回的是多个参数的话,map
只能接受一个参数,即一个value
,有时候匹配的会返回多个rating
相近的人的结果,这时候如果用map
接收可能会产生一些蜜汁错误,因此用MultiValueMap
的话可以省事点。。。
用到的api:
MultiValueMap.getFirst(key)
返回对应key的value
列表的第一个值。
设置网关
为了防止用户破坏系统,我们应该设置一定的访问权限,让自己的系统更加安全
这里可以仿照之前写过的SecurityConfig
添加spring security
依赖
pom.xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<version>2.7.1</version>
</dependency>
配置SecurityConfig
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
...
.antMatchers("/player/add/","/player/remove/").hasIpAddress("127.0.0.1") //只允许本地访问
...
}
设置Matching System项目的启动入口
MatchingSystemApplication.java
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class MatchingSystemApplication {
public static void main(String[] args){
SpringApplication.run(MatchingSystemApplication.class,args);
}
}
准备工作
将之前写的springboot项目backend
引入进现在的backendcloud
把之前backend
里的src
文件夹粘贴进backendcloud
里的backend
模块中
注意:要同时配置相应的pom.xml
将匹配链接对接到Matching System
向后端发请求
工具:RestTemplate,可以在两个springboot
之间进行通信
为了将RestTemplate取出来,我们要先建立一个config类 用@Configuration
注解
我们想取得谁就要加一个@Bean
注解(前面有提到过)
后面如果要用到这个类的时候,就直接@Autowired
注入进去
backend\config\RestTemplateConfig.java
@Configuration
public class RestTemplateConfig {
@Bean
public RestTemplate getRestTemplate() {
return new RestTemplate();
}
}
Spring的@Bean注解用于告诉方法,产生一个Bean对象,然后这个Bean对象交给Spring管理。 产生这个
Bean
对象的方法Spring只会调用一次,随后这个Spring将会将这个Bean对象放在自己的IOC容器中。@Bean明确地指示了一种方法,什么方法呢?产生一个bean
的方法,并且交给Spring容器管理;从这我们就明白了为啥@Bean是放在方法的注释上了,因为它很明确地告诉被注释的方法,你给我产生一个Bean,然后交给Spring容器,剩下的你就别管了。记住,@Bean就放在方法上,就是让方法去产生一个Bean,然后交给Spring容器。
如上面getRestTemplate()
生成了一个RestTemplate
对象,然后这个RestTemplate
对象交给Spring管理,后面就可以直接@Autowired
注入这个对象了。
backend\consumer\utils\WebSocketServer.java
将之前调试用的matchpoll
删掉
并编写新的匹配逻辑
先将上面写的RestTemplate
类注入进来
private static RestTemplate restTemplate;
@Autowired
public void setRestTemplate(RestTemplate restTemplate) {
WebSocketServer.restTemplate = restTemplate;
}
一些比较感性的理解:当你注入@Autowired
的时候,springboot会调查相应的带有@Configuration
的接口/类,看看是否有对应的带有@Bean
注解的方法,若存在则调用这个函数方法,把返回值赋过来。(似乎与函数名无关,如:getRestTemplate
和setRestTemplate
)
开始匹配服务
首先要把之前的数据库也引入进现在的这个springboot项目中
private void startMatching() {
System.out.println("start matching!");
//向后端发请求
MultiValueMap<String,String> data = new LinkedMultiValueMap<>();
data.add("user_id",this.user.getId().toString());
data.add("rating",this.user.getRating().toString());
restTemplate.postForObject(addPlayerUrl,data,String.class);//发送请求
//(url,数据,返回值类型的class) 反射机制?
}
注:restTemplate.postForObject(addPlayerUrl,data,String.class);
发送请求给Matchin System里的MatchingController
,里面用@RequestParam MultiValueMap<String, String> data
接收传过来的数据data。
删除匹配服务
private void stopMatching() {
System.out.println("stop matching!");
MultiValueMap<String,String> data = new LinkedMultiValueMap<>();
data.add("user_id",this.user.getId().toString());
restTemplate.postForObject(removePlayerUrl,data,String.class);
}
现在我们实现了浏览器向ws端(backend)发送匹配请求,ws端再发送请求给Matching System端
思路:把所有当前匹配的用户放在一个数组(matchinPool)里,每隔1s扫描一遍数组,把rating较接近的两名用户匹配在一起,随着时间的推移,两名用户允许的rating差可以不断扩大,保证了所有用户都可以匹配在一起。
在Impl
文件夹里新建一个utils
工具包,编写MatchingPool.java
和Player.java
类(对应于上面的数组和用户信息)
MatchingPool.java
是一个多线程的类,要继承自Thread
类
public class MatchingPool extends Thread {
private static List<Player> players = new ArrayList<>(); //多个线程公用的,要上锁
//这里不用线程安全的类,因为我们自己会手动加锁把不安全的变为安全的
private final ReentrantLock lock = new ReentrantLock();
public void addPlayer(Integer userId, Integer rating) {
lock.lock();
try {
players.add(new Player(userId, rating, 0));
} finally {
lock.unlock();
}
}
public void removePlayer(Integer userId) {
lock.lock();
try {
players.removeIf(player -> player.getUserId().equals(userId));
} finally {
lock.unlock();
}
}
@Override
public void run() {
}
}
在匹配服务里把实现添加与删除用户的逻辑
MatchingSystem\service\Impl\MatchingServiceImpl.java
public class MatchingServiceImpl implements MatchingService {
public final static 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 successfully";
}
@Override
public String removePlayer(Integer userId) {
System.out.println("remove player: " + userId);
matchingPool.removePlayer(userId);
return "remove player successfully";
}
}
匹配逻辑:搞个无限循环,周期性执行,每次sleep(1000),若没有匹配的人选,则等待时间++,若有匹配的人选则进行匹配。匹配的rating差会随着等待时间而增加(rating差每等待1s则*10)。
匹配原则:为了提高用户体验,等待时间越长的玩家越优先匹配。
即列表players从前往后匹配。用一个标记数组标记有没有匹配过即可,checkMatched()
是判断这两个玩家是否能成功匹配在一起。sendResult()
是发送匹配结果。
private void matchPlayers() { //尝试匹配所有玩家
boolean[] used = new boolean[players.size()];
for (int i = 0; i < players.size(); i++) {
if (used[i]) continue;
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<Player> newPlayers = new ArrayList<>();
for (int i = 0; i < players.size(); i++) {
if (!used[i]) {
newPlayers.add(players.get(i));
}
}
players = newPlayers;
/* for (int i = 0; i < players.size(); i++) { 错误示范
if (used[i]) players.remove(players.get(i));
}*/
}
TIPS:这里标注一下我初学遇到的坑点,ArrayList
循环删除某个元素不能直接循环一遍然后remove
,因为每次循环的时候,ArrayList
的size()
都会改变,所以循环是有问题的,这样只能保证你删掉一个符合要求的元素,而不能实现循环删掉所有符合要求的元素,因此我们要从另一个角度思考问题,用一个新的ArrayList
存放每一个不需要删除的元素,然后原来的引用直接指向新的List即可。
这里也提供另一种实现循环remove的方法:用迭代器Iterator
eg:
Iterator<Player> iterator = players.iterator();
while (iterator.hasNext()) {
if (要删除的条件) iterator.remove();
}
但是我们上面的删除还涉及到used数组,所以迭代器删除法并不适合,所以要用新列表赋值法!!
对于checkMatch
判断两个玩家是否能成功匹配,还要考虑其等待时间,要判断分差能不能小于等于a与b的等待时间的最小值*10即
r
a
t
i
n
g
D
e
l
t
a
<
=
m
i
n
(
w
a
i
t
i
n
g
T
i
m
e
a
,
w
a
i
t
i
n
g
T
i
m
e
b
)
?
10
ratingDelta<=min(waitingTimea,waitingTimeb)?10
ratingDelta<=min(waitingTimea,waitingTimeb)?10
private boolean checkMatched(Player a, Player b) { //判断两名玩家是否匹配
int ratingDelta = Math.abs(a.getRating() - b.getRating());
int waitingTime = Math.min(a.getWaitingTime(), b.getWaitingTime());
return ratingDelta <= waitingTime * 10;
}
我们要在backend
端再写一个接受MatchingSystem
端匹配成功的信息的Service
和相应的Controller
GameStartController.java
@RestController
public class StartGameController {
@Autowired
private StartGameService startGameService;
@PostMapping("/pk/start/game/")
public String startGame(@RequestParam MultiValueMap<String, String> 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);
}
}
GameStartServiceImpl.java
@Service
public class StartGameServiceImpl implements StartGameService {
@Override
public String startGame(Integer aId, Integer bId) {
System.out.println("start game: " + aId + " " + bId);
WebSocketServer.startGame(aId, bId);
return "start game successfully";
}
}
注意:要把上面的路由/pk/start/game/
放行,只能本地访问
SecurityConfig.java
...
.antMatchers("/pk/start/game/").hasIpAddress("127.0.0.1")
...
为了实现springboot之间的通信,我们要像前文一样使用一个Bean类,方法为调用RestTemplate类。即上文的RestTemplateConfig.java
为了能让Spring里面的Bean注入进来,需要在MatchingPool.java
里加上@Component
@Component
...
private static RestTemplate restTemplate;
@Autowired
public void setRestTemplate(RestTemplate restTemplate) {
MatchingPool.restTemplate = restTemplate;
}
...
private void sendResult(Player a, Player b) { // 返回匹配结果给ws端
MultiValueMap<String, String> data = new LinkedMultiValueMap<>();
data.add("a_id", a.getUserId().toString());
data.add("b_id", b.getUserId().toString());
restTemplate.postForObject(startGameURL, data, String.class);
}
...
如果一名玩家开始匹配后断开了连接,按照我们上面的做法,断开连接后的玩家会一直处于匹配池中,这样我们的Matching System
后端会报错,因为我们凡是要获取玩家信息的时候,该玩家已经掉线了,不存在了,会get一个空玩家信息,空信息是没有属性的,而我们后面会调用玩家属性,这是不合理的,肯定会报错的,我们需要修改这个bug:在每次get之前都要判断一下玩家信息是否为空,若不为空再进行下面的逻辑。