苏州移动网站建设,以橙色为主的网站,营销型网站建设吉林,找人做自建房图纸去哪个网站笔记内容转载自 AcWing 的 SpringBoot 框架课讲义#xff0c;课程链接#xff1a;AcWing SpringBoot 框架课。 CONTENTS 1. 重构项目1.1 初始化Spring Cloud项目1.2 创建匹配系统框架 2. 实现匹配系统微服务2.1 数据库更新2.2 Web后端与匹配系统后端通信2.3 实现匹配逻辑2.4 …笔记内容转载自 AcWing 的 SpringBoot 框架课讲义课程链接AcWing SpringBoot 框架课。 CONTENTS 1. 重构项目1.1 初始化Spring Cloud项目1.2 创建匹配系统框架 2. 实现匹配系统微服务2.1 数据库更新2.2 Web后端与匹配系统后端通信2.3 实现匹配逻辑2.4 Web后端接收匹配结果 1. 重构项目
1.1 初始化Spring Cloud项目
现在需要把匹配系统设计成一个微服务也就是一个独立的系统可以认为是一个新的 SpringBoot 后端当之前的服务器获取到两名玩家的匹配请求后会向后台的匹配系统服务器发送 HTTP 请求匹配系统类似于之前的 Game在接收到请求之后也会单独开一个新的线程来匹配可以设计成每隔一秒扫一遍匹配池中已有的玩家然后判断能否匹配出来如果可以就将匹配结果通过 HTTP 请求返回。
匹配系统和网站后端是两个并列的后端项目因此可以修改一下项目结构将这两个后端改为子项目然后新建一个新的父级项目。
我们新建一个 Spring 项目项目名为 backendcloud还是选用 Maven 管理项目组名为 com.kob。注意 2023.11.24 之后 SpringBoot2.X 版本正式弃用SpringBoot3.X 版本需要 Java17 及以上。我们现在选择 SpringBoot3.2.0 版本依赖选上 Spring Web 即可。
父级项目是没有逻辑的因此可以把 src 目录删掉然后修改一下 pom.xml首先在 descriptionbackendcloud/description 后添加一行packagingpom/packaging然后添加 Spring Cloud 的依赖前往 Maven 仓库搜索并安装以下依赖
spring-cloud-dependencies
接着在 backendcloud 目录下创建匹配系统子项目选择新建一个模块Module选择空项目匹配系统的名称为 matchingsystem在高级设置中将组 ID 设置为 com.kob.matchingsystem。
这个新建的子项目本质上也是一个 SpringBoot我们将父级目录的 pom.xml 中的 Spring Web 依赖剪切到 matchingsystem 中的 pom.xml。
1.2 创建匹配系统框架
由于有两个 SpringBoot 服务因此需要修改一下匹配系统的端口在 resources 目录下创建 application.properties 文件
server.port3001在 com.kob.matchingsystem 包下创建 controller 和 service 包在 service 包下创建 impl 包。先在 service 包下创建 MatchingService 接口
package com.kob.matchingsystem.service;public interface MatchingService {String addPlayer(Integer userId, Integer rating); // 将玩家添加到匹配池中String removePlayer(Integer userId); // 从匹配池中删除玩家
}然后简单实现一下 MatchingServiceImpl
package com.kob.matchingsystem.service.impl;import com.kob.matchingsystem.service.MatchingService;
import org.springframework.stereotype.Service;Service
public class MatchingServiceImpl implements MatchingService {Overridepublic String addPlayer(Integer userId, Integer rating) {System.out.println(Add Player: userId , Rating: rating);return success;}Overridepublic String removePlayer(Integer userId) {System.out.println(Remove Player: userId);return success;}
}最后在 controller 包下创建 MatchingController
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 {Autowiredprivate MatchingService matchingService;PostMapping(/matching/add/)public String addPlayer(RequestParam MultiValueMapString, String data) { // 注意这边不能用MapInteger userId Integer.parseInt(Objects.requireNonNull(data.getFirst(user_id)));Integer rating Integer.parseInt(Objects.requireNonNull(data.getFirst(rating)));return matchingService.addPlayer(userId, rating);}PostMapping(/matching/remove/)public String removePlayer(RequestParam MultiValueMapString, String data) {Integer userId Integer.parseInt(Objects.requireNonNull(data.getFirst(user_id)));return matchingService.removePlayer(userId);}
}现在需要将这个匹配系统子项目变为 Spring 项目将 Main 改名为 MatchingSystemApplication然后将其修改为 SpringBoot 的入口
package com.kob.matchingsystem.service;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);}
}2. 实现匹配系统微服务
2.1 数据库更新
我们将 rating 放到用户身上而不是 BOT 上每个用户对应一个自己的天梯分。在 user 表中创建 rating并将 bot 表中的 rating 删去然后需要修改对应的 pojo还有 service.impl.user.account 包下的 RegisterServiceImpl 类以及 service.impl.user.bot 包下的 AddServiceImpl 和 UpdateServiceImpl 类。
2.2 Web后端与匹配系统后端通信
先在 backend 项目的 config 包下创建 RestTemplateConfig 类便于之后在其他地方注入 RestTemplate。RestTemplate 能够在应用中调用 REST 服务。它简化了与 HTTP 服务的通信方式统一了 RESTful 的标准封装了 HTTP 链接我们只需要传入 URL 及返回值类型即可
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 {Beanpublic RestTemplate getRestTemplate() {return new RestTemplate();}
}我们将 WebSocketServer 的简易匹配代码删去然后使用 HTTP 请求向 matchingsystem 后端发送匹配请求注意我们将 startGame() 方法改为 public因为之后需要在处理匹配成功的 Service 中调用该方法来启动游戏
package com.kob.backend.consumer;import com.alibaba.fastjson2.JSONObject;
import com.kob.backend.consumer.utils.Game;
import com.kob.backend.consumer.utils.JwtAuthentication;
import com.kob.backend.mapper.RecordMapper;
import com.kob.backend.mapper.UserMapper;
import com.kob.backend.pojo.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;import jakarta.websocket.*;
import jakarta.websocket.server.PathParam;
import jakarta.websocket.server.ServerEndpoint;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;import java.io.IOException;
import java.util.concurrent.ConcurrentHashMap;Component
ServerEndpoint(/websocket/{token}) // 注意不要以/结尾
public class WebSocketServer {// ConcurrentHashMap是一个线程安全的哈希表用于将用户ID映射到WS实例public static final ConcurrentHashMapInteger, WebSocketServer users new ConcurrentHashMap();private User user;private Session session null;private Game game null;private static UserMapper userMapper;public static RecordMapper recordMapper; // 要在Game中调用private static RestTemplate restTemplate; // 用于发送HTTP请求// 向匹配系统发送请求的URLprivate static final String matchingAddPlayerUrl http://127.0.0.1:3001/matching/add/;private static final String matchingRemovePlayerUrl http://127.0.0.1:3001/matching/remove/;Autowiredpublic void setUserMapper(UserMapper userMapper) {WebSocketServer.userMapper userMapper;}Autowiredpublic void setRecordMapper(RecordMapper recordMapper) {WebSocketServer.recordMapper recordMapper;}Autowiredpublic void setRestTemplate(RestTemplate restTemplate) {WebSocketServer.restTemplate restTemplate;}OnOpenpublic void onOpen(Session session, PathParam(token) String token) throws IOException {this.session session;Integer userId JwtAuthentication.getUserId(token);user userMapper.selectById(userId);if (user ! null) {users.put(userId, this);System.out.println(Player user.getId() Connected!);} else {this.session.close();}}OnClosepublic void onClose() {if (user ! null) {users.remove(this.user.getId());System.out.println(Player user.getId() Disconnected!);}stopMatching(); // 断开连接时取消匹配}OnMessagepublic void onMessage(String message, Session session) { // 一般会把onMessage()当作路由JSONObject data JSONObject.parseObject(message);String event data.getString(event); // 取出event的内容if (start_match.equals(event)) { // 开始匹配this.startMatching();} else if (stop_match.equals(event)) { // 取消匹配this.stopMatching();} else if (move.equals(event)) { // 移动move(data.getInteger(direction));}}OnErrorpublic void onError(Session session, Throwable error) {error.printStackTrace();}public void sendMessage(String message) { // 从后端向当前链接发送消息synchronized (session) { // 由于是异步通信需要加一个锁try {session.getBasicRemote().sendText(message);} catch (IOException e) {e.printStackTrace();}}}public void startGame(Integer aId, Integer bId) {User a userMapper.selectById(aId), b userMapper.selectById(bId);game new Game(13, 14, 20, a.getId(), b.getId());game.createMap();users.get(a.getId()).game game;users.get(b.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());JSONObject respA new JSONObject(), respB new JSONObject(); // 发送给A/B的信息respA.put(event, match_success);respA.put(opponent_username, b.getUsername());respA.put(opponent_photo, b.getPhoto());respA.put(game, respGame);users.get(a.getId()).sendMessage(respA.toJSONString()); // A不一定是当前链接因此要在users中获取respB.put(event, match_success);respB.put(opponent_username, a.getUsername());respB.put(opponent_photo, a.getPhoto());respB.put(game, respGame);users.get(b.getId()).sendMessage(respB.toJSONString());}private void startMatching() { // 需要向MatchingSystem发送请求MultiValueMapString, String data new LinkedMultiValueMap();data.add(user_id, String.valueOf(user.getId()));data.add(rating, String.valueOf(user.getRating()));String resp restTemplate.postForObject(matchingAddPlayerUrl, data, String.class); // 参数为请求地址、数据、返回值的Classif (success.equals(resp)) {System.out.println(Player user.getId() start matching!);}}private void stopMatching() { // 需要向MatchingSystem发送请求MultiValueMapString, String data new LinkedMultiValueMap();data.add(user_id, String.valueOf(user.getId()));String resp restTemplate.postForObject(matchingRemovePlayerUrl, data, String.class);if (success.equals(resp)) {System.out.println(Player user.getId() stop matching!);}}private void move(Integer direction) {if (game.getPlayerA().getId().equals(user.getId())) {game.setNextStepA(direction);} else if (game.getPlayerB().getId().equals(user.getId())) {game.setNextStepB(direction);}}
}现在将两个后端项目都启动起来可以在 IDEA 下方的服务Services选项卡的 Add Service 中点击 Run Configuration Type然后选中 Spring Boot这样就能在下方窗口中看到两个 SpringBoot 后端的情况。
尝试在前端中开始匹配可以看到 matchingsystem 后端控制台输出Add Player: 1, Rating: 1500。
2.3 实现匹配逻辑
匹配系统需要将当前正在匹配的用户放到一个匹配池中然后开一个新线程每隔一段时间去扫描一遍匹配池将能够匹配的玩家匹配在一起我们的匹配逻辑是匹配两名分值接近的玩家且随着时间的推移两名玩家的分差可以越来越大。
首先需要添加 Project Lombok 依赖我们使用与之前 Web 后端相同的依赖版本
!-- https://mvnrepository.com/artifact/org.projectlombok/lombok --
dependencygroupIdorg.projectlombok/groupIdartifactIdlombok/artifactIdversion1.18.30/versionscopeprovided/scope
/dependency在 matchingsystem 项目的 service.impl 包下创建 utils 包然后在其中创建 Player 类
package com.kob.matchingsystem.service.impl.utils;import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;Data
NoArgsConstructor
AllArgsConstructor
public class Player {private Integer userId;private Integer rating;private Integer waitingTime; // 等待时间
}接着创建 MatchingPool 类用来维护我们的这个新线程
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.reactive.function.BodyInserters;
import org.springframework.web.client.RestTemplate;import java.util.*;
import java.util.concurrent.locks.ReentrantLock;Component // 为了在类中能够注入Bean
public class MatchingPool extends Thread {private static ListPlayer players new ArrayList(); // 我们之后会自己加锁因此不需要用线程安全的集合private ReentrantLock lock new ReentrantLock();private static RestTemplate restTemplate;private static final String startGameUrl http://127.0.0.1:3000/pk/startgame/;Autowiredpublic void setRestTemplate(RestTemplate restTemplate) {WebSocketServer.restTemplate restTemplate;}public void addPlayer(Integer userId, Integer rating) {lock.lock();try {// TODO创建一个新的Player添加到players中} finally {lock.unlock();}}public void removePlayer(Integer userId) {lock.lock();try {// TODO将某个Player从players中删掉} finally {lock.unlock();}}private void increaseWaitingTime(Integer waitingTime) { // 将当前所有等待匹配的玩家等待时间加waitingTime秒for (Player player: players) {player.setWaitingTime(player.getWaitingTime() waitingTime);}}private boolean checkMatched(Player a, Player b) { // 判断两名玩家是否能够匹配int ratingDelta Math.abs(a.getRating() - b.getRating()); // 分差int minWatingTime Math.min(a.getWaitingTime(), b.getWaitingTime()); // 等待时间较短的玩家符合匹配要求那么等待时间长的也一定符合要求return ratingDelta minWatingTime * 10; // 每多匹配一秒则匹配的分值范围加10}private void sendResult(Player a, Player b) { // 返回匹配结果给Web后端MultiValueMapString, String data new LinkedMultiValueMap();data.add(a_id, String.valueOf(a.getUserId()));data.add(b_id, String.valueOf(b.getUserId()));String resp restTemplate.postForObject(startGameUrl, data, String.class);}private void matchPlayers() { // 尝试匹配所有玩家SetPlayer used new HashSet(); // 标记玩家是否已经被匹配for (int i 0; i players.size(); i) {if (used.contains(players.get(i))) continue;for (int j i 1; j players.size(); j) {if (used.contains(players.get(j))) continue;Player a players.get(i), b players.get(j);if (checkMatched(a, b)) {used.add(a);used.add(b);sendResult(a, b);break;}}}// TODO从players中移除used中的玩家}Overridepublic void run() {while (true) {try {Thread.sleep(1000);System.out.println(players); // 输出当前匹配池中的玩家lock.lock();try {increaseWaitingTime(1);matchPlayers();} finally {lock.unlock();}} catch (InterruptedException e) {e.printStackTrace();break;}}}
}现在即可将这个线程在 MatchingServiceImpl 中定义出来
package com.kob.matchingsystem.service.impl;import com.kob.matchingsystem.service.MatchingService;
import com.kob.matchingsystem.service.impl.utils.MatchingPool;
import org.springframework.stereotype.Service;Service
public class MatchingServiceImpl implements MatchingService {public static final MatchingPool matchingPool new MatchingPool(); // 全局只有一个匹配线程Overridepublic String addPlayer(Integer userId, Integer rating) {System.out.println(Add Player: userId , Rating: rating);matchingPool.addPlayer(userId, rating);return success;}Overridepublic String removePlayer(Integer userId) {System.out.println(Remove Player: userId);matchingPool.removePlayer(userId);return success;}
}可以在启动 matchingsystem 项目的时候就将该线程启动即在 MatchingSystemApplication 这个主入口处启动
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 MatchingSystemApplication {public static void main(String[] args) {MatchingServiceImpl.matchingPool.start(); // 启动匹配线程SpringApplication.run(MatchingSystemApplication.class, args);}
}2.4 Web后端接收匹配结果
我们的 Web 后端还需要从 matchingsystem 接收请求即接收匹配系统匹配成功的信息。在 backend 项目的 service 以及 service.impl 包下创建 pk 包然后在 service.pk 包下创建 StartGameService 接口
package com.kob.backend.service.pk;public interface StartGameService {String startGame(Integer aId, Integer bId);
}然后在 service.impl.pk 包下创建接口的实现 StartGameServiceImpl
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 {Overridepublic String startGame(Integer aId, Integer bId) {System.out.println(Start Game: Player aId and Player bId);WebSocketServer webSocketServer WebSocketServer.users.get(aId);webSocketServer.startGame(aId, bId);return success;}
}接着在 controller.pk 包下创建 StartGameController
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 {Autowiredprivate StartGameService startGameService;PostMapping(/pk/startgame/)public String startGame(RequestParam MultiValueMapString, 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);}
}实现完最后别忘了在 SecurityConfig 中放行这个 URL。