思维导图

实现匹配系统

更改项目结构

查看该历史版本

实现匹配池类

进入matchingsystem这个子项目里面。

service/impl/utils/新建玩家类,因为匹配系统匹配的是玩家,需要这个辅助类。

1
2
3
4
5
6
7
8
9
10
11
12
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/utils下实现匹配池类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
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<>();


public void addPlayer(Integer userId, Integer rating) {

}

public void removePlayer(Integer userId) {

}

private void increaseWaitingTime() {
// 将所有当前玩家的等待时间 + 1
}

private boolean checkMatched(Player a, Player b) {
// 判断两名玩家是否匹配
}

private void sendResult(Player a, Player b) {
// 返回匹配结果
}

private void matchPlayers() {
// 尝试匹配所有玩家
}

@Override
public void run() {
while(true) {
try {
Thread.sleep(1000);
// 涉及到操作players变量,加锁;
lock.lock();
try {
increaseWaitingTime();
matchPlayers();
} finally {
lock.unlock();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}

在项目入口中启动匹配线程

1
2
3
4
5
6
7
8
9
10
11
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);
}
}

匹配池添加,删除用户

后端发送消息

之前在写了一个傻瓜式匹配,需要把和这个有关的代码全部重写

backend/consumer/WebSocketServer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79

package com.kob.backend.consumer;

// 1.删除以下两个包
import java.util.Iterator;
import java.util.concurrent.CopyOnWriteArraySet;

@Component
// url链接:ws://127.0.0.1:3000/websocket/**
@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");
}

...
}

取消完后就可以向匹配系统发送消息了。构造SpringCloud服务之间通信用的是RestTemplate

RestTemplate 是 Spring 提供的用于访问 Rest 服务的客户端,RestTemplate 提供了多种便捷访问远程 Http 服务的方法,能够大大提高客户端的编写效率。

backend/consumer/WebSocketServer编写发送函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import org.springframework.web.client.RestTemplate;
import org.springframework.util.MultiValueMap;
import org.springframework.util.LinkedMultiValueMap;

@Component
// url链接:ws://127.0.0.1:3000/websocket/**
@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);
}

...
}

匹配系统接收并处理

接收

matchingsystem/service/下定义接口MatchingService

1
2
3
4
5
6
7
8
9

package com.kob.matchingsystem.service;

public interface MatchingService {

String addPlayer(Integer userId, Integer rating);

String removePlayer(Integer userId);
}

matchingsystem/impl/下定义实现接口的类MatchingServiceImpl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import com.example.matchingsystem.service.MatchingService;
import com.example.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("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";
}
}

matchingsystem/controller下定义控制器MatchingController类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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 来实现权限控制。具体方法就是只允许后端的IP地址访问。

经过上面的操作,匹配池就能收到后端的请求了。

处理

在匹配池类中实现add方法和remove方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
private static List<Player> players = new ArrayList<>();
private ReentrantLock lock = new ReentrantLock();

public void addPlayer(Integer userId, Integer rating) {
//涉及到操作player,加锁
lock.lock();
try {
players.add(new Player(userId, rating, 0));
} finally {
lock.unlock();
}
}
public void removePlayer(Integer userId) {
//涉及到操作player,加锁
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();
}
}

匹配池向后端返回匹配结果

匹配池判断匹配成功的逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82


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;
}

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()); //分差
int waitingTime = Math.min(a.getWaitingTime(), b.getWaitingTime());
return ratingDelta <= waitingTime * 10;
}

private void matchPlayers() {
System.out.println("matchPlayers: " + players.toString());
//标记是否被匹配
boolean[] vis = new boolean[players.size()];
// 先枚举等待最久的玩家,恰好是players前面的玩家等待的的久
for (int i = 0; i < players.size(); i++) {
for (int j = i + 1; j < players.size(); j++) {
if (vis[j] || vis[i]) continue;
Player a = players.get(i), b = players.get(j);
if (checkMatched(a, b)) {
vis[i] = vis[j] = true;
sendResult(a, b);
break;
}
}
}

//筛出来剩下的玩家
List<Player> newPlayers = new ArrayList<>();
for (int i = 0; i < players.size(); i++) {
if (!vis[i]) {
newPlayers.add(players.get(i));
}
}
players = newPlayers;
}

@Override
public void run() {
while (true) {
try {
Thread.sleep(1000);
lock.lock();
try {
increaseWaitingTime();
matchPlayers();
} finally {
lock.unlock();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}


}

匹配池发送匹配结果

1
2
3
4
5
6
7
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);
}

后端接收并处理

backend/service/pk定义接口StartGameService

1
2
3
public interface StartGameService {
String startGame(Integer aId, Integer bId);
}

backend/service/impl/pk定义StartGameService类实现该接口

由于之前重构WebSocketServer类时,已经把创建新游戏的逻辑单独抽取出来,所以只需要调用该方法就好。

1
2
3
4
5
6
7
8
9
10
11
12
13
import com.example.backend.consumer.WebSocketServer;
import com.example.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 game:" + aId + " " + bId);
WebSocketServer.startGame(aId, bId);
return "start game success";
}
}

backend/controller/pk下定义StartGameController实现控制器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import com.example.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 a_id = Integer.parseInt(Objects.requireNonNull(data.getFirst("a_id")));
Integer b_id = Integer.parseInt(Objects.requireNonNull(data.getFirst("b_id")));
return startGameService.startGame(a_id, b_id);
}
}

异常处理

假设A玩家在匹配成功前断开websocket链接,那么WebSocketServer类中的users表就会删除该玩家的websocketurl。但是,该玩家并没有在匹配系统中的匹配池中删除。

也就是说,该玩家依然会参与匹配,后端依然会接收到该玩家匹配成功的消息。

看下面这个函数

backend/consumer/WebSocketServer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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();
users.get(a.getId()).game = game; //维护用户A的websocket
users.get(b.getId()).game = game; //维护用户B的websocket
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("event", "start-matching");
respb.put("opponent_username", a.getUsername());
respb.put("opponent_photo", a.getPhoto());
respb.put("game", respGame);
users.get(b.getId()).sendMessage(respb.toJSONString()); //向前端发送信息
}

函数会根据玩家的userIdusers里找出该名玩家,显然这个返回值是null。如果不加特判,在上述边界情况下后端就会发生异常。

解决的方法也很简单,在调用users.get(userId)后,判断取出来的玩家是不是非null就行了。如果是null,就不往这个掉线的玩家的前端发信息。

这样的结果是另一名玩家不受影响,游戏照常玩,分数照样加。掉线的玩家就当挂机处理,听起来非常的符合逻辑。

修改backend/consumer/WebSocketServer

1
2
3
4
5
6
7
8
9
10
11
if(users.get(a.getId()) != null)
users.get(a.getId()).game = game;

if(users.get(b.getId()) != null)
users.get(b .getId()).game = game;

if(users.get(a.getId()) != null)
users.get(a.getId()).sendMessage(respA.toJSONString());

if(users.get(b.getId()) != null)
users.get(b.getId()).sendMessage(respB.toJSONString());

修改backend/consumer/utils/Game类的sendAllMessage方法

1
2
3
4
5
6
private void sendAllMessage(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);
}