本节实现功能
实现匹配系统的微服务,并通过父级项目进行管理两个微服务
思路细化
- 新建父级项目
- 因为父级项目没有逻辑只是管理两个微服务,所以整理父级项目,删除父级项目的src,配置父级项目的pom
- 两个微服务模块的创建,先创建匹配微服务模块
- 对匹配微服务的pom进行配置,还有端口的配置
- 匹配微服务已经创建完成,现在要搭建匹配微服务的功能
- 在匹配微服务中创建一个service,impl,controller,由于匹配 玩家可以点击开始匹配,也可以点击取消匹配,所以应当实现的逻辑为添加一名用户和删除一名用户(也就是开始和取消)
- 由于Spring Cloud是http请求,所以可能会接收到用户的伪请求,matchingsystem只能对于后端请求,因此需要防止外部请求,通过Spring Security来实现权限控制(也就是在创建的SecurityConfig中修改只能后端访问的链接)
- 通过导入配置信息实现相应的功能,分别导入父级导入,匹配微服务导入,入口启动配置(也就是后端的启动)
- 创建网页后端模块,直接将原本的网页后端src复制过来,再导入相应的配置
- 两个模块基本创建完毕,下面要实现两个后端之间的通信,先是网页后端向匹配后端发送信息所以创建RestTemplate类进行通信
- 在WebSocketServer中封装游戏开始的startGame,将游戏开始与开始匹配独立开来
- 为了提高人的体验感,所以给人加上战力,通过战力进行匹配,修改user数据库,加上rating,在bot数据库删去rating,对后端相应的代码进行更改
- 通过网页后端向匹配微服务发送请求,所以要在网页后端中实现开始匹配和取消匹配,通过Map储存信息并发往匹配微服务和对应链接下的操作
- 然后就是匹配微服务接受信息并处理,然后再发往网页后端
- 在接受信息时引入依赖lombok maven,在匹配时要用到用户的三个信息,id,rating,等待时间
- 创建匹配池(service/impl/utils/MatchingPool),实现相关的匹配逻辑
- 匹配服务接受网页后端的请求(添加用户匹配或用户取消匹配),创建这两个类,同时通过这两个类向匹配池发送信息
- 启动匹配线程
- 匹配微服务通过创建matchingsystem/config/RestTemplateConfig类,将信息发送给网页后端
- 网页后端接受匹配微服务反馈的信息,同样在网页后端创建一个service,impl,controller,接受匹配微服务传过来的用户的id
- 在网页后端通过SecurityConfig实现权限控制的放行
- 为了增强程序的稳定性,对于用户非法离开不能继续匹配时进行特判(也就是看匹配微服务师傅能正常传入用户的id)
实现匹配系统的微服务
- 微服务概念 及 本项目匹配思路
每个服务是一个独立的程序,每个服务之间可以相互通信。此项目中,BackEnd 和 MatchSystem都是一个微服务!
当BackEnd接收到玩家开始匹配的请求,BackEnd会想MatchSystem发送http请求,通知MatchSystem开始匹配,
MatchSystem会单独开一个线程开始匹配,匹配逻辑为:每秒都会扫描匹配池中的所有玩家,判断玩家能否相互匹
配,若能,则发送http请求给BackEnd。此项目使用的微服务技术是Spring Cloud。Spring Cloud技术涉及的
很多不只是服务间的相关通信,还包括网关、负载均衡等优化配置,由于项目并发量没那么大,因此未使用这些技
术。
- Spring Cloud和Thrift的区别
Spring Cloud:每个服务都是一个Server,每个Server相互通信使用的是http协议。
Thrift:用Thrift生成的微服务,BackEnd与服务之间的通信为Socket协议,比http快一些。
1. 新建微服务父级项目
2. 整理父级项目
- 删除父级项目的src,父级项目没有逻辑,只是管理backend backnedcloud 两个微服务
- 配置父级项目的pom
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.2</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.kob</groupId>
<artifactId>backendcloud</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>backendcloud</name>
<description>backendcloud</description>
<!-- 修改处1 -->
<packaging>pom</packaging>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- 修改处2 -->
<!-- https://mvnrepository.com/artifact/org.springframework.cloud/spring-cloud-dependencies -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>2021.0.3</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<!-- 修改处3 -->
<version>2.7.2</version>
</plugin>
</plugins>
</build>
</project>
- 新建模块matchisystem
- matchingsystem 的pom管理
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>backendcloud</artifactId>
<groupId>com.kob</groupId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<groupId>com.kob.matchingsystem</groupId>
<artifactId>matchingsystem</artifactId>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<!-- 修改处 -->
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<modules>
<module>matchingsystem</module>
</modules>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.2</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.kob</groupId>
<artifactId>backendcloud</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>backendcloud</name>
<description>backendcloud</description>
<packaging>pom</packaging>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<!-- 删除了两个srping boot dependency -->
<!-- https://mvnrepository.com/artifact/org.springframework.cloud/spring-cloud-dependencies -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>2021.0.3</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>2.7.2</version>
</plugin>
</plugins>
</build>
</project>
- 端口的配置
在matchingsystem下找到resource创建application.properties,将端口号修改 server.port=3001
3. 匹配系统的搭建
- 创建接口(创建一个MatchingService)
matchingsystem/service/MatchingService.java
package com.kob.matchingsystem.service;
public interface MatchingService {
String addPlayer(Integer userId, Integer rating);
String removePlayer(Integer userId);
}
- 实现接口(创建一个MatchingServiceImpl)
matchingsystem/service/impl/MatchingServiceImpl.java
package com.kob.matchingsystem.service.impl;
import com.kob.matchingsystem.service.MatchingService;
import org.springframework.stereotype.Service;
@Service
public class MatchingServiceImpl implements MatchingService {
@Override
public String addPlayer(Integer userId, Integer rating) {
System.out.println("Add Player: " + userId + " " + rating);
return "add player success";
}
@Override
public String removePlayer(Integer userId) {
System.out.println("Remove Player: " + userId);
return "Remove player success";
}
}
- 控制器
matchingsystem/controller/MatchingController.java
package com.kob.matchingsystem.controller;
import com.kob.matchingsystem.service.MatchingService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.MultiValueMap;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.Objects;
@RestController
public class MatchingController {
@Autowired
private MatchingService matchingService;
// 参数不能使用普通map,MultiValueMap和普通map的区别时,这个是一个键对应多个值
@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);
}
}
- 权限控制
由于Spring Cloud是http请求,所以可能会接收到用户的伪请求,matchingsystem只能对于后端请求,因此需要防止外部请求,通过Spring Security来实现权限控制。
- 导入配置
父级导入
backendcloud/pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>backendcloud</artifactId>
<groupId>com.kob</groupId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<groupId>com.kob.matchingsystem</groupId>
<artifactId>matchingsystem</artifactId>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- 此处修改 -->
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<version>2.7.1</version>
</dependency>
</dependencies>
</project>
匹配微服务导入
matchingsystem/config/SecurityConfig.java
package com.kob.matchingsystem.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
// 放行这两个接口
.antMatchers("/player/add/", "/player/remove/").hasIpAddress("127.0.0.1")
.antMatchers(HttpMethod.OPTIONS).permitAll()
.anyRequest().authenticated();
}
}
入口启动配置
matchingsystem/MatchSystemApplication.java
package com.kob.matchingsystem;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class MatchSystemApplication {
public static void main(String[] args) {
SpringApplication.run(MatchSystemApplication.class, args);
}
}
可以简单测试一下
4. 后端转化成微服务
创建一个新的模块backend
将backend的src目录删掉,将原本后端的src目录复制过来
* 对backend pom进行修改
backend/pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>backendcloud</artifactId>
<groupId>com.kob</groupId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<groupId>com.kob.backend</groupId>
<artifactId>backend</artifactId>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-jdbc -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
<version>2.7.1</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.projectlombok/lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.24</version>
<scope>provided</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.29</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.baomidou/mybatis-plus-boot-starter -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.2</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.baomidou/mybatis-plus-generator -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>3.5.3</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<version>2.7.1</version>
</dependency>
<!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt-api -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>
<!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt-impl -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt-jackson -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.github.penggle</groupId>
<artifactId>kaptcha</artifactId>
<exclusions>
<exclusion>
<artifactId>javax.servlet-api</artifactId>
<groupId>javax.servlet</groupId>
</exclusion>
</exclusions>
<version>2.3.2</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-websocket -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
<version>2.7.2</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.alibaba/fastjson -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>2.0.11</version>
</dependency>
</dependencies>
</project>
5. 实现后端两个微服务之间的通信
* 封装startGame
package com.kob.backend.consumer;
// 1.删除以下两个包
import java.util.Iterator;
import java.util.concurrent.CopyOnWriteArraySet;
@Component
@ServerEndpoint("/websocket/{token}")
public class WebSocketServer {
...
// 2.删除matchingPool变量
...
@OnClose
public void onClose() {
// 关闭链接
System.out.println("disconnected!");
if(this.user != null) {
users.remove(this.user.getId());
// 3. 删除取消匹配逻辑
}
}
// 4.抽取匹配成功后的逻辑为一个函数
public static void startGame(Integer aId, Integer bId) {
User a = userMapper.selectById(aId), b = userMapper.selectById(bId);
Game game = new Game(13, 14, 20, a.getId(), b.getId());
game.createMap();
// 一局游戏一个线程,会执行game类的run方法
game.start();
users.get(a.getId()).game = game;
users.get(b .getId()).game = game;
JSONObject respGame = new JSONObject();
// 玩家的id以及横纵信息
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());
// 发送给A的信息
JSONObject respA = new JSONObject();
respA.put("event", "start-matching");
respA.put("opponent_username", b.getUsername());
respA.put("opponent_photo", b.getPhoto());
respA.put("game", respGame);
// 通过userId取出a的连接,给A发送respA
users.get(a.getId()).sendMessage(respA.toJSONString());
// 发送给B的信息
JSONObject respB = new JSONObject();
respB.put("event", "start-matching");
respB.put("opponent_username", a.getUsername());
respB.put("opponent_photo", a.getPhoto());
respB.put("game", respGame);
// 通过userId取出b的连接,给B发送respB
users.get(b.getId()).sendMessage(respB.toJSONString());
}
// 5.删除原先匹配逻辑,把匹配的逻辑交给另一个服务,服务成功后再调用startGame函数开始游戏逻辑
private void startMatching() {
System.out.println("start matching!");
}
// 6.删除取消匹配逻辑
private void stopMatching() {
System.out.println("stop matching");
}
...
}
- 配置RestTemplate(后端两个微服务实现通信需要依靠这个配置类)
RestTemplate是Spring提供的用于访问Rest服务的客户端,RestTemplate提供了多种便捷访问远程Http服务的方法,能够大大提高客户端的编写效率。
backend/config/RestTemplateConfig.java
package com.kob.backend.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;
@Configuration
public class RestTemplateConfig {
@Bean
public RestTemplate getRestTemplate() {
return new RestTemplate();
}
}
- 修改数据库,在user中引入rating,在bot中删除rating(通过rating进行排名)
- 修改相应的代码
backend/pojo/User.java
package com.kob.backend.pojo;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {
@TableId(type = IdType.AUTO)
private Integer id;
private String username;
private String password;
private String photo;
private Integer rating;
}
backend/pojo/Bot.java
package com.kob.backend.pojo;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Date;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Bot {
@TableId(type = IdType.AUTO)
private Integer id;
private Integer userId;
private String title;
private String description;
private String content;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone="Asia/Shanghai")
private Date createtime;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone="Asia/Shanghai")
private Date modifytime;
}
backend/service/impl/user/account/RegisterServiceImpl.java
package com.kob.backend.service.impl.user.account;
...
@Service
public class RegisterServiceImpl implements RegisterService {
...
@Override
public Map<String, String> register(String username, String password, String confirmedPassword) {
...
// 修改处:增加rating为1500的分数
User user = new User(null, username, encodedPassword, photo, 1500);
...
}
}
backend/service/impl/user/bot/AddServiceImpl.java
package com.kob.backend.service.impl.user.bot;
...
@Service
public class AddServiceImpl implements AddService {
@Autowired
private BotMapper botMapper;
@Override
public Map<String, String> add(Map<String, String> data) {
...
Bot bot = new Bot(null, user.getId(), title, description, content, now, now);
...
}
}
backend/service/impl/user/bot/UpdateServiceImpl
package com.kob.backend.service.impl.user.bot;
...
@Service
public class UpdateServiceImpl implements UpdateService {
@Autowired
private BotMapper botMapper;
@Override
public Map<String, String> update(Map<String, String> data) {
...
Bot new_bot = new Bot(
bot.getId(),
user.getId(),
title,
description,
content,
bot.getCreatetime(),
new Date()
);
...
}
}
- 在backEnd中发送给匹配微服务 添加玩家和删除玩家 的请求
backend/consumer/WebSocketServer.java
package com.kob.backend.consumer;
...
import org.springframework.web.client.RestTemplate;
import org.springframework.util.MultiValueMap;
import org.springframework.util.LinkedMultiValueMap;
@Component
@ServerEndpoint("/websocket/{token}")
public class WebSocketServer {
private static RestTemplate restTemplate;
private final static String addPlayerUrl = "http://127.0.0.1:3001/player/add/";
private final static String removePlayerUrl = "http://127.0.0.1:3001/player/remove/";
...
@Autowired
public void setRestTemplate(RestTemplate restTemplate) {
WebSocketServer.restTemplate = restTemplate;
}
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);
}
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);
}
...
}
测试一下
6. 匹配微服务的实现
* 引入依赖lombok maven
matchingsystem/pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>backendcloud</artifactId>
<groupId>com.kob</groupId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<groupId>com.kob.matchingsystem</groupId>
<artifactId>matchingsystem</artifactId>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<version>2.7.1</version>
</dependency>
<!-- 修改处 -->
<!-- https://mvnrepository.com/artifact/org.projectlombok/lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.24</version>
<scope>provided</scope>
</dependency>
</dependencies>
</project>
- 匹配服务也有玩家的概念,创建一个Player类
matchingsystem/service/impl/utils/Player.java
package com.kob.matchingsystem.service.impl.utils;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Player {
private Integer userId;
private Integer rating;
private Integer waitingTime;
}
- 匹配服务接受网页后端的请求(添加玩家或删除玩家)
matchingsystem/service/impl/MatchingServiceImpl.java
package com.kob.matchingsystem.service.impl;
...
import com.kob.matchingsystem.service.impl.utils.MatchingPool;
@Service
public class MatchingServiceImpl implements MatchingService {
public static final 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 success";
}
@Override
public String removePlayer(Integer userId) {
System.out.println("Remove Player: " + userId);
matchingPool.removePlayer(userId);
return "Remove player success";
}
}
- 发送消息给backEnd消息配置类:RestTemplate.java
matchingsystem/config/RestTemplateConfig
package com.kob.matchingsystem.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;
@Configuration
public class RestTemplateConfig {
@Bean
public RestTemplate getRestTemplate() {
return new RestTemplate();
}
}
- 创建匹配池
匹配池:匹配分数最接近的玩家,根据匹配时间增长,匹配范围逐渐增大。
操作:包括添加玩家,删除玩家,匹配玩家,发送给后端匹配成功的结果。
策略:为了防止匹配时间过长,优先将先匹配的玩家优先匹配,防止用户流失。
matchingsystem/service/impl/utils/MatchingPool.java
package com.kob.matchingsystem.service.impl.utils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.locks.ReentrantLock;
// 匹配池是多线程的
@Component
public class MatchingPool extends Thread {
private static List<Player> players = new ArrayList<>();
private ReentrantLock lock = new ReentrantLock();
private static RestTemplate restTemplate;
private static final String startGameUrl = "http://127.0.0.1:3000/pk/start/game/";
@Autowired
public void setRestTemplate(RestTemplate restTemplate) {
MatchingPool.restTemplate = restTemplate;
}
public void addPlayer(Integer userId, Integer rating) {
// 在多个线程(匹配线程遍历players时,主线程调用方法时)会操作players变量,因此加锁
lock.lock();
try {
players.add(new Player(userId, rating, 0));
} finally {
lock.unlock();
}
}
public void removePlayer(Integer userId) {
// 在多个线程(匹配线程遍历players时,主线程调用方法时)会操作players变量,因此加锁
lock.lock();
try {
List<Player> newPlayers = new ArrayList<>();
for(Player player : players) {
if(!player.getUserId().equals(userId)) {
newPlayers.add(player);
}
}
players = newPlayers;
} finally {
lock.unlock();
}
}
private void increaseWaitingTime() { // 将所有当前玩家的等待时间 + 1
for(Player player : players) {
player.setWaitingTime(player.getWaitingTime() + 1);
}
}
private boolean checkMatched(Player a, Player b) { // 判断两名玩家是否匹配
// 获取两名分差
int ratingDelta = Math.abs(a.getRating() - b.getRating());
// min: 若取min则代表两者分差都小于 等待时间 * 10,实力差距最接近
// max: 若取max则代表有一方分差小于 等待时间 * 10,实力差距可能会大
int waitingTime = Math.min(a.getWaitingTime(), b.getWaitingTime());
return ratingDelta <= waitingTime * 10;
}
private void sendResult(Player a, Player b) { // 返回匹配结果
System.out.println("send result: " + a + " " + b);
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);
}
private void matchPlayers() { // 尝试匹配所有玩家
System.out.println("match players: " + players.toString());
// 标记是否被匹配
boolean[] used = new boolean[players.size()];
// 先枚举等待最久的玩家,恰好是players前面的玩家等待的的久
for(int i = 0; i < players.size(); i++) {
if(used[i]) continue;
// i只要和比i大的匹配,就正好所有玩家匹配一次
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;
}
@Override//每秒实现匹配
public void run() {
while(true) {
try {
Thread.sleep(1000);
// 涉及到操作players变量,加锁;
lock.lock();
try {
increaseWaitingTime();
matchPlayers();
} finally {
lock.unlock();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
- 启动匹配线程
matchingsystem/MatchSystemApplication.java
package com.kob.matchingsystem;
import com.kob.matchingsystem.service.impl.MatchingServiceImpl;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class MatchSystemApplication {
public static void main(String[] args) {
MatchingServiceImpl.matchingPool.start(); // 启动匹配线程
SpringApplication.run(MatchSystemApplication.class, args);
}
}
7. 网页后端接受匹配微服务的消息
* 创建接口
backend/service/pk/StartGameService.java
package com.kob.backend.service.pk;
public interface StartGameService {
String startGame(Integer aId, Integer bId);
}
- 实现接口
backend/service/impl/pk/StartGameServiceImpl.java
package com.kob.backend.service.impl.pk;
import com.kob.backend.consumer.WebSocketServer;
import com.kob.backend.service.pk.StartGameService;
import org.springframework.stereotype.Service;
@Service
public class StartGameServiceImpl implements StartGameService {
@Override
public String startGame(Integer aId, Integer bId) {
System.out.println("start gameL: " + aId + " " + bId);
WebSocketServer.startGame(aId, bId);
return "start game success";
}
}
- 控制器
backend/controller/pk/StartGameController.java
package com.kob.backend.controller.pk;
import com.kob.backend.service.pk.StartGameService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.MultiValueMap;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.Objects;
@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);
}
}
- 权限控制(对Match System服务放行接口)
package com.kob.backend.config;
...
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
...
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
// 放行这两个接口
.antMatchers("/user/account/token/", "/user/account/register/").permitAll()
.antMatchers("/pk/start/game/").hasIpAddress("127.0.0.1") // 增加此行
.antMatchers(HttpMethod.OPTIONS).permitAll()
.anyRequest().authenticated();
http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
}
...
}
- 增加程序的稳定性:若一方停电或者断网,加以特判
backend/consumer/WebSocketServer.java
package com.kob.backend.consumer;
...
@Component
@ServerEndpoint("/websocket/{token}") // 注意不要以'/'结尾
public class WebSocketServer {
...
public static void startGame(Integer aId, Integer bId) {
User a = userMapper.selectById(aId), b = userMapper.selectById(bId);
Game game = new Game(13, 14, 20, a.getId(), b.getId());
game.createMap();
// 一局游戏一个线程,会执行game类的run方法
game.start();
if(users.get(a.getId()) != null)//特判是不是因外影响而导致不能匹配
users.get(a.getId()).game = game;
if(users.get(b.getId()) != null)
users.get(b .getId()).game = game;
JSONObject respGame = new JSONObject();
// 玩家的id以及横纵信息
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());
// 发送给A的信息
JSONObject respA = new JSONObject();
respA.put("event", "start-matching");
respA.put("opponent_username", b.getUsername());
respA.put("opponent_photo", b.getPhoto());
respA.put("game", respGame);
// 通过userId取出a的连接,给A发送respA
if(users.get(a.getId()) != null)
users.get(a.getId()).sendMessage(respA.toJSONString());
// 发送给B的信息
JSONObject respB = new JSONObject();
respB.put("event", "start-matching");
respB.put("opponent_username", a.getUsername());
respB.put("opponent_photo", a.getPhoto());
respB.put("game", respGame);
// 通过userId取出b的连接,给B发送respB
if(users.get(b.getId()) != null)
users.get(b.getId()).sendMessage(respB.toJSONString());
}
...
}
backend/consumer/utils/Game.java
package com.kob.backend.consumer.utils;
...
public class Game extends Thread {
...
private void senAllMessage(String message) {
if(WebSocketServer.users.get(playerA.getId()) != null)
WebSocketServer.users.get(playerA.getId()).sendMessage(message);
if(WebSocketServer.users.get(playerB.getId()) != null)
WebSocketServer.users.get(playerB.getId()).sendMessage(message);
}
...
}