思维导图
更改项目结构
查看该历史版本
实现匹配池类
进入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() { }
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); 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;
import java.util.Iterator; import java.util.concurrent.CopyOnWriteArraySet;
@Component
@ServerEndpoint("/websocket/{token}") public class WebSocketServer { ...
...
@OnClose public void onClose() { System.out.println("disconnected!"); if(this.user != null) { users.remove(this.user.getId()); } }
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.start();
users.get(a.getId()).game = game; users.get(b .getId()).game = game;
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()); }
private void startMatching() { System.out.println("start matching!"); }
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
@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;
@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) { 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(); } }
|
匹配池向后端返回匹配结果
匹配池判断匹配成功的逻辑
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() { 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()]; 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; 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("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()); }
|
函数会根据玩家的userId
在users
里找出该名玩家,显然这个返回值是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); }
|