bot代码执行 排行榜页面

Bot代码的执行(botrunningsystem)

  • 添加依赖:
<dependency>
    <groupId>org.jooq</groupId>
    <artifactId>joor-java-8</artifactId>
    <version>0.9.14</version>
</dependency>
  • 配置restTemplatesecurity
@Configuration
public class RestTemplateConfig {
    @Bean
    public RestTemplate getRestTemplate() {
        return new RestTemplate();
    }
}
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                .antMatchers("/bot/add/").hasIpAddress("127.0.0.1")
                .antMatchers(HttpMethod.OPTIONS).permitAll()
                .anyRequest().authenticated();
    }
}

每次后端(backend)会调用bot/add/接口往bots队列中添加Bot

public void addBot(Integer userId, String botCode, String input) {
    lock.lock();
    try {
        bots.add(new Bot(userId, botCode, input));
        // 当 bot 添加结束,需要唤起其他线程进行消费
        condition.signalAll();
    } finally {
        lock.unlock();
    }
}

botPool的核心代码(这里相当于手动实现了一个消息队列):

private void consume(Bot bot) {
    Consumer consumer = new Consumer();
    consumer.startTimeout(2000, bot);
}

@Override
public void run() {
    while (true) {
        lock.lock();
        if (bots.isEmpty()) {
            try {
                // 当队列中没有等待消费的Bot时,让线程等待,当有新添加的Bot时,会被condition.signalAll();唤醒
                condition.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
                break;
            } finally {
                lock.unlock();
            }
        } else {
            Bot bot = bots.poll();
            lock.unlock();
            // consume 可能会执行几秒钟,需要先解锁
            consume(bot);
        }
    }
}

而在Consumer中,需要控制每个Bot的执行时间:

public void startTimeout(long timeout, Bot bot) {
    this.bot = bot;
    this.start();
    try {
        this.join(timeout);
    } catch (InterruptedException e) {
        e.printStackTrace();
    } finally {
        // 最多等待 timeout 秒,然后中断
        this.interrupt();
    }
}

Consumer中的核心代码:

@Override
public void run() {
    UUID uuid = UUID.randomUUID();
    String uid = uuid.toString().substring(0, 8);
    // 需要保障每次的类名不一样,否则只编译一次
    Supplier<Integer> botInterface = Reflect.compile(
            "com.kob.botrunningsystem.utils.Bot" + uid,
            addUid(bot.getBotCode(), uid)
    ).create().get();
    // 将输入写入文件以便后续扩展(后期可以在docker中运行,就需要从文件中读取输入)
    File file = new File("input.txt");
    try (PrintWriter fout = new PrintWriter(file)) {
        fout.println(bot.getInput());
        fout.flush();
    } catch (FileNotFoundException e) {
        throw new RuntimeException(e);
    }
    // 执行Bot代码,获取结果
    Integer direction = botInterface.get();
    // 将Bot执行结果返回
    MultiValueMap<String, String> data = new LinkedMultiValueMap<>();
    data.add("userId", bot.getUserId().toString());
    data.add("direction", direction.toString());

    restTemplate.postForObject(RECEIVE_BOT_MOVE_URL, data, String.class);
}

传递bot信息

  • 传递路径:
    • 前端选择人或Bot开始匹配 ->
    • 3000服务websocket中startMatching函数 ->
    • 匹配系统添加玩家 ->
    • 匹配池添加玩家(Player类添加botId信息), 进行匹配 ->
    • 匹配成功, 发送信息添加BotId ->
    • 3000服务接收匹配系统传递的数据, 调用startGame(添加botId参数) ->
    • Game类中添加相应玩家的bot信息, 在nextStep中判断是人工操作还是机器人操作向BotRunning服务发送信息

添加BotRunning服务

  • 设计:
    • Bot池: 单独的线程, 存储3000服务发送的bot信息, 使用自制消息队列控制池中bot
      • run方法中循环方式: 只有当队列不为空时, 去执行相应方法, 其他时间阻塞; 使用Condition进行控制
    • Consumer: 单独的线程, 用来执行Bot代码
    • Controller + Service : 提供相应的接口添加Bot信息
  • Bot池: 生产者消费者模型, 在对bot的操作时需要加锁, 因为涉及多个线程
    • addBot方法: 提供给外界添加任务的方法
      • condition.signalAll(): 当有任务进来时, 唤醒所有线程即当前阻塞的BOT_POOL, 会自己释放锁
    • consume: 消费bot, 即开启线程去执行Bot代码
    • run方法:
      • 池为空时: condition.await(), 释放当前锁, 阻塞当前线程; 异常需要手动释放锁
      • 不为空: 拿出bot并进行消费, consume; 先释放锁再去消费, 因为执行代码比较耗时
  • Consumer: 执行代码, 单独开启线程
    • startTimeout(timeout, bot): 设置代码执行最长时间对线程进行控制, 当超出时间或者执行完毕中断当前线程
      • 进来开启线程this.start(), 设置bot信息
      • 如何进行控制: join(timeout)方法: 线程执行完毕或timeout时间后, 执行join后面的代码(this.interrpt())
    • run方法: 执行代码
      • Reflect.compile("package name", "code").create.get(); : 需要保证类名不一致, 即在类名后添加随机Id
      • 生成的实例去执行接口响应的方法: nextMove(当前局面), 将返回值发送给3000服务
  • 3000服务接收下一步信息
    • 我们已经中断了从前端获取输入进行移动, 需要重新调用之前进行移动的方法 game.setNextStepA(direction);

游戏完整的流程

d920547a3ff846296c9f05f7b33801f5-ef4ed006
  • client1, client2点击开始匹配
  • 3000服务通过websocket接受玩家信息, 发送给matching服务
  • matching服务匹配池接收3000服务发送的玩家信息, 通过相应的策略匹配两名玩家, 发送给3000服务
  • 3000服务接收对战玩家信息, 开启游戏startGame
  • startGame创建Game线程(创建地图即相关信息), 通过nextStep获取输入
  • nextStep
    • 用户手动输入
      • 判断输入合法性
        • 合法: 发送信息给前端, 继续获取下一步输入
        • 不合法: 结束游戏, 判断输赢
    • Bot执行
      • 发送bot信息给BotRunning服务
      • BotRunning服务通过BOT_POOL接收bot信息进行处理
      • Consumer消费bot, 生成下一步走向
      • 发送给3000服务
        • 判断输入合法性
          • 合法: 发送信息给前端, 继续获取下一步输入
          • 不合法: 结束游戏, 判断输赢
PixPin_2024-11-08_00-31-03-1ca858da

排行榜页面以及查漏补缺

https://git.acwing.com/Zwww/kob/-/commit/0ce3c2435887b9add751cc4ba594aa90ed4f2824


项目分区导航bot界面 增删改查 ⬅️ | 07-bot代码执行 排行榜页面 | ➡️ 微服务-匹配逻辑细化