6.1 实现微服务:匹配系统(上

master
barney 2 years ago
parent 42074fb132
commit 1b4d8d808a
  1. 13
      backend/pom.xml
  2. 7
      backend/src/main/java/com/kob/backend/config/SecurityConfig.java
  3. 18
      backend/src/main/java/com/kob/backend/config/WebSocketConfig.java
  4. 146
      backend/src/main/java/com/kob/backend/consumer/WebSocketServer.java
  5. 97
      backend/src/main/java/com/kob/backend/consumer/utils/Game.java
  6. 21
      backend/src/main/java/com/kob/backend/consumer/utils/JwtAuthentication.java
  7. 64
      web/src/assets/scripts/GameMap.js
  8. 5
      web/src/components/GameMap.vue
  9. 100
      web/src/components/MatchGround.vue
  10. 2
      web/src/store/index.js
  11. 32
      web/src/store/pk.js
  12. 70
      web/src/views/pk/PkIndexView.vue
  13. 1
      web/src/views/user/account/UserLoginView.vue

@ -74,6 +74,19 @@
<scope>runtime</scope> <scope>runtime</scope>
</dependency> </dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
<version>2.7.2</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>2.0.11</version>
</dependency>
<dependency> <dependency>

@ -9,6 +9,7 @@ import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod; import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.config.http.SessionCreationPolicy;
@ -48,4 +49,10 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter {
http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class); http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
} }
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/websocket/**");
}
} }

@ -0,0 +1,18 @@
package com.kob.backend.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
/**
* @author zfp
*/
@Configuration
public class WebSocketConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}

@ -0,0 +1,146 @@
package com.kob.backend.consumer;
import com.alibaba.fastjson.JSONObject;
import com.kob.backend.consumer.utils.Game;
import com.kob.backend.consumer.utils.JwtAuthentication;
import com.kob.backend.controller.pojo.User;
import com.kob.backend.mapper.UserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.Iterator;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArraySet;
/**
* @author zfp
*/
@Component
@ServerEndpoint("/websocket/{token}") // 注意不要以'/'结尾
public class WebSocketServer {
// 保存所有用户对应的请求链接
// 线程安全的一个HashMap
final private static ConcurrentHashMap<Integer,WebSocketServer> users = new ConcurrentHashMap<>();
// 保存当前的匹配池
final private static CopyOnWriteArraySet<User> matchpool = new CopyOnWriteArraySet<>();
// 发起请求链接对应的用户
private User user;
private Session session = null;
// 注入userMapper(和之前的有点不一样)
private static UserMapper userMapper;
// // 保存地图
// private Game game = null;
@Autowired
public void setUserMapper(UserMapper userMapper) {
WebSocketServer.userMapper = userMapper;
}
@OnOpen
public void onOpen(Session session, @PathParam("token") String token) throws IOException {
// 建立连接
this.session = session;
// 根据token查询用户id(JWT验证)
Integer userId = JwtAuthentication.getUserId(token);
// 根据userId查找对应的用户
this.user = userMapper.selectById(userId);
// 连接成功
if (this.user != null) {
users.put(userId,this);
System.out.println("Connected Success!");
}else {
this.session.close();
}
System.out.println(users);
}
@OnClose
public void onClose() {
// 关闭链接
System.out.println("Disconnected!");
if (this.user != null) {
users.remove(this.user.getId());
matchpool.remove(this.user);
}
}
private void startMatching() {
System.out.println("start-----matching----");
matchpool.add(this.user);
System.out.println(matchpool.size());
while (matchpool.size() >= 2) {
Iterator<User> it = matchpool.iterator();
User a = it.next(), b = it.next();
matchpool.remove(a);
matchpool.remove(b);
Game game = new Game(13,14,20);
game.createMap();
JSONObject respA = new JSONObject();
respA.put("event","start-matching");
respA.put("opponent_username",b.getUsername());
respA.put("opponent_photo",b.getPhoto());
respA.put("gamemap",game.getG());
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("gamemap",game.getG());
users.get(b.getId()).sendMessage(respB.toJSONString());
}
}
private void stopMatching() {
System.out.println("stop-----matching----");
matchpool.remove(this.user);
}
@OnMessage
public void onMessage(String message, Session session) {
// 从Client接收消息(读取前端发来的信息)
System.out.println("Received the message!");
JSONObject data = JSONObject.parseObject(message);
String event = data.getString("event");
if ("start-matching".equals(event)) {
startMatching();
}
else if ("stop-matching".equals(event)){
stopMatching();
}
}
@OnError
public void onError(Session session, Throwable error) {
error.printStackTrace();
}
public void sendMessage(String msg) {
// 给前端发送信息
synchronized (this.session) {
try {
this.session.getBasicRemote().sendText(msg);
} catch (IOException e) {
e.printStackTrace();
}
}
}
}

