SpringBoot学习笔记-实现微服务:匹配系统(上)
笔记内容转载自 AcWing 的 SpringBoot 框架课讲义,课程链接:AcWing SpringBoot 框架课。
CONTENTS
- 1. 配置WebSocket
- 2. 前后端WebSocket通信
- 2.1 WS通信的建立
- 2.2 加入JWT验证
- 3. 前后端匹配业务
- 3.1 实现前端页面
- 3.2 实现前后端交互逻辑
- 3.3 同步游戏地图
我们的游戏之后是两名玩家对战,因此需要实现联机功能,在这之前还需要实现一个匹配系统,能够匹配分数相近的玩家进行对战。
想要进行匹配就至少要有两个客户端,当两个客户端都向服务器发送匹配请求后并不会马上得到返回结果,一般会等待一段时间,这个时间是未知的,因此这个匹配是一个异步的过程,对于这种异步的过程或者是计算量比较大的过程我们都会用一个额外的服务来操作。
那么这个额外的用于匹配的服务可以称为 Matching System,这是另外一个程序(进程),当后端服务器接收到前端的请求后就会将请求发送给 Matching System,这个匹配系统维护了一堆用户的集合,它会不断地去匹配分数最接近的用户,当匹配成功一组用户后就会将结果返回给后端服务器,再由后端将匹配结果立即返回给对应的前端。这种服务就被称为微服务,可以用 Spring Cloud 实现。
用以前的 HTTP 请求很难达到这种效果,之前我们是在客户端向后端发送请求,且后端在短时间内就会返回结果,HTTP 请求只能满足这种一问一答式的服务。而我们现在需要实现的效果是客户端发送请求后不知道经过多长时间后端才会返回结果,对于这种情况需要使用 WebSocket 协议(WS),该协议不仅支持客户端向服务器发送请求,也支持服务器向客户端发送请求。
在前端向服务器发送请求后,服务器会维护好一个 WS 链接,这个链接其实就是一个 WebSocketServer 类的实例,所有和这个链接相关的信息都会存到这个类中。
1. 配置WebSocket
我们之前每次刷新网页就会随机生成游戏地图,该过程是在浏览器本地执行的,当我们要实现匹配功能时,地图就不能由两名玩家各自的客户端生成,否则就基本不可能完全一样了。
当匹配成功后应该由服务器端创建一个 Game 任务,将游戏放到该任务下执行,统一生成地图,且判断移动或者输赢等逻辑之后也应该移到后端来执行。
生成好地图后服务器就将地图传给两名玩家的前端,然后等待玩家的键盘输入或者是 Bot 代码的输入,Bot 代码的输入也属于一个微服务。
首先我们先在 pom.xml 文件中添加以下依赖:
spring-boot-starter-websocketfastjson
接着在 config 包下创建 WebSocketConfig 配置类:
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;@Configuration
public class WebSocketConfig {@Beanpublic ServerEndpointExporter serverEndpointExporter() {return new ServerEndpointExporter();}
}
然后我们创建一个 consumer 包,在其中创建 WebSocketServer 类:
package com.kob.backend.consumer;import org.springframework.stereotype.Component;import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;@Component
@ServerEndpoint("/websocket/{token}") // 注意不要以'/'结尾
public class WebSocketServer {@OnOpenpublic void onOpen(Session session, @PathParam("token") String token) {// 建立链接}@OnClosepublic void onClose() {// 关闭链接}@OnMessagepublic void onMessage(String message, Session session) {// 从Client接收消息}@OnErrorpublic void onError(Session session, Throwable error) {error.printStackTrace();}
}
之前我们配置的 Spring Security 设置了屏蔽除了授权之外的其他所有链接,因此我们需要在 SecurityConfig 类中放行一下 WebSocket 的链接:
package com.kob.backend.config;import com.kob.backend.config.filter.JwtAuthenticationTokenFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
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.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {@Autowiredprivate JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;@Beanpublic PasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder();}@Bean@Overridepublic AuthenticationManager authenticationManagerBean() throws Exception { // AuthenticationManager用于处理身份验证return super.authenticationManagerBean();}@Overrideprotected void configure(HttpSecurity http) throws Exception { // 配置HttpSecurityhttp.csrf().disable().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and().authorizeRequests().antMatchers("/user/account/login/", "/user/account/register/").permitAll() // 需要公开的链接在这边写即可.antMatchers(HttpMethod.OPTIONS).permitAll().anyRequest().authenticated();http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);}@Overridepublic void configure(WebSecurity web) throws Exception {web.ignoring().antMatchers("/websocket/**");}
}
如果是使用新版的配置而不是使用 WebSecurityConfigurerAdapter 可以按以下方式配置:
package com.kob.backend.config;import com.kob.backend.config.filter.JwtAuthenticationTokenFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
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.WebSecurityCustomizer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;@Configuration
@EnableWebSecurity
public class SecurityConfig {@Autowiredprivate JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;@Beanpublic PasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder();}@Beanpublic AuthenticationManager authenticationManagerBean(AuthenticationConfiguration authenticationConfiguration) throws Exception {return authenticationConfiguration.getAuthenticationManager();}@Beanpublic SecurityFilterChain filterChain(HttpSecurity http) throws Exception {http.csrf().disable().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and().authorizeRequests().antMatchers("/user/account/login/", "/user/account/register/").permitAll().antMatchers(HttpMethod.OPTIONS).permitAll().anyRequest().authenticated();http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);return http.build();}@Beanpublic WebSecurityCustomizer webSecurityCustomizer(){return (web) -> web.ignoring().antMatchers("/websocket/**");}
}
2. 前后端WebSocket通信
2.1 WS通信的建立
WebSocket 不属于单例模式(同一个时间每个类只能有一个实例,我们每建一个 WS 链接都会新创建一个实例),不是标准的 Spring 中的组件,因此在注入 Mapper 时不能用 @Autowired 直接注入,一般是将 @Autowired 写在一个 set() 方法上,Spring 会根据方法的参数类型从 IoC 容器中找到该类型的 Bean 对象注入到方法的行参中,并且自动反射调用该方法。
我们先假设前端传过来的是用户 ID 而不是 JWT 令牌:
package com.kob.backend.consumer;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 javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;import java.io.IOException;
import java.util.concurrent.ConcurrentHashMap;@Component
@ServerEndpoint("/websocket/{token}") // 注意不要以'/'结尾
public class WebSocketServer {// ConcurrentHashMap是一个线程安全的哈希表,用于将用户ID映射到WS实例private static final ConcurrentHashMap<Integer, WebSocketServer> users = new ConcurrentHashMap<>();private User user;private Session session = null;private static UserMapper userMapper;@Autowiredpublic void setUserMapper(UserMapper userMapper) {WebSocketServer.userMapper = userMapper;}@OnOpenpublic void onOpen(Session session, @PathParam("token") String token) {this.session = session;System.out.println("Connected!");Integer userId = Integer.parseInt(token);this.user = userMapper.selectById(userId);users.put(userId, this);}@OnClosepublic void onClose() {System.out.println("Disconnected!");if (this.user != null) {users.remove(this.user.getId());}}@OnMessagepublic void onMessage(String message, Session session) {System.out.println("Receive message!");}@OnErrorpublic void onError(Session session, Throwable error) {error.printStackTrace();}public void sendMessage(String message) { // 从后端向当前链接发送消息synchronized (this.session) { // 由于是异步通信,需要加一个锁try {this.session.getBasicRemote().sendText(message);} catch (IOException e) {e.printStackTrace();}}}
}
然后我们先在前端的 PKIndexView 组件中调试,当组件被挂载完成后发出请求建立 WS 链接,当被卸载后关闭 WS 链接:
<template><PlayGround />
</template><script>
import PlayGround from "@/components/PlayGround.vue";
import { onMounted, onUnmounted } from "vue";
import { useStore } from "vuex";export default {components: {PlayGround,},setup() {const store = useStore();let socket = null;let socket_url = `ws://localhost:3000/websocket/${store.state.user.id}/`;onMounted(() => {socket = new WebSocket(socket_url);store.commit("updateOpponent", {username: "我的对手",photo: "https://cdn.acwing.com/media/article/image/2022/08/09/1_1db2488f17-anonymous.png",});socket.onopen = () => { // 链接成功建立后会执行console.log("Connected!");store.commit("updateSocket", socket);};socket.onmessage = (msg) => { // 接收到后端消息时会执行const data = JSON.parse(msg.data); // Spring传过来的数据是放在消息的data中console.log(data);};socket.onclose = () => { // 关闭链接后会执行console.log("Disconnected!");};});onUnmounted(() => {socket.close(); // 如果不断开链接每次切换页面都会创建新链接,就会导致有很多冗余链接});},
};
</script><style scoped></style>
现在我们在对战页面每次刷新后都可以在浏览器控制台或后端控制台中看到 WS 的输出信息。
接下来我们要将 WebSocket 存到前端的 store 中,在 store 目录下创建 pk.js 用来存储和对战页面相关的全局变量:
export default {state: {status: "matching", // 当前状态,matching表示正在匹配,playing表示正在对战socket: null, // 前端和后端建立的链接opponent_username: "", // 对手的用户名opponent_photo: "", // 对手的头像},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;},},actions: {},modules: {},
};
同时要在 store/index.js 中引入进来:
import { createStore } from "vuex";
import ModuleUser from "./user";
import ModulePk from "./pk";export default createStore({state: {},getters: {},mutations: {},actions: {},modules: {user: ModuleUser,pk: ModulePk,},
});
2.2 加入JWT验证
现在我们直接使用用户的 ID 建立 WS 链接,这是不安全的,因为前端可以自行修改这个 ID,因此就需要加入 JWT 验证。
WebSocket 中没有 Session 的概念,因此我们在验证的时候前端就不用将信息放到表头里了,直接放到链接中就行:
...<script>
...export default {...setup() {...let socket_url = `ws://localhost:3000/websocket/${store.state.user.jwt_token}/`;...},
};
</script>...
验证的逻辑可以参考之前的 JwtAuthenticationTokenFilter,我们可以把这个验证的模块单独写到一个文件中,在 consumer 包下创建 utils 包,然后创建一个 JwtAuthentication 类:
package com.kob.backend.consumer.utils;import com.kob.backend.utils.JwtUtil;
import io.jsonwebtoken.Claims;public class JwtAuthentication {public static Integer getUserId(String token) {int userId = -1;try {Claims claims = JwtUtil.parseJWT(token);userId = Integer.parseInt(claims.getSubject());} catch (Exception e) {throw new RuntimeException(e);}return userId;}
}
然后就可以在 WebSocketServer 中解析 JWT 令牌:
package com.kob.backend.consumer;import com.kob.backend.consumer.utils.JwtAuthentication;
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 javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;import java.io.IOException;
import java.util.concurrent.ConcurrentHashMap;@Component
@ServerEndpoint("/websocket/{token}") // 注意不要以'/'结尾
public class WebSocketServer {...@OnOpenpublic void onOpen(Session session, @PathParam("token") String token) throws IOException {this.session = session;System.out.println("Connected!");Integer userId = JwtAuthentication.getUserId(token);this.user = userMapper.selectById(userId);if (user != null) {users.put(userId, this);} else {this.session.close();}}...
}
3. 前后端匹配业务
3.1 实现前端页面
我们需要实现一个前端的匹配页面,并能够切换匹配和对战页面,可以根据之前在 store 中存储的 status 状态来动态展示页面。首先在 components 目录下创建 MatchGround.vue 组件,其中需要展示玩家自己的头像和用户名以及对手的头像和用户名,当点击开始匹配按钮时向 WS 链接发送开始匹配的消息,点击取消按钮时发送取消匹配的消息:
<template><div class="matchground"><div class="row"><div class="col-md-6" style="text-align: center;"><div class="photo"><img class="img-fluid" :src="$store.state.user.photo"></div><div class="username">{{ $store.state.user.username }}</div></div><div class="col-md-6" style="text-align: center;"><div class="photo"><img class="img-fluid" :src="$store.state.pk.opponent_photo"></div><div class="username">{{ $store.state.pk.opponent_username }}</div></div><div class="col-md-12 text-center" style="margin-top: 14vh;"><button @click="click_match_btn" type="button" class="btn btn-info btn-lg">{{ 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("开始匹配");const click_match_btn = () => {if (match_btn_info.value === "开始匹配") {match_btn_info.value = "取消";store.state.pk.socket.send(JSON.stringify({ // 将json封装成字符串发送给后端,后端会在onMessage()中接到请求event: "start_match", // 表示开始匹配}));} else {match_btn_info.value = "开始匹配";store.state.pk.socket.send(JSON.stringify({event: "stop_match", // 表示停止匹配}));}};return {match_btn_info,click_match_btn,};},
};
</script><style scoped>
div.matchground {width: 60vw;height: 70vh;margin: 40px auto;border-radius: 10px;background-color: rgba(50, 50, 50, 0.5);
}img {width: 35%;border-radius: 50%;margin: 14vh 0 1vh 0;
}.username {font-size: 24px;font-weight: bold;color: white;
}
</style>
3.2 实现前后端交互逻辑
当用户点击开始匹配按钮后,前端要向服务器发出一个请求,后端接收到请求后应该将该用户放入匹配池中,由于目前还没有实现微服务,因此我们先在 WebSocketServer 后端用一个 Set 维护正在匹配的玩家,当匹配池中满两名玩家就将其匹配在一起,然后将匹配结果返回给两名玩家的前端:
package com.kob.backend.consumer;import com.alibaba.fastjson2.JSONObject;
import com.kob.backend.consumer.utils.JwtAuthentication;
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 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;@Component
@ServerEndpoint("/websocket/{token}") // 注意不要以'/'结尾
public class WebSocketServer {// ConcurrentHashMap是一个线程安全的哈希表,用于将用户ID映射到WS实例private static final ConcurrentHashMap<Integer, WebSocketServer> users = new ConcurrentHashMap<>();// CopyOnWriteArraySet也是线程安全的private static final CopyOnWriteArraySet<User> matchPool = new CopyOnWriteArraySet<>(); // 匹配池private User user;private Session session = null;private static UserMapper userMapper;@Autowiredpublic void setUserMapper(UserMapper userMapper) {WebSocketServer.userMapper = userMapper;}@OnOpenpublic void onOpen(Session session, @PathParam("token") String token) throws IOException {this.session = session;System.out.println("Connected!");Integer userId = JwtAuthentication.getUserId(token);this.user = userMapper.selectById(userId);if (user != null) {users.put(userId, this);} else {this.session.close();}}@OnClosepublic void onClose() {System.out.println("Disconnected!");if (this.user != null) {users.remove(this.user.getId());matchPool.remove(this.user);}}@OnMessagepublic void onMessage(String message, Session session) { // 一般会把onMessage()当作路由System.out.println("Receive message!");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();}}@OnErrorpublic void onError(Session session, Throwable error) {error.printStackTrace();}public void sendMessage(String message) { // 从后端向当前链接发送消息synchronized (this.session) { // 由于是异步通信,需要加一个锁try {this.session.getBasicRemote().sendText(message);} catch (IOException e) {e.printStackTrace();}}}private void startMatching() {System.out.println("Start matching!");matchPool.add(this.user);while (matchPool.size() >= 2) { // 临时调试用的,未来要替换成微服务Iterator<User> it = matchPool.iterator();User a = it.next(), b = it.next();matchPool.remove(a);matchPool.remove(b);JSONObject respA = new JSONObject(); // 发送给A的信息respA.put("event", "match_success");respA.put("opponent_username", b.getUsername());respA.put("opponent_photo", b.getPhoto());users.get(a.getId()).sendMessage(respA.toJSONString()); // A不一定是当前链接,因此要在users中获取JSONObject respB = new JSONObject(); // 发送给B的信息respB.put("event", "match_success");respB.put("opponent_username", a.getUsername());respB.put("opponent_photo", a.getPhoto());users.get(b.getId()).sendMessage(respB.toJSONString());}}private void stopMatching() {System.out.println("Stop matching!");matchPool.remove(this.user);}
}
接着修改一下 PKIndexView,当接收到 WS 链接从后端发送过来的匹配成功消息后需要更新对手的头像和用户名:
...<script>
...export default {...setup() {...onMounted(() => {...socket.onmessage = (msg) => { // 接收到后端消息时会执行const data = JSON.parse(msg.data); // Spring传过来的数据是放在消息的data中console.log(data);if (data.event === "match_success") { // 匹配成功store.commit("updateOpponent", {username: data.opponent_username,photo: data.opponent_photo,});setTimeout(() => { // 3秒后再进入游戏地图界面store.commit("updateStatus", "playing");}, 3000);}};socket.onclose = () => { // 关闭链接后会执行console.log("Disconnected!");store.commit("updateStatus", "matching"); // 进入游戏地图后玩家点击其他页面应该是默认退出游戏};...});...},
};
</script>...
测试的时候需要用两个浏览器,如果没有两个浏览器可以在 Edge 浏览器的右上角设置菜单中新建 InPrivate 窗口,这样就可以自己登录两个不同的账号进行匹配测试。
3.3 同步游戏地图
现在匹配成功后两名玩家进入游戏时看到的地图是不一样的,因为目前地图还都是在每名玩家本地的浏览器生成的,那么我们就需要将生成地图的逻辑放到服务器端。
先在后端的 consumer.utils 包下创建 Game 类,用来管理整个游戏流程,然后我们在其中先实现地图的随机生成:
package com.kob.backend.consumer.utils;import java.util.Arrays;
import java.util.Random;public class Game {private final Integer rows;private final Integer cols;private final Integer inner_walls_count;private final boolean[][] g;private static final int[] dx = { -1, 0, 1, 0 }, 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 boolean[rows][cols];}public boolean[][] 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] = true;for (int i = 0; i < 4; i++) {int nx = sx + dx[i], ny = sy + dy[i];if (!g[nx][ny] && check_connectivity(nx, ny, tx, ty)) {g[sx][sy] = false; // 注意在这里我们用的g就是原始数组,因此修改后要记得还原return true;}}g[sx][sy] = false; // 记得还原return false;}private boolean drawMap() {// 初始化障碍物标记数组for (int i = 0; i < this.rows; i++) {Arrays.fill(g[i], false);}// 给地图四周加上障碍物for (int r = 0; r < this.rows; r++) {g[r][0] = g[r][this.cols - 1] = true;}for (int c = 0; c < this.cols; c++) {g[0][c] = g[this.rows - 1][c] = true;}// 添加地图内部的随机障碍物,需要有对称性因此枚举一半即可,另一半对称生成Random random = new Random();for (int i = 0; i < this.inner_walls_count / 2; i++) {for (int j = 0; j < 10000; j++) {int r = random.nextInt(this.rows); // 返回0~this.rows-1的随机整数int c = random.nextInt(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;}}return check_connectivity(this.rows - 2, 1, 1, this.cols - 2);}public void createMap() {for (int i = 0; i < 10000; i++) {if (drawMap()) {break;}}}
}
然后在 WebSocketServer 类中当匹配成功时创建游戏地图,暂时先将其存到局部变量中,之后再进行优化:
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(); // 发送给A的信息respA.put("event", "match_success");respA.put("opponent_username", b.getUsername());respA.put("opponent_photo", b.getPhoto());respA.put("game_map", game.getG());users.get(a.getId()).sendMessage(respA.toJSONString()); // A不一定是当前链接,因此要在users中获取JSONObject respB = new JSONObject(); // 发送给B的信息respB.put("event", "match_success");respB.put("opponent_username", a.getUsername());respB.put("opponent_photo", a.getPhoto());respB.put("game_map", game.getG());users.get(b.getId()).sendMessage(respB.toJSONString());
}
接下来需要在前端的 store/pk.js 文件中将地图存为全局变量:
export default {state: {...game_map: null, // 游戏地图},getters: {},mutations: {...updateGameMap(state, game_map) {state.game_map = game_map;},},actions: {},modules: {},
};
在 PKIndexView.vue 中当接收到匹配成功的消息后需要更新地图:
...<script>
...export default {...setup() {...onMounted(() => {...socket.onmessage = (msg) => { // 接收到后端消息时会执行const data = JSON.parse(msg.data); // Spring传过来的数据是放在消息的data中console.log(data);if (data.event === "match_success") { // 匹配成功store.commit("updateOpponent", { // 更新对手信息username: data.opponent_username,photo: data.opponent_photo,});store.commit("updateGameMap", data.game_map); // 更新游戏地图setTimeout(() => { // 3秒后再进入游戏地图界面store.commit("updateStatus", "playing");}, 3000);}};...});...},
};
</script>...
然后需要在 GameMap.vue 中将全局变量传给游戏地图 GameMap.js:
...<script>
import { ref, onMounted } from "vue";
import { GameMap } from "@/assets/scripts/GameMap";
import { useStore } from "vuex";export default {setup() {const store = useStore();let parent = ref(null);let canvas = ref(null);onMounted(() => {new GameMap(canvas.value.getContext("2d"), parent.value, store);});return {parent,canvas,};},
};
</script>...
最后就可以在 GameMap.js 中将地图渲染出来:
...export class GameMap extends AcGameObject {constructor(ctx, parent, store) { // ctx表示画布,parent表示画布的父元素...this.store = store;}...create_walls_online() { // 通过后端生成的数据创建地图const g = this.store.state.pk.game_map;for (let r = 0; r < this.rows; r++) {for (let c = 0; c < this.cols; c++) {if (g[r][c]) {this.walls.push(new Wall(r, c, this));}}}}...start() {// for (let i = 0; i < 10000; i++) {// if (this.create_walls())// break;// }this.create_walls_online(); // 在线生成地图this.add_listening_events();}...
}
相关文章:
SpringBoot学习笔记-实现微服务:匹配系统(上)
笔记内容转载自 AcWing 的 SpringBoot 框架课讲义,课程链接:AcWing SpringBoot 框架课。 CONTENTS 1. 配置WebSocket2. 前后端WebSocket通信2.1 WS通信的建立2.2 加入JWT验证 3. 前后端匹配业务3.1 实现前端页面3.2 实现前后端交互逻辑3.3 同步游戏地图 …...
重磅!全球首个“绿色黑灯工厂”落户中国,竟然是这家企业……
作者:叶蓁 “52”、“白加黑”、“无人看守作业”,这是九牧“绿色黑灯工厂”的几大关键词。 九牧绿色黑灯工厂不仅是单体产量最大的工厂,也是全球首个入选的“绿色黑灯工厂”。 11月17日,中国节能协会授予九牧5G智能马桶工厂全球…...
go语言学习-异常处理
1、异常场景 网络故障硬件故障组件故障输入错误逻辑错误链路调度错误 2、异常处理方式 # python或者java异常处理 try 可能出现的错误 catch对错误进行处理 xxx,err : 代码 if err ! nil {代码出现错误,需要做处理 }3、自定义错误 有两种方法:1、通过…...
如何使用 JavaScript 实现图片上传并转换为 LaTeX 公式
在本教程中,我们将学习如何使用 JavaScript 创建一个上传图片的功能,并将所选图片转换为 LaTeX 公式。我们将使用 FileReader 对象来读取图片并将其转换为 Base64 格式,然后利用 img2latex API 将其转换为 LaTeX 公式。 1. HTML 结构 首先&…...
深刻理解MySQL8游标处理中not found
深刻理解MySQL8游标处理中not found 最近使用MySQL的游标,在fetch循环过程中,程序总是提前退出 ,百思不得其解,经过测试,原来是对于游标处理中not found的定义理解有误,默认是视同Oracle的游标not found定…...
甄知燕千云与SAP、EBS、TC、NS等应用深度集成,智能提单一键畅通,效能一键提升
当今全球化时代下,全球商业环境面临前所未有的机遇和挑战,企业需要持续的业务变革、组织优化来进行降本增效,企业管理软件已成为中小企业、大型企业数字化转型不可或缺的管理工具,企业内管理软件系统也越来越多。 为了适应当前企业…...
第99步 深度学习图像目标检测:SSDlite建模
基于WIN10的64位系统演示 一、写在前面 本期,我们继续学习深度学习图像目标检测系列,SSD(Single Shot MultiBox Detector)模型的后续版本,SSDlite模型。 二、SSDlite简介 SSDLite 是 SSD 模型的一个变种,…...
用EasyAVFilter将网络文件或者本地文件推送RTMP出去的时候发现CPU占用好高,用的也是vcodec copy呀,什么原因?
最近同事在用EasyAVFilter集成在EasyDarwin中做视频拉流转推RTMP流的功能的时候,发现怎么做CPU占用都会很高,但是视频没有调用转码,vcodec用的就是copy,这是什么原因呢? 我们用在线的RTSP流就不会出现这种情况&#x…...
Vatee万腾科技的独特力量:Vatee数字时代创新的新视野
在数字化时代的浪潮中,Vatee万腾科技以其独特而强大的创新力量,为整个行业描绘了一幅崭新的视野。这不仅是一场科技创新的冒险,更是对未来数字时代发展方向的领先探索。 Vatee万腾将创新视为数字时代发展的引擎,成为推动行业向前的…...
【JavaSE】基础笔记 - 异常(Exception)
目录 1、异常的概念和体系结构 1.1、异常的概念 1.2、 异常的体系结构 1.3 异常的分类 2、异常的处理 2.1、防御式编程 2.2、异常的抛出 2.3、异常的捕获 2.3.1、异常声明throws 2.3.2、try-catch捕获并处理 3、自定义异常类 1、异常的概念和体系结构 1.1、异常的…...
QTableWidget——编辑单元格
文章目录 前言熟悉QTableWiget,通过实现单元格的合并、拆分、通过编辑界面实现表格内容及属性的配置、实现表格的粘贴复制功能熟悉QTableWiget的属性 一、[单元格的合并、拆分](https://blog.csdn.net/qq_15672897/article/details/134476530?spm1001.2014.3001.55…...
编译QT Mysql库并集成使用
安装MSVC编译器与Windows 10 SDK 打开Visual Studio Installer,如果已经安装过内容了可能是如下页面,点击修改(头一回打开的话不需要这一步): 然后在工作负荷中勾选使用C的桌面开发,它会帮我们勾选好一些…...
利用企业被执行人信息查询API保障商业交易安全
前言 在当今竞争激烈的商业环境中,企业为了保障商业交易的安全性不断寻求新的手段。随着技术的发展,利用企业被执行人信息查询API已经成为了一种强有力的工具,能够帮助企业在商业交易中降低风险,提高合作的信任度。 企业被执行人…...
【深度学习】P1 深度学习基础框架 - 张量 Tensor
深度学习基础框架 张量 Tensor 张量数据操作导入创建张量获取张量信息改变张量张量运算 张量与内存 张量 Pytorch 是一个深度学习框架,用于开发和训练神经网络模型。 而其核心数据结构,则是张量 Tensor,类似于 Numpy 数组,但是可…...
vue2 识别页面参数中的html
在Vue 2中,你可以使用v-html指令来识别页面参数中的HTML内容。v-html指令允许你将HTML代码作为Vue模板的一部分进行渲染。 以下是一个示例,演示了如何在Vue 2中使用v-html指令来识别页面参数中的HTML内容: <template><div v-html&…...
matlab 一些画图法总结(持续更新)
*****************************************画Dmd_L极坐标表示法**************************************** if(~exist(Dmd_L_array)) Dmd_L_array []; end Dmd_L_array [Dmd_L_array; Dmd_L]; thetaangle(Dmd_L_array); rabs(Dmd_L_array); polarplot(theta,r,o); *****…...
MDK AC5和AC6是什么?在KEIL5中添加和选择ARMCC版本
前言 看视频有UP主提到“AC5”“AC6”这样的词,一开始有些不理解,原来他说的是ARMCC版本。 keil自带的是ARMCC5,由于ARMCC5已经停止维护了,很多开发者会选择ARMCC6。 在维护公司“成年往事”项目可能就会遇到新KEIL旧版本编译器…...
杰发科技AC7801——EEP内存分布情况
简介 按照文档进行配置 核心代码如下 /*!* file sweeprom_demo.c** brief This file provides sweeprom demo test function.**//* Includes */ #include <stdlib.h> #include "ac780x_sweeprom.h" #include "ac780x_debugout.h"/* Define …...
【mybatis注解实现条件查询】
文章目录 步骤1: 引入MyBatis依赖步骤2: 创建数据模型步骤3: 创建Mapper接口步骤4: 配置MyBatis步骤5: 执行条件查询 步骤1: 引入MyBatis依赖 <dependency><groupId>org.mybatis</groupId><artifactId>mybatis</artifactId><version>3.x.…...
【广州华锐互动】VR线上课件制作软件满足数字化教学需求
随着科技的不断发展,虚拟现实(VR)技术在教学领域的应用逐渐成为趋势。其中,广州华锐互动开发的VR线上课件制作软件更是备受关注。这种工具为教师提供了便捷的制作VR课件的手段,使得VR教学成为可能,极大地丰…...
HTML 列表、表格、表单
1 列表标签 作用:布局内容排列整齐的区域 列表分类:无序列表、有序列表、定义列表。 例如: 1.1 无序列表 标签:ul 嵌套 li,ul是无序列表,li是列表条目。 注意事项: ul 标签里面只能包裹 li…...
学校招生小程序源码介绍
基于ThinkPHPFastAdminUniApp开发的学校招生小程序源码,专为学校招生场景量身打造,功能实用且操作便捷。 从技术架构来看,ThinkPHP提供稳定可靠的后台服务,FastAdmin加速开发流程,UniApp则保障小程序在多端有良好的兼…...
【C++特殊工具与技术】优化内存分配(一):C++中的内存分配
目录 一、C 内存的基本概念 1.1 内存的物理与逻辑结构 1.2 C 程序的内存区域划分 二、栈内存分配 2.1 栈内存的特点 2.2 栈内存分配示例 三、堆内存分配 3.1 new和delete操作符 4.2 内存泄漏与悬空指针问题 4.3 new和delete的重载 四、智能指针…...
Webpack性能优化:构建速度与体积优化策略
一、构建速度优化 1、升级Webpack和Node.js 优化效果:Webpack 4比Webpack 3构建时间降低60%-98%。原因: V8引擎优化(for of替代forEach、Map/Set替代Object)。默认使用更快的md4哈希算法。AST直接从Loa…...
uniapp 字符包含的相关方法
在uniapp中,如果你想检查一个字符串是否包含另一个子字符串,你可以使用JavaScript中的includes()方法或者indexOf()方法。这两种方法都可以达到目的,但它们在处理方式和返回值上有所不同。 使用includes()方法 includes()方法用于判断一个字…...
跨平台商品数据接口的标准化与规范化发展路径:淘宝京东拼多多的最新实践
在电商行业蓬勃发展的当下,多平台运营已成为众多商家的必然选择。然而,不同电商平台在商品数据接口方面存在差异,导致商家在跨平台运营时面临诸多挑战,如数据对接困难、运营效率低下、用户体验不一致等。跨平台商品数据接口的标准…...
Canal环境搭建并实现和ES数据同步
作者:田超凡 日期:2025年6月7日 Canal安装,启动端口11111、8082: 安装canal-deployer服务端: https://github.com/alibaba/canal/releases/1.1.7/canal.deployer-1.1.7.tar.gz cd /opt/homebrew/etc mkdir canal…...
职坐标物联网全栈开发全流程解析
物联网全栈开发涵盖从物理设备到上层应用的完整技术链路,其核心流程可归纳为四大模块:感知层数据采集、网络层协议交互、平台层资源管理及应用层功能实现。每个模块的技术选型与实现方式直接影响系统性能与扩展性,例如传感器选型需平衡精度与…...
作为点的对象CenterNet论文阅读
摘要 检测器将图像中的物体表示为轴对齐的边界框。大多数成功的目标检测方法都会枚举几乎完整的潜在目标位置列表,并对每一个位置进行分类。这种做法既浪费又低效,并且需要额外的后处理。在本文中,我们采取了不同的方法。我们将物体建模为单…...
python数据结构和算法(1)
数据结构和算法简介 数据结构:存储和组织数据的方式,决定了数据的存储方式和访问方式。 算法:解决问题的思维、步骤和方法。 程序 数据结构 算法 算法 算法的独立性 算法是独立存在的一种解决问题的方法和思想,对于算法而言&a…...
