SpringBoot


1.SpringBoot解决跨域问题

package com.kob.backend.config;
import org.springframework.context.annotation.Configuration;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@Configuration
public class CorsConfig implements Filter {
    @Override
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
        HttpServletResponse response = (HttpServletResponse) res;
        HttpServletRequest request = (HttpServletRequest) req;

        String origin = request.getHeader("Origin");
        if(origin!=null) {
            response.setHeader("Access-Control-Allow-Origin", origin);
        }

        String headers = request.getHeader("Access-Control-Request-Headers");
        if(headers!=null) {
            response.setHeader("Access-Control-Allow-Headers", headers);
            response.setHeader("Access-Control-Expose-Headers", headers);
        }

        response.setHeader("Access-Control-Allow-Methods", "*");
        response.setHeader("Access-Control-Max-Age", "3600");
        response.setHeader("Access-Control-Allow-Credentials", "true");

        chain.doFilter(request, response);
    }

    @Override
    public void init(FilterConfig filterConfig) {

    }

    @Override
    public void destroy() {
    }
}

2.SpringBoot配置Spring Security

是用户认证操作,一种授权机制,目的是安全

(1)添加依赖:

spring-boot-starter-security

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
    <version>2.7.0</version>
</dependency>

刷新之后显示登陆:

image-20230218173026959

默认的叫 **Username **是 user ,密码自动生成。

image-20230218173311657

(2)Spring Security使用密文添加用户

@GetMapping("/user/add/{userId}/{username}/{password}/")
    public String addUser(
            @PathVariable int userId,
            @PathVariable String username,
            @PathVariable String password) {
        PasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
        String encodedPassword = passwordEncoder.encode(password);
        User user = new User(userId, username, encodedPassword);
        userMapper.insert(user);
        return "Add User Successfully";
    }

3.SpringBoot实现JwtToken验证

(1)添加依赖:

在 pom.xml 中添加下列依赖:

  • jjwt-api
  • jjwt-impl
  • jjwt-jackson

添加之后点击重新加载。

<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-api</artifactId>
    <version>0.11.2</version>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-impl</artifactId>
    <version>0.11.2</version>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-jackson</artifactId>
    <version>0.11.2</version>
    <scope>runtime</scope>
</dependency>

(2) 编写、修改相关类

  • 实现 JwtUtil 类,在 backend 目录下创建软件包 utils 并创建 JwtUtil 类。
    JwtUtil 类为jwt 工具类,用来创建、解析 jwt token。
package com.kob.backend.utils;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.stereotype.Component;

import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;
import java.util.Date;
import java.util.UUID;

@Component
public class JwtUtil {
    public static final long JWT_TTL = 60 * 60 * 1000L * 24 * 14;  // 有效期14天
    public static final String JWT_KEY = "SDFGjhdsfalshdfHFdsjkdsfds121232131afasdfac";

    public static String getUUID() {
        return UUID.randomUUID().toString().replaceAll("-", "");
    }

    public static String createJWT(String subject) {
        JwtBuilder builder = getJwtBuilder(subject, null, getUUID());
        return builder.compact();
    }

    private static JwtBuilder getJwtBuilder(String subject, Long ttlMillis, String uuid) {
        SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
        SecretKey secretKey = generalKey();
        long nowMillis = System.currentTimeMillis();
        Date now = new Date(nowMillis);
        if (ttlMillis == null) {
            ttlMillis = JwtUtil.JWT_TTL;
        }

        long expMillis = nowMillis + ttlMillis;
        Date expDate = new Date(expMillis);
        return Jwts.builder()
                .setId(uuid)
                .setSubject(subject)
                .setIssuer("sg")
                .setIssuedAt(now)
                .signWith(signatureAlgorithm, secretKey)
                .setExpiration(expDate);
    }

    public static SecretKey generalKey() {
        byte[] encodeKey = Base64.getDecoder().decode(JwtUtil.JWT_KEY);
        return new SecretKeySpec(encodeKey, 0, encodeKey.length, "HmacSHA256");
    }

    public static Claims parseJWT(String jwt) throws Exception {
        SecretKey secretKey = generalKey();
        return Jwts.parserBuilder()
                .setSigningKey(secretKey)
                .build()
                .parseClaimsJws(jwt)
                .getBody();
    }
}
  • 实现 JwtAuthenticationTokenFilter 类

在 backend 的 config 目录下创建 config 软件包,并创建 JwtAuthenticationTokenFilter 类。
实现 JwtAuthenticationTokenFilter 类,用来验证 jwt token ,如果验证成功,则将 User 信息注入上下文中。

package com.kob.backend.config.filter;


import com.kob.backend.mapper.UserMapper;
import com.kob.backend.pojo.User;
import com.kob.backend.service.impl.utils.UserDetailsImpl;
import com.kob.backend.utils.JwtUtil;
import com.sun.istack.internal.NotNull;
import io.jsonwebtoken.Claims;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
    @Autowired
    private UserMapper userMapper;

    @Override
    protected void doFilterInternal(HttpServletRequest request, @NotNull HttpServletResponse response, @NotNull FilterChain filterChain) throws ServletException, IOException {
        String token = request.getHeader("Authorization");

        if (!StringUtils.hasText(token) || !token.startsWith("Bearer ")) {
            filterChain.doFilter(request, response);
            return;
        }

        token = token.substring(7);

        String userid;
        try {
            Claims claims = JwtUtil.parseJWT(token);
            userid = claims.getSubject();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }

        User user = userMapper.selectById(Integer.parseInt(userid));

        if (user == null) {
            throw new RuntimeException("用户名未登录");
        }

        UserDetailsImpl loginUser = new UserDetailsImpl(user);
        UsernamePasswordAuthenticationToken authenticationToken =
                new UsernamePasswordAuthenticationToken(loginUser, null, null);

        SecurityContextHolder.getContext().setAuthentication(authenticationToken);

        filterChain.doFilter(request, response);
    }
}

配置config.SecurityConfig类

放行登录、注册等接口。

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.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 {
    @Autowired
    private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                .antMatchers("/user/account/token/", "/user/account/register/").permitAll()
                .antMatchers(HttpMethod.OPTIONS).permitAll()
                .anyRequest().authenticated();

        http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
    }
}

4.Springboot实现匹配系统

(1)匹配系统的流程

要实现匹配系统起码要有两个客户端client1,client2,当客户端打开对战页面并开始匹配时,会给后端服务器server发送一个请求,而匹配是一个异步的过程,什么时候返回结果是不可预知的,所以我们要写一个专门的匹配系统,维护一堆用户的集合,当用户发起匹配请求时,请求会先传给后端服务器,然后再传给匹配系统处理,匹配系统会不断地在用户里去筛选,将rating较为相近的的用户匹配到一组。当成功匹配后,匹配系统就会返回结果给springboot的后端服务器,继而返回给客户端即前端。然后我们就能在前端看到匹配到的对手是谁啦。

(2)websocket协议

因为匹配是异步的过程,且需要前后端双向交互,而普通的http协议是单向的,一问一答式的,属于立即返回结果的类型,不能满足我们的异步需求,因此我们需要一个新的协议websocket:不仅客户端可以主动向服务器端发送请求,服务器端也可以主动向客户端发送请求,是双向双通的,且支持异步。简单来说就是客户端向后端发送请求,经过不确定的时间,会返回一次或多次结果给客户端。
基本原理: 每一个ws连接都会在后端维护起来,客户端连接服务器的时候会创建一个WebSocketServer类。每创建一个链接就是new一个WebSocketServer类的实例,所有与链接相关的信息,都会存在这个类里面。

(3)前面逻辑的优化

由于每次刷新都会刷新不同的地图,为了公平起见,应该把地图的生成放在服务器端,然后返回结果给前端。
为了防止作弊,游戏的一系列操作、判断逻辑都应该放在服务器端,前端只是呈现动画。
我们可以从客户端获取输入,也可以通过微服务从代码端获得输入。
简单的流程如下: Game -> Create map -> 返回给客户端 -> 客户端等待匹配waiting (sleep) -> 匹配成功则进行一系列游戏逻辑

用线程安全的set定义匹配池:

final private static CopyOnWriteArraySet<Users> matchPoll = new CopyOnWriteArraySet<>();

开始匹配时,将用户放进拼配池里,取消匹配时将用户移除匹配池,匹配过程在目前调试阶段可以简单地两两匹配

解决同步问题

前文也提到过,生成地图,游戏逻辑等与游戏相关的操作都应该放在服务端,不然的话客户每次刷新得到的地图都不一样,游戏的公平性也不能得到保证。因此,我们要将之前在前端写的游戏逻辑全部转移到后端(云端),前端只负责动画的演示即可。

实现游戏同步

实际上我们在游戏对战的时候存在三个棋盘,两个是对战双方客户端里存在的棋盘,一个是云端存在的棋盘,我们要求实现云端与两个客户端之间的同步。

实现方法:

游戏总流程示意图:

Matching System <---------- WS服务器 <--------------Client1 、client2
·····································|
·····································|
·····································|(维护)
·····································|
···································Game
·····································|
·································CreateMap
·····································|
······(有时间限制)·············Next Step(等待玩家或bot输入) <------Client1 (Client2…) 或 bot
·····································|
································Judge System判断两名玩家下一步走法是否合法
·····································|
··································Result (Nest Step超时的话直接返回Result)
引入线程:

为了优化游戏体验度,我们的Game不能作为单线程去处理,每一个Game要另起一个新线程来做。
从Next Step开始的操作可以当成一个线程,获取用户操作可以当成另一个线程。
这里我们涉及到两个线程之间进行通信的问题,以及线程开锁解锁的问题。

每一局单独的游戏都会new 一个新的Game类,都是一个单独的线程

将类改成多线程

继承一个 Thread类,并且ALT + INS重写run()方法
我们开始进行线程的执行的时候,线程的入口函数就是这个run()函数consumer/utils/Game.java.

将用户的操作nextStep存起来,方便外面的线程调用,
在Game线程里面会读取两个玩家的操作nextStepA/B的值,
在外面Client线程里面则会修改这两个变量的值,
这里涉及到了线程的读写同步问题!
需要加上进程同步锁
一般来说就是先上锁再读写,后解锁
try{} finally {lock.unlock();}可以保证报异常的情况下也可以解锁而不会产生死锁
简单总结一下就是:先上锁再操作,具体可以参考OS相关的内容o(╯□╰)o
所以以下涉及到nextStepA 和 nextStepB 的,不管是读还是写,只要出现了的话就要考虑到上锁和解锁方面的问题了,consumer/utils/Game.java

实现接受客户端玩家输入的操作

后端接受前端两名玩家输入的操作后,才开始进行下一步操作。为了游戏的流畅性,提高玩家的游戏体验感,我们规定,如果超过一定的时间后,另一名玩家仍然未能给予操作,我们就判定这个玩家lose了。

可以用sleep函数来实现等待效果,定最长等待时间为5s。
这里可以按照自己的情况合理地规定等待时间,可以通过增加循环次数,减少sleep时间优化玩家操作手感,以牺牲服务器的计算量换取玩家的操作的流畅性。
tips:要在循环里面上锁,在外面上锁会死锁!
还需要注意的是,我们前端设置1s走5步,200ms走一步,所以为了操作顺利,不会因为操作太快而读入多个操作,我们每一次读取前都要先sleep 200ms,规范一下

实现Try again逻辑

接下来我们把Try again按钮实现一下,玩家可以在游戏结束后点击这个按钮再来一局游戏。
实现逻辑也比较简单,每次点击按钮,把游戏页面展示状态status从playing 改成 matching即可,这样整个游戏页面就返回到匹配页面了。
不要忘记了要updateLoser改成none,即重新开始游戏前还没有loser
还有把对手头像updateOpponent成默认的灰头像。

设计录像数据库

为了后期存储对战录像,我们需要先设计一个存储对象的数据库。
数据库内容包括

Id          record的id 自增 主键
a_id        用户a的id
a_sx        用户a在地图中的横坐标
a_sy        用户a在地图中的纵坐标
b_id        用户b的id  
b_sx        用户b在地图中的横坐标 
b_sy        用户b在地图中的纵坐标
a_steps     用户a走的步数
b_steps     用户a走的步数
Map         两个用户对战的地图信息
Loser       记录失败方的名字
Createtime  创建时间

建立相应的pojo,mapper层。

准备工作完成后,我们就可以开始写将数据写入数据库的逻辑了

consumer/utils/Game.java

 private String getMapString() {
        StringBuilder res = new StringBuilder();
        for (int i = 0; i < rows; i++) {
            for (int j = 0; j < cols; j++) {
                if (mark[i][j]) res.append(1);
                else res.append(0);
            }
        }
        return res.toString();
    }

private void saveRecord() {
        Record record = new Record(
                null, //因为之前创建数据库时是把id定义为自动递增,所以这里不用手动传id
                playerA.getId(),
                playerA.getSx(),
                playerA.getSy(),
                playerB.getId(),
                playerB.getSx(),
                playerB.getSy(),
                playerA.getStepsString(),
                playerB.getStepsString(),
                getMapString(),
                loser,
                new Date()
        );

        WebSocketServer.recordMapper.insert(record); //ws里数据库的注入

    }

至此,就完成了联机匹配和存储游戏对局数据的大部分内容。

JAVA注解大概分为两类

(1)一类是使用Bean,即是把已经在xml文件中配置好的Bean拿来用,完成属性、方法的组装;比如@Autowired , @Resource,可以通过byTYPE(@Autowired)、byNAME(@Resource)的方式获取Bean;

(2)一类是注册Bean,@Component , @Repository , @ Controller , @Service , @Configration这些注解都是把你要实例化的对象转化成一个Bean,放在IoC容器中,等你要用的时候,它会和上面的@Autowired , @Resource配合到一起,把对象、属性、方法完美组装。

@controller :标注控制层,也可以理解为接收请求处理请求的类。
@service:标注服务层,也就是内部逻辑处理层。
@repository:标注数据访问层,也就是用于数据获取访问的类(组件)。
@component 其他不属于以上三类的类,但是会同样注入spring容器以被获取使用。它的作用就是实现bean的注入

@AutoWired 就是在你声明了注册类后,可以用该注解注入进当前写的类中。
凡是子类及带属性、方法的类都注册Bean到Spring中,交给它管理;@Bean用在方法上,告诉Spring容器,你可以从下面这个方法中拿到一个Bean。调用的时候和@Component一样,用@Autowired 调用有@Bean注解的方法,多用于第三方类无法写@Component的情况。

微服务实现匹配系统

根据上一part的设计逻辑,我们可以用微服务去代替之前调试用的匹配系统,使匹配系统功能更加完善。
微服务:是一个独立的程序,可以认为是另起了一个新的springboot。
我们把这个新的springboot叫做Matching System作为我们的匹配系统,与之对应的是Matching Server,即匹配的服务器后端。

当我们之前的springboot也就是游戏对战的服务器后端backend Server获取了两个匹配的玩家信息后,会向Matching Server服务器后端发送一个http请求,而当Matching Server接收到了请求后,会开一个独立的线程Matching开始进行玩家匹配。
匹配逻辑也非常简单,即每隔1s会扫描当前已有的所有玩家,判断当前玩家的rating是否相近,能否匹配出来,若能匹配出来则将结果返回给backend Server(通过http返回)

实现手法:Spring Cloud

创建backendcloud

我们项目的结构会出现变化,要先创建一个新的springboot项目backendcloud作为父项目,包含两个并列的子项目Matching System和backend。

注意:backendcloud 创建时要引入Spring Web依赖,不然的话后面自己要在pom.xml里手动添加!

因为父级项目是不用写逻辑的,可以把他的整个src文件删掉。

配置pom.xml

<packaging>pom</packaging>

加上Spring Cloud依赖

<dependency>
           <groupId>org.springframework.cloud</groupId>
           <artifactId>spring-cloud-dependencies</artifactId>
           <version>2021.0.3</version>
           <type>pom</type>
           <scope>import</scope>
       </dependency>

在backendcloud项目文件夹下创建两个模块:MatchingSystem, backend,相当于两个并列的springboot项目。

Matching System

配置pom.xml
将父项目里的spring web依赖转移到Matching System的pom.xml里

配置端口
在resources文件夹里创建文件application.properties

server.port = 3001

匹配服务的实现

和之前写的业务逻辑一样,先写个匹配的服务接口MatchingService,然后在Impl里实现对应的接口
这里提供参考逻辑:matchingsystem\service\impl\MatchingServiceImpl.java

@Service

public class MatchingServiceImpl implements MatchingService {
    @Override
    public String addPlayer(Integer userId, Integer rating) {
        System.out.println("add player: " + userId + " " + rating);
        return "add player successfully";
    }

    @Override
    public String removePlayer(Integer userId) {
        System.out.println("remove player: " + userId);
        return "remove player successfully";
    }
}

实现匹配的Controller

matchingsystem\controller\MatchingController.java

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

注意:这里用的是MultiValueMap,即一个键值key可以对应多个value值,一个key对应一个列表list
定义:MultiValueMap<String, String> valueMap = new LinkedMultiValueMap<>();
这里如果用@Requestparam + map接收所有参数的话会不严谨,因为若url返回的是多个参数的话,map只能接受一个参数,即一个value,有时候匹配的会返回多个rating相近的人的结果,这时候如果用map接收可能会产生一些蜜汁错误,因此用MultiValueMap的话可以省事点。。。
用到的api:
MultiValueMap.getFirst(key)返回对应key的value列表的第一个值。

设置网关
为了防止用户破坏系统,我们应该设置一定的访问权限,让自己的系统更加安全

这里可以仿照之前写过的SecurityConfig

添加spring security依赖

<dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
        <version>2.7.1</version>
    </dependency>

配置SecurityConfig

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
  ...
                .antMatchers("/player/add/","/player/remove/").hasIpAddress("127.0.0.1") //只允许本地访问
  ...
}

设置Matching System项目的启动入口

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);
    }
}
实现收到请求后的匹配具体逻辑

思路:把所有当前匹配的用户放在一个数组(matchinPool)里,每隔1s扫描一遍数组,把rating较接近的两名用户匹配在一起,随着时间的推移,两名用户允许的rating差可以不断扩大,保证了所有用户都可以匹配在一起。

在Impl文件夹里新建一个utils工具包,编写MatchingPool.java和Player.java类(对应于上面的数组和用户信息)

MatchingPool.java是一个多线程的类,要继承自Thread类

在匹配服务里把实现添加与删除用户的逻辑

  • 匹配逻辑:搞个无限循环,周期性执行,每次sleep(1000),若没有匹配的人选,则等待时间++,若有匹配的人选则进行匹配。匹配的rating差会随着等待时间而增加(rating差每等待1s则*10)。

  • 匹配原则:为了提高用户体验,等待时间越长的玩家越优先匹配。即列表players从前往后匹配。用一个标记数组标记有没有匹配过即可,checkMatched()是判断这两个玩家是否能成功匹配在一起。sendResult()是发送匹配结果。

TIPS:这里标注一下我初学遇到的坑点,ArrayList循环删除某个元素不能直接循环一遍然后remove,因为每次循环的时候,ArrayList的size()都会改变,所以循环是有问题的,这样只能保证你删掉一个符合要求的元素,而不能实现循环删掉所有符合要求的元素,因此我们要从另一个角度思考问题,用一个新的ArrayList存放每一个不需要删除的元素,然后原来的引用直接指向新的List即可。
这里也提供另一种实现循环remove的方法:用迭代器Iterator

Iterator<Player> iterator = players.iterator();
       while (iterator.hasNext()) {
           if (要删除的条件) iterator.remove();
       }

但是我们上面的删除还涉及到used数组,所以迭代器删除法并不适合,所以要用新列表赋值法!!

对于checkMatch判断两个玩家是否能成功匹配,还要考虑其等待时间,要判断分差能不能小于等于a与b的等待时间的最小值*10即 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

Matching System调用ws端的接口

为了实现springboot之间的通信,我们要像前文一样使用一个Bean类,方法为调用RestTemplate类。即上文的RestTemplateConfig.java

为了能让Spring里面的Bean注入进来,需要在MatchingPool.java里加上@Component

对于匹配时断开连接的处理

如果一名玩家开始匹配后断开了连接,按照我们上面的做法,断开连接后的玩家会一直处于匹配池中,这样我们的Matching System后端会报错,因为我们凡是要获取玩家信息的时候,该玩家已经掉线了,不存在了,会get一个空玩家信息,空信息是没有属性的,而我们后面会调用玩家属性,这是不合理的,肯定会报错的,我们需要修改这个bug:在每次get之前都要判断一下玩家信息是否为空,若不为空再进行下面的逻辑。