@ -0,0 +1,97 @@
package com.kob.backend.consumer.utils;
import java.util.Random;
/**
* @author zfp
*/
public class Game {
final private Integer rows;
final private Integer cols;
final private Integer inner_walls_count;
final private int[][] g;
final private static int[] dx = {-1,0,1,0};
final private static int[] dy = {0,1,0,-1};
public Game(Integer rows,Integer cols,Integer inner_walls_count) {
this.rows = rows;
this.cols = cols;
this.inner_walls_count = inner_walls_count;
this.g = new int[rows][cols];
}
public int[][] getG() {
return g;
}
private boolean check_connectivity(int sx,int sy,int tx,int ty) {
if (sx == tx && sy == ty) {
return true;
}
g[sx][sy] = 1;
for (int i = 0; i < 4; i++) {
int x = sx + dx[i] , y = sy + dy[i];
if (x >= 0 && x < this.rows && y >= 0 && y < this.cols && g[x][y] == 0 ) {
if (check_connectivity(x,y,tx,ty)) {
g[sx][sy] = 0;
return true;
}
}
}
g[sx][sy] = 0;
return false;
}
// 绘制地图是否成功
private boolean draw() {
for (int i = 0; i < this.rows; i++) {
for (int j = 0; j < this.cols; j++) {
// 0表示空地,1表示障碍物
g[i][j] = 0;
}
}
// 四周加上障碍物
for (int r = 0; r < this.rows; r++) {
g[r][0] = g[r][this.cols - 1] = 1;
}
for (int c = 0; c < this.cols; c++) {
g[0][c] = g[this.rows - 1][c] = 1;
}
// 创建随机障碍物
Random random = new Random();
for (int i = 0; i < this.inner_walls_count / 2; i++) {
for (int j = 0; j < 1000; j++) {
int r = random.nextInt(this.rows);
int c = random.nextInt(this.cols);
// 判断对称的位置是否有障碍物
if (g[r][c] == 1 || g[this.rows - 1 - r][this.cols - 1 - c] == 1){
continue;
}
// 不要生成到蛇初始的位置上
if (r == this.rows - 2 && c == 1 || r == 1 && c == this.cols - 2) {
continue;
}
// 生成的位置没有问题
g[r][c] = g[this.rows - 1 - r][this.cols - 1 - c] = 1;
break;
}
}
return this.check_connectivity(this.rows - 2, 1,1,this.cols - 2);
}
public void createMap() {
// 最多随机1000次,判断能够生成有效的地图
for (int i = 0; i < 1000; i++) {
if (draw()) {
break;
}
}
}
}

@ -0,0 +1,21 @@
package com.kob.backend.consumer.utils;
import com.kob.backend.utils.JwtUtil;
import io.jsonwebtoken.Claims;
/**
* @author zfp
*/
public class JwtAuthentication {
public static Integer getUserId(String token) {
// 返回值为-1时表示不存在
int userId = -1;
try {
Claims claims = JwtUtil.parseJWT(token);
userId = Integer.parseInt(claims.getSubject());
} catch (Exception e) {
throw new RuntimeException(e);
}
return userId;
}
}

@ -3,7 +3,7 @@ import { Snake } from "./Snake";
import { Wall } from "./Wall"; import { Wall } from "./Wall";
export class GameMap extends AcGameObject { export class GameMap extends AcGameObject {
constructor(ctx, parent) { constructor(ctx, parent,store) {
super(); // 继承类一直要先调用父类的构造函数 super(); // 继承类一直要先调用父类的构造函数
this.ctx = ctx; this.ctx = ctx;
@ -13,6 +13,8 @@ export class GameMap extends AcGameObject {
this.rows = 13; this.rows = 13;
this.cols = 14; this.cols = 14;
this.store = store;
this.inner_walls_count = 20; // 障碍物的数量(最大建议80) this.inner_walls_count = 20; // 障碍物的数量(最大建议80)
this.walls = []; // 所有障碍物组成的数组 this.walls = []; // 所有障碍物组成的数组
@ -24,59 +26,8 @@ export class GameMap extends AcGameObject {
]; ];
} }
check_connectivity(g, sx, sy, tx, ty) { // 判断生成的地图是否可以连通
if (sx == tx && sy == ty) return true;
g[sx][sy] = true;
let dx = [-1, 0, 1, 0], dy = [0, 1, 0, -1];
for (let i = 0; i < 4; i++) {
let x = sx + dx[i], y = sy + dy[i];
if (!g[x][y] && this.check_connectivity(g, x, y, tx, ty))
return true;
}
return false;
}
create_walls() { // 判断是否生成有效的地图 create_walls() { // 判断是否生成有效的地图
const g = []; const g = this.store.state.pk.gamemap;
for (let r = 0; r < this.rows; r++) {
g[r] = [];
for (let c = 0; c < this.cols; c++) {
g[r][c] = false;
}
}
// 给四周加上障碍物
for (let r = 0; r < this.rows; r++) {
g[r][0] = g[r][this.cols - 1] = true;
}
for (let c = 0; c < this.cols; c++) {
g[0][c] = g[this.rows - 1][c] = true;
}
// 创建随机障碍物
for (let i = 0; i < this.inner_walls_count / 2; i++) {
for (let j = 0; j < 1000; j++) {
let r = parseInt(Math.random() * this.rows);
let c = parseInt(Math.random() * this.cols);
// 两个对称的位置都没有障碍物
if (g[r][c] || g[this.rows - 1 - r][this.cols - 1 - c]) continue;
// 不能将障碍物生成到两条蛇的起始位置处(左下,右上)
if (r == this.rows - 2 && c == 1 || r == 1 && c == this.cols - 2)
continue;
// 每次将障碍物生成到对称的两个位置
g[r][c] = g[this.rows - 1 - r][this.cols - 1 - c] = true;
break;
}
}
const copy_g = JSON.parse(JSON.stringify(g));
// 如果生成的地图无法连通,则返回false,需要重新生成
if (!this.check_connectivity(copy_g, this.rows - 2, 1, 1, this.cols - 2))
return false;
for (let r = 0; r < this.rows; r++) { for (let r = 0; r < this.rows; r++) {
for (let c = 0; c < this.cols; c++) { for (let c = 0; c < this.cols; c++) {
@ -86,8 +37,6 @@ export class GameMap extends AcGameObject {
} }
} }
} }
return true;
} }
add_listening_events() { // 绑定监听事件 add_listening_events() { // 绑定监听事件
@ -110,10 +59,7 @@ export class GameMap extends AcGameObject {
start() { start() {
// 尝试1000次,直到找到符合条件的地图为止 this.create_walls();
for (let i = 0; i < 1000; i++)
if (this.create_walls())
break;
this.add_listening_events(); this.add_listening_events();
} }

@ -8,14 +8,15 @@
<script> <script>
import { GameMap } from "@/assets/scripts/GameMap"; // jsGameMap(public) import { GameMap } from "@/assets/scripts/GameMap"; // jsGameMap(public)
import { ref, onMounted } from 'vue' import { ref, onMounted } from 'vue'
import { useStore } from 'vuex'
export default { export default {
setup() { setup() {
let parent = ref(null); let parent = ref(null);
let canvas = ref(null); let canvas = ref(null);
const store = useStore();
onMounted(() => { onMounted(() => {
new GameMap(canvas.value.getContext('2d'), parent.value) new GameMap(canvas.value.getContext('2d'), parent.value,store)
}); });
return { return {

@ -0,0 +1,100 @@
<template>
<div class="matchground">
<div class="row">
<div class="col-6">
<div class="user_photo">
<img :src="$store.state.user.photo" alt="">
</div>
<div class="username">
<span class="info">用户名:&nbsp;&nbsp;</span>{{ $store.state.user.username }}
</div>
</div>
<div class="col-6">
<div class="user_photo">
<img :src="$store.state.pk.opponent_photo" alt="">
</div>
<div class="username">
<span class="info">用户名:&nbsp;&nbsp;</span>{{ $store.state.pk.opponent_username }}
</div>
</div>
<div class="col-12" style="text-align: center; padding-top: 10vh;">
<button type="button" v-if="is_matched" class="btn btn-success btn-lg" @click="click_match_btn">{{match_btn_info}}</button>
<button type="button" v-else class="btn btn-danger btn-lg" @click="click_match_btn">{{match_btn_info}}</button>
</div>
</div>
</div>
</template>
<script>
import { ref } from 'vue'
import {useStore} from 'vuex'
export default {
setup() {
const store = useStore();
let match_btn_info = ref("开始匹配");
let is_matched = ref(true);
const click_match_btn = () => {
if (match_btn_info.value === "开始匹配") {
match_btn_info.value = "取消匹配";
is_matched.value = false;
// json
store.state.pk.socket.send(JSON.stringify({
event: "start-matching",
}));
}else {
match_btn_info.value = "开始匹配";
is_matched.value = true;
store.state.pk.socket.send(JSON.stringify({
event: "stop-matching",
}));
}
}
return {
match_btn_info,
is_matched,
click_match_btn,
}
},
}
</script>
<style scoped>
.matchground {
width: 60vw;
height: 70vh;
margin: 40px auto;
background-color: rgba(123, 197, 212, 0.5);
}
.info {
font-size: 24px;
color: black;
}
img {
margin-top: 10vh;
border-radius: 50%;
width: 20vh;
}
.user_photo {
text-align: center;
}
.username {
margin-top: 40px;
font-size: 24px;
font-weight: bold;
color:crimson;
text-align: center;
}
</style>

@ -1,5 +1,6 @@
import { createStore } from 'vuex' import { createStore } from 'vuex'
import ModuleUser from "./user" // 导入user.js import ModuleUser from "./user" // 导入user.js
import ModulePk from "./pk"
export default createStore({ export default createStore({
state: { state: {
@ -12,5 +13,6 @@ export default createStore({
}, },
modules: { modules: {
user: ModuleUser, user: ModuleUser,
pk: ModulePk,
} }
}) })

@ -0,0 +1,32 @@
export default {
state: { // 全局变量
status: "matching", // matching表示匹配界面,playing表示对战界面
socket: null,
opponent_username: "", //对手的用户名
opponent_photo: "", // 对手的头像
gamemap: null, // 对战的地图
},
getters: {
},
mutations: { // 用于修改全局数据
updateSocket(state,socket) {
state.socket = socket;
},
updateOpponent(state,opponent) {
state.opponent_username = opponent.username;
state.opponent_photo = opponent.photo;
},
updateStatus(state,status) {
state.status = status;
},
updateGameMap(state,gamemap) {
state.gamemap = gamemap;
},
},
actions: { // 在actions中调用修改全局变量的函数
},
modules: {
}
}

@ -1,16 +1,70 @@
<template> <template>
<PlayGround/> <!-- status等于playing才进入对战界面 -->
<PlayGround v-if="$store.state.pk.status === 'playing'" />
<MatchGround v-if="$store.state.pk.status === 'matching'" />
</template> </template>
<script> <script>
import PlayGround from '../../components/PlayGround.vue' import PlayGround from "../../components/PlayGround.vue";
import MatchGround from "../../components/MatchGround.vue";
import { onMounted, onUnmounted } from "vue"; //
import { useStore } from "vuex";
export default { export default {
components: { components: {
PlayGround PlayGround,
} MatchGround,
} },
setup() {
const store = useStore();
const socket_url = `ws://localhost:3000/websocket/${store.state.user.token}/`;
let socket = null;
// (pk)
// onMountedsetup
onMounted(() => {
store.commit("updateOpponent", {
username: "my opponent",
photo:
"https://cdn.acwing.com/media/article/image/2022/08/09/1_1db2488f17-anonymous.png",
});
// socket
socket = new WebSocket(socket_url);
socket.onopen = () => {
store.commit("updateSocket", socket);
console.log("connnected!");
};
//
socket.onmessage = (msg) => {
const data = JSON.parse(msg.data);
//
if (data.event === "start-matching") {
console.log(data)
store.commit("updateOpponent", {
username: data.opponent_username,
photo: data.opponent_photo,
});
setTimeout(() => {
store.commit("updateStatus", "playing");
}, 2000);
//
store.commit("updateGameMap", data.gamemap);
}
};
socket.onclose = () => {
console.log("disconnected!");
store.commit("updateStatus", "matching");
};
});
// pk
onUnmounted(() => {
socket.close();
});
},
};
</script> </script>
<style scoped> <style scoped></style>
</style>

@ -73,7 +73,6 @@ export default {
store.dispatch("getInfo",{ store.dispatch("getInfo",{
success() { success() {
router.push({name:"home"}); router.push({name:"home"});
console.log(store.state.user);
} }
}); });
}, },

Loading…
Cancel
Save