背景
在开发实时对战游戏时,匹配系统是连接玩家与对局的核心模块。传统的单体架构将匹配逻辑与游戏主服务耦合在一起,不仅难以扩展,还会因高并发匹配请求而影响核心业务的稳定性。为了解耦、提升系统弹性,采用微服务架构,将匹配系统独立为一个服务,通过 HTTP 接口与主后端(Backend)及 WebSocket 服务协作。
微服务实现匹配系统
根据前文设计逻辑,使用微服务代替调试用的匹配系统,使功能更加完善。微服务是一个独立的程序,可视为新的 SpringBoot 项目。我们将这个新的 SpringBoot 命名为 Matching System,与之对应的是 Matching Server(匹配的服务器后端)。
当游戏对战的服务器后端(backend Server)获取了两个匹配的玩家信息后,会向 Matching Server 发送一个 HTTP 请求。Matching Server 接收到请求后,开启独立线程进行玩家匹配。匹配逻辑简单,每隔 1s 扫描当前已有的所有玩家,判断 rating 是否相近,若能匹配则将结果返回给 backend Server。
项目结构搭建
修改项目结构,将两个后端改为子项目,新建父级项目 backendcloud。
注意:backendcloud 创建时要引入 Spring Web 依赖。
配置 pom.xml:
<packaging>pom</packaging>
加上 Spring Cloud 依赖:
<dependencyManagement>
<dependencies>
<!-- 导入 Spring Cloud BOM -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
在 backendcloud 目录下创建两个模块:MatchingSystem, backend。
接着创建匹配系统子项目 matchingsystem,组 ID 设置为 com.gameforces.matchingsystem。
将父级目录的 pom.xml 中的 Spring Web 依赖剪切到 matchingsystem 中的 pom.xml:
<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>
Matching System 配置
配置 pom.xml,添加 Security 依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<version>2.7.1</version>
</dependency>
配置端口 resources/application.properties:
server.port=3001
编写匹配的服务接口 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 {
@Override
public String addPlayer(Integer userId, Integer rating) {
System.out.println("addPlayer: " + userId + " " + rating);
return "add Player success";
}
@Override
public String removePlayer(Integer userId) {
System.out.println("removePlayer: " + userId);
return "remove Player success";
}
}
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;
@RestController
public class MatchingController {
@Autowired
private MatchingService matchingService;
@PostMapping("/player/add/")
public String addPlayer(@RequestParam MultiValueMap<String, String> data) {
Integer userId = Integer.parseInt(data.getFirst("user_id"));
Integer rating = Integer.parseInt(data.getFirst("rating"));
return matchingService.addPlayer(userId, rating);
}
@PostMapping("/player/remove/")
public String removePlayer(@RequestParam MultiValueMap<String, String> data) {
Integer userId = Integer.parseInt(data.getFirst("user_id"));
return matchingService.removePlayer(userId);
}
}
注意:这里用的是 MultiValueMap,即一个键值 key 可以对应多个 value 值。若 URL 返回多个参数,map 只能接受一个 value,而 MultiValueMap 可以处理列表,避免潜在错误。
设置网关
为了防止用户破坏系统,设置访问权限,配置 SecurityConfig:
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();
}
}
启动入口 MatchingSystemApplication.java:
package com.kob.matchingsystem;
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);
}
}
backend 对接
将之前写的 springboot 项目 backend 引入进现在的 backendcloud。
将匹配链接对接到 Matching System
工具:RestTemplate,用于两个 springboot 之间通信。
建立 config 类 RestTemplateConfig.java:
@Configuration
public class RestTemplateConfig {
public RestTemplate getRestTemplate() {
return new RestTemplate();
}
}
backend\consumer\utils\WebSocketServer.java 中编写新的匹配逻辑 startGame:
private void startGame(Integer aId, Integer bId) {
User a = userMapper.selectById(aId);
User b = userMapper.selectById(bId);
Game 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();
respA.put("event", "start-matching");
respA.put("opponent_username", b.getUsername());
respA.put("opponent_photo", b.getPhoto());
respA.put("game", respGame);
users.get(a.getId()).sendMessage(respA.toJSONString());
JSONObject respB = new JSONObject();
respB.put(, );
respB.put(, a.getUsername());
respB.put(, a.getPhoto());
respB.put(, respGame);
users.get(b.getId()).sendMessage(respB.toJSONString());
}
将 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);
}
实现收到请求后的匹配具体逻辑
思路:把所有当前匹配的用户放在一个数组 (matchinPool) 里,每隔 1s 扫描一遍数组,把 rating 较接近的两名用户匹配在一起。等待时间越长,允许的 rating 差越大。
在 Impl 文件夹里新建 utils 工具包,编写 MatchingPool.java 和 Player.java 类。
MatchingPool.java 继承自 Thread:
package com.kob.matchingsystem.service.impl.utils;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.locks.ReentrantLock;
public class MatchingPool extends Thread {
private static List<Player> players = new ArrayList<>();
private 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 {
List<Player> newPlayers = new ArrayList<>();
for (Player player : players) {
if (!player.getUserId().equals(userId)) {
newPlayers.add(player);
}
}
players = newPlayers;
} finally {
lock.unlock();
}
}
@Override
public void run() {
}
}
MatchingServiceImpl.java:
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 final static MatchingPool matchingPool = new MatchingPool();
@Override
public String addPlayer(Integer userId, Integer rating) {
System.out.println("addPlayer: " + userId + " " + rating);
matchingPool.addPlayer(userId, rating);
return "add Player success";
}
@Override
public String removePlayer(Integer userId) {
System.out.println("removePlayer: " + userId);
matchingPool.removePlayer(userId);
return "remove Player success";
}
}
匹配逻辑:无限循环,周期性执行,sleep(1000)。若没有匹配的人选,则等待时间++;若有匹配的人选则进行匹配。匹配的 rating 差会随着等待时间而增加(rating 差每等待 1s 则 *10)。
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;
}
checkMatch 判断两名玩家是否匹配,考虑其等待时间: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") ...
Matching System 调用 ws 端的接口
为了能让 Spring 里面的 Bean 注入进来,需要在 MatchingPool.java 里加上 @Component。
@Component
public class MatchingPool extends Thread {
// ... other fields ...
private static RestTemplate restTemplate;
@Autowired
public void setRestTemplate(RestTemplate restTemplate) {
MatchingPool.restTemplate = restTemplate;
}
private void sendResult(Player a, Player 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);
}
}
总结
经过以上设计与实现,成功构建了一个基于微服务的匹配系统,并顺利将其对接至主后端和 WebSocket 服务。