5.Springboot实现简易AI对战

Springboot实现类似OJ的代码执行功能

由于我们这个项目是可以通过执行bot代码来实现AI的操作的,因此我们要设计一个新的微服务Bot Running System专门去跑我们的代码,类似于OJ,但又并不是传统意义上的oj评测,而是通过代码的运行来完成AI的操作。

同样的,Bot Running System也会有一个独立的线程Bot Pool去存放我们的每一个bot代码且将其执行,并将代码执行结果返回给步骤判断阶段,也就是ws端的Next Step阶段。

以上就是我们代码执行微服务的主要逻辑思路。

创建后端BotRunningSystem

  • 与前面类似,在根目录backendcloud下创建一个新模块BotRunningSystem
  • 将模块MatchingSystem的依赖都复制到BotRunningSystem里
  • 添加新依赖 joor-java-8 :可以在Java中动态编译Java代码
  • 未来如果想要执行其他语言的代码,可以在云端创建一个有内存上限的docker,在docker里面执行代码
  • 给该springboot项目创建端口3002(server.port=3002) \resources\application.properties

把Main文件重命名为常见的BotRunningSystemApplication作为该springboot的入口
\botrunningsystem\BotRunningSystemApplication

@SpringBootApplication

public class BotRunningSystemApplication {
    public static void main(String[] args) {
        SpringApplication.run(BotRunningSystemApplication.class, args);
    }
}

编写BotRunningSystem的api接口

与前面所有的springboot项目类似,我们要实现接口就得先创建好controller层和service层,在service层里新建接口,在impl里实现相应的接口,最后controller层定义相对应的url调用对应的服务。
service层

package com.popgame.botrunningsystem.service;

public interface BotRunningService {
    String addBot(Integer userId, String botCode, String input);

}

实现该接口

@Service

public class BotRunningServiceImpl implements BotRunningService {
    @Override
    public String addBot(Integer userId, String botCode, String input) {
        System.out.println("add bot: " + userId + " " + botCode + " " + input);
        return "add bot successfully";
    }
}

编写Controller层,BotRunningController.java

package com.popgame.botrunningsystem.controller;

import com.popgame.botrunningsystem.service.BotRunningService;
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 BotRunningController {
    @Autowired
    private BotRunningService botRunningService;

    @PostMapping("/bot/add/")
    public String addBot(@RequestParam MultiValueMap<String, String> data) {
        Integer userId = Integer.parseInt(Objects.requireNonNull(data.getFirst("user_id")));
        String botCode = data.getFirst("bot_code");
        String input = data.getFirst("input");
        return botRunningService.addBot(userId,botCode,input);
    }
}
给该url添加网关

与上一章写的config目录下的RestTemplateConfig.java与SecurityConfig.java 类似,只需要更改url即可,这里就不再赘述了。

如上,我们的BotRunningSystem后端框架就搭建完了,还需要更改前端让用户可以选择用bot对战还是人工对战

工程课 SpringBoot-7.1. Springboot实现简易ai对战(https://www.acwing.com/solution/content/133480/)

6.Springboot创建排行榜和录像回放

我采用了Elo Rating System来作为我的rating变化机制

Elo Rating System是由匈牙利裔美国物理学家Arpad Elo创建的一个衡量各类对弈活动水平的评价方法,是当今对弈水平评估的公认的权威方法。被广泛用于国际象棋、围棋、足球、篮球等运动。网络游戏英雄联盟、魔兽世界内的竞技对战系统也采用此分级制度。

总结:

从Elo的工作模式中我们可以得出以下几点:

Elo会给出玩家一场对局的获胜概率。Elo积分相差越大,积分高的一方获胜概率就越大;
每一场对局后,对阵双方都会进行一部分积分交换,胜者得分,败者失分;
如果两名玩家的积分相差很大,代表高分方获胜的概率极大,因此即便赢了也涨不了多少分,败方也掉不了多少分。但倘若被低分方爆出冷门,那高分方将失去大量分数。

实现对局列表页面(https://www.acwing.com/solution/content/134342/)


  目录