P5. 微服务: Bot代码的执行

P5. 微服务: Bot代码的执行

    • 0 概述
    • 1 Bot代码执行框架
    • 2 Bot代码传递给BotRunningSystem
    • 3 微服务: Bot代码执行的实现逻辑
      • 3.1 整体微服务逻辑概述
      • 3.2 生产者消费者模型实现
      • 3.3 consume() 执行代码函数的实现
      • 3.4 执行结果返回给 nextStep
    • 4 扩展
      • 4.1 Bot代码的语言

0 概述

  • 本章介绍的是项目中第二个微服务的设计与实现,具体包括如何从前端一步一步获取要执行的代码如何动态编译与执行不同玩家的代码执行完成之后结果如何一步一步返回到前端


1 Bot代码执行框架

首先要先写一个 api 接收传给该微服务的 Bot 代码,还是 service, service.impl, controller 的顺序实现,在实现完成后要添加 网关 SecurityConfig 和用于微服务之间通信的 RestTemplateComfig。两者的作用在P4. 微服务: 匹配系统(下)中有详细介绍。

具体逻辑先写个调试用的,看看能不能正确接收到传递过来的信息。

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

另外,整个Bot代码执行微服务的功能是接收代码,把代码扔到队列中,每次运行一段代码,再把运行结果返回给 game 服务器。

执行代码这边先规定用 Java 语言,上线之后可以更换成 docker 中执行其他语言。这边选择的是通过 joor 包的方式,在 Java 中动态编译 Java 代码需要添加依赖 joor-java-8



2 Bot代码传递给BotRunningSystem

要想让微服务执行 bot 代码首先要正确接收到 botCode,需要根据整个系统的通信路径一层一层改,最开始在 Client 可以选择真人出阵或者选择自己写的代码出阵,绑定前端的变量 select_bot,如果为 -1 则表示真人出战,不然就是 bot_id,用户在请求匹配的时候会把该变量作为 bot_id 参数带上。

前端只要实现一个复选框,再通过 v-model 绑定就行了,这边略过。

在这里插入图片描述

接下来展示如何一层一层往后传递的过程,正好复习一下之前整个系统的通信过程:

  • Client → WebSocketServer → MatchingSystem

    const click_match_btn = () => {if (match_btn_info.value === "开始匹配") {match_btn_info.value = "取消";store.state.pk.socket.send(JSON.stringify({event: "start-matching",bot_id: select_bot.value,}));}
    }
    
    @OnMessage
    public void onMessage(String message, Session session) {JSONObject data = JSONObject.parseObject(message);String event = data.getString("event");if ("start-matching".equals(event)) {startMatching(Integer.parseInt(data.getString("bot_id")));}
    }private void startMatching(Integer botId) {System.out.println("Start Matching!");MultiValueMap<String, String> data = new LinkedMultiValueMap<>();data.put("user_id", Collections.singletonList(this.user.getId().toString()));data.put("rating", Collections.singletonList(this.user.getRating().toString()));data.put("bot_id", Collections.singletonList(botId.toString()));restTemplate.postForObject(addPlayerUrl, data, String.class);
    }
    
  • MatchingSystem → MatchingPool

    @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")));Integer botId = Integer.parseInt(Objects.requireNonNull(data.getFirst("bot_id")));return matchingService.addPlayer(userId, rating, botId);
    }
    
    public void addPlayer(Integer userId, Integer rating, Integer botId) {lock.lock();try {// 这边要记得修改 Player 的属性,让 Player 一直带着 botplayers.add(new Player(userId, rating, botId, 0));} finally {lock.unlock();}
    }
    
  • MatchingPool → WebSocketServer

    private void sendResult(Player a, Player b) {MultiValueMap<String, String> data = new LinkedMultiValueMap<>();data.put("a_id", Collections.singletonList(a.getUserId().toString()));data.put("a_bot_id", Collections.singletonList(a.getBotId().toString()));data.put("b_id", Collections.singletonList(b.getUserId().toString()));data.put("b_bot_id", Collections.singletonList(b.getBotId().toString()));restTemplate.postForObject(startGameUrl, data, String.class);
    }
    
    @PostMapping("/pk/game/start/")
    public String startGame(@RequestParam MultiValueMap<String, String> data) {Integer aId = Integer.parseInt(Objects.requireNonNull(data.getFirst("a_id")));Integer bId = Integer.parseInt(Objects.requireNonNull(data.getFirst("b_id")));Integer aBotId = Integer.parseInt(Objects.requireNonNull(data.getFirst("a_bot_id")));Integer bBotId = Integer.parseInt(Objects.requireNonNull(data.getFirst("b_bot_id")));// 该方法最后调用的是 WebSockerServer 的 startGame 方法return startGameService.startGame(aId, bId, aBotId, bBotId);
    }
    
  • WebSocketServer → Game

    public static void startGame(Integer aId, Integer bId, Integer aBotId, Integer bBotId) {User a = userMapper.selectById(aId), b = userMapper.selectById(bId);// 这边查询的时候,如果是真人出马,那 botId 为 -1,意味着 selectById 找不到,返回 nullBot botA = botMapper.selectById(aBotId), botB = botMapper.selectById(bBotId);Game game = new Game(13,14,20,a.getId(),b.getId(),botA,botB);/* ... */
    }
    
    public Game(Integer rows, Integer cols, Integer inner_walls_count, Integer a_id, Integer b_id, Bot botA, Bot botB) {this.rows = rows;this.cols = cols;this.inner_walls_count = inner_walls_count;this.g = new int[rows][cols];Integer botIdA = -1, botIdB = -1;String botCodeA = "", botCodeB = "";// 判断是否为真人出战if (botA != null) {botIdA = botA.getId();botCodeA = botA.getContent();}if (botB != null) {botIdB = botB.getId();botCodeB = botB.getContent();}// 同样要更改 Player 属性,让其带着 botId, botCodethis.playerA = new Player(a_id, this.rows - 2, 1, new ArrayList<>(), botIdA, botCodeA);this.playerB = new Player(b_id, 1, this.cols - 2, new ArrayList<>(), botIdB, botCodeB);
    }
    
  • Game → BotRunningSystem

    其中 input 为当前局面的所有信息,在返回值中可以看到包含了地图信息,双方玩家的坐标和历史移动信息。

    private void sendBotCode(Player player) {// 如果是真人出马那就直接接收信息if (player.getBotId().equals(-1)) return;MultiValueMap<String, String> data = new LinkedMultiValueMap<>();data.put("user_id", Collections.singletonList(player.getId().toString()));data.put("bot_code", Collections.singletonList(player.getBotCode()));data.put("input", Collections.singletonList(getInput(player)));WebSocketServer.restTemplate.postForObject(botAddUrl, data, String.class);
    }private boolean nextStep() {/* ... */sendBotCode(playerA);sendBotCode(playerB);/* ... */
    }
    
    @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);
    }
    
  • 其他小细节处理:

    • 如果用的是 bot 出战,那就要屏蔽之后真人的输入。

      private void move(int direction) {if (game.getPlayerA().getId().equals(user.getId())) {if (game.getPlayerA().getBotId().equals(-1))game.setNextStepA(direction);} else if (game.getPlayerB().getId().equals(user.getId())) {if (game.getPlayerB().getBotId().equals(-1))game.setNextStepB(direction);}
      }
      


3 微服务: Bot代码执行的实现逻辑

3.1 整体微服务逻辑概述

该微服务的本质是生产者消费者模型: 可以不断接收用户的输入,当接收的信息比较多的时候,会把所有接收到的代码放到队列中,该队列存放的是当前所有的任务。生产者每次发送一个任务,就把该任务放到队列中;消费者是个不断循环的单独的线程,不停地等待新的任务过来,每完成一个工作,就检查队列是否为空,如果队列非空,则从队头拿出代码执行,直到队列中没有任务。


3.2 生产者消费者模型实现

  • 消费者线程 BotPool

    BotPool 不能像 MatchPool 一样每秒执行一次,应该是一旦有任务进来,就要立刻执行,不能让用户等待。因此不能用 sleep 的形式实现,这边选择使用条件变量来手动实现一个消息队列。

    • 线程的启动和之前一样在 SpringBoot 开启之前就启动。
    /* 在 BotRunningServiceImpl 中定义 */
    /* public final static BotPool botPool = new BotPool(); */@SpringBootApplication
    public class BotRunningSystemApplication {public static void main(String[] args) {BotRunningServiceImpl.botPool.start();SpringApplication.run(BotRunningSystemApplication.class, args);}
    }
    
    • run 本质上是手动实现了一个消息队列。

      和后面的 addBot(生产者) 一起,模拟个具体场景去理解整个过程。

      run 的具体逻辑过程: 如果 bots 队列为空,则该线程应该被阻塞住 condition.wait(),一旦有新的代码进来了,就要发个信号量唤醒该线程。通过条件变量 condition 来实现这个过程。

      队列 bots 虽然不是线程安全的,但是我们自己把它管理成线程安全的。bots 有两个线程操作,生产者给它加任务,消费者(当前线程)从队列中取出队头代码。所以通过加锁保证线程安全。

      private final ReentrantLock lock = new ReentrantLock();
      private final Condition condition = lock.newCondition();
      private final Queue<Bot> bots = new LinkedList<>();@Override
      public void run() {while (true) {lock.lock();if (bots.isEmpty()) {try {// 如果队列为空则阻塞住,await自动包含释放锁的操作condition.await();} catch (InterruptedException e) {e.printStackTrace();lock.unlock();break;}} else {Bot bot = bots.remove();lock.unlock();consume(bot);}}
      }
      
    • 向队列 bots 中添加一个任务(生产者)。

      public void addBot(Integer userId, String botCode, String input) {lock.lock();try {bots.add(new Bot(userId, botCode, input));condition.signalAll();} finally {lock.unlock();}
      }
      
      @Service
      public class BotRunningServiceImpl implements BotRunningService {public final static BotPool botPool = new BotPool();@Overridepublic String addBot(Integer userId, String botCode, String input) {System.out.println("add bot: " + userId + " " + botCode + " " + input);botPool.addBot(userId, botCode, input);return "add bot success!";}
      }
      

3.3 consume() 执行代码函数的实现

方便起见,我们用的是 joor 来动态编译与执行 Java 语言代码(字符串)。

为了让执行代码的时候时间可控,consume 每次要执行一个新的代码,都开一个线程去执行,因为线程如果超时的话会自动断掉。

如果玩家自己实现的代码有问题,那新开的那个线程会崩掉,但没有任何影响,直接执行下一个队头的代码。

  • 新开一个辅助类 Consumer 作为新的线程,实现一个 startTimeoutconsume() 调用:

    join(timeout) 函数的作用是: 在 join 之前新开了个新线程去执行 run,当前线程阻塞在 join,直到满足 (1) 等待时间到达 timeout 秒,或者是(2) 新线程执行完毕。最后记得不管无论如何,都去中断 interrupt 新线程。

    public void startTimeout(long timeout, Bot bot) {this.bot = bot;this.start();		try {this.join(timeout);					// 最多等待 timeout 的时间} catch (InterruptedException e) {e.printStackTrace();} finally {this.interrupt();   }
    }
    
  • Consumer 动态编译与执行代码过程:

    • 先在 botrunningsystem.utils 下定义一下用户需要实现的接口:

      public interface BotInterface {Integer nextMove(String input);
      }
      

      并且在 botrunningsystem.utils 下随便写一个代码实现,当做是用户传过来的代码(后续用的是前端传给后端的代码)。

      public class Bot implements com.kob.botrunningsystem.utils.BotInterface {@Overridepublic Integer nextMove(String input) {return 0;}
      }
      
    • 最后调用 joor.Reflect.compile 编译并执行一下代码:

      Reflect.compile() 编译代码,参数包括要编译的类所在的包名和具体的类定义。

      create().get() 用来创建并获取编译后的类实例。

      @Override
      public void run() {// 编译并且获取实例BotInterface botInterface = Reflect.compile("com.kob.botrunningsystem.utils.Bot","package com.kob.botrunningsystem.utils;\n" +"\n" +"public class Bot implements com.kob.botrunningsystem.utils.BotInterface {\n" +"    @Override\n" +"    public Integer nextMove(String input) {\n" +"        return 0;\n" +"    }\n" +"}\n").create().get();// 执行System.out.println(botInterface.nextMove(bot.getInput()));
      }
      
    • 上面就是基本的调用过程,但是有个问题就是 Reflect 编译的时候,重名的类只会编译一次。所以,之后每次执行的时候要保证每个用户的类名不同,这边使用 UUID 的随机字符串来实现。

      private String addUid(String code, String uid) {int k = code.indexOf(" implements com.kob.botrunningsystem.utils.BotInterface");return code.substring(0, k) + uid + code.substring(k);
      }@Override
      public void run() {UUID uuid = UUID.randomUUID();String uid = uuid.toString().substring(0, 8);BotInterface botInterface = Reflect.compile("com.kob.botrunningsystem.utils.Bot" + uid,addUid(bot.getBotCode(), uid)).create().get();System.out.println(botInterface.nextMove(bot.getInput()));
      }
      
  • 最后实现一下 BotPool 中的 consume:

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

至此,就可以动态地获取用户的输入代码,动态地编译并执行啦~~

别忘了模拟具体场景,看一下具体的执行流程。

下一步需要把执行后的结果返回给 nextStep


3.4 执行结果返回给 nextStep

backend 主服务器上实现一个接收 consume 返回的结果的接口 service, service.impl, controller, SecurityConfig:

@RestController
public class ReceiveBotMoveController {@Autowiredprivate ReceiveBotMoveService receiveBotMoveService;@PostMapping("/pk/receive/bot/move/")public String receiveBotMove(@RequestParam MultiValueMap<String, String> data) {Integer userId = Integer.parseInt(Objects.requireNonNull(data.getFirst("user_id")));Integer direction = Integer.parseInt(Objects.requireNonNull(data.getFirst("direction")));return receiveBotMoveService.receiveBotMove(userId, direction);}
}
@Service
public class ReceiveBotMoveServiceImpl implements ReceiveBotMoveService {@Overridepublic String receiveBotMove(Integer userId, Integer direction) {System.out.println("receive bot move: " + userId + " direction: " + direction);if (WebSocketServer.users.get(userId) != null) {Game game = WebSocketServer.users.get(userId).game;if (game != null) {if (game.getPlayerA().getId().equals(userId)) {game.setNextStepA(direction);} else if (game.getPlayerB().getId().equals(userId)) {game.setNextStepB(direction);}}}return "receive success";}
}

这样就实现接收到 direction 后把 direction 传给 game 的部分了,现在还要写一下 Consumer 怎么把 direction 传给 backend

复习一下,注入 restTemplate 的方法:

@Component // 记得加上注解
public class Consumer  extends Thread {private static RestTemplate restTemplate;@Autowiredpublic void setRestTemplate(RestTemplate restTemplate) {Consumer.restTemplate = restTemplate;}
}

Consumer 中使用微服务之间的通信,把 direction 传过去。

public void run() {/* ... */Integer direction = botInterface.nextMove(bot.getInput());MultiValueMap<String, String> data = new LinkedMultiValueMap<>();data.put("user_id", Collections.singletonList(bot.getUserId().toString()));data.put("direction", Collections.singletonList(direction.toString()));restTemplate.postForObject(receiveBotMoveUrl, data, String.class);
}

至此就可以开始调试啦,大功告成~



4 扩展

4.1 Bot代码的语言

在本文实现的系统中只能通过 Java 语言来写 Bot 的具体代码,如果想改成多语言执行只需要改 consume 函数,在项目上线之后每次执行程 Bot 代码需要开一个 docker,在 docker 中来实现。这就涉及到 Java 中如何执行终端命令,以及如何使用 docker 等问题了,后续要进一步拓展的时候可以实现。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/bicheng/42527.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Vulnhub靶场DC-5练习

目录 0x00 准备0x01 主机信息收集0x02 站点信息收集0x03 漏洞查找与利用1. 利用burpsuite爆破文件包含的参数2. 文件包含3. nginx日志挂马4. 反弹shell5.漏洞利用和提权 0x04 总结 0x00 准备 下载链接&#xff1a;https://download.vulnhub.com/dc/DC-5.zip 介绍&#xff1a; …

kafka-3

Kafka 消费组 consumer-offsets-N 稀疏索引 Kafka集群 集群搭建 集群启动和验证 Topic的意义 Topic和Partition 分区 副本 集群操作指令 多分区&多副本 多分区消费组 Rebalance机制 Rebalance机制处理流程 Rebalance机制-Range Rebalance机制-RoudRobin Rebalance机制-St…

计数排序的实现

原理 对一个数组进行遍历&#xff0c;再创建一个count数组 每找到一个值则在count数组中对应的位置加一&#xff0c;再在count数组中找到数字上方的count值&#xff0c;count值为几&#xff0c;则打印几次数组中的值. 开空间 相对映射 排序的实现 void CountSort(int* a, i…

PageHelper分页查询遇到的小问题

如果我们是这样子直接查询 pagehelper会拼接导我们的sql语句之后 这样子我们搜索出来的list&#xff0c;就是里面参杂了PageHelper的东西 所以我们可以直接转成我们的Page类型 但是如果我们搜索出来的是List<Blog>&#xff0c;我有些信息不想返回给前端&#xff0c;所以…

mac M1安装 VSCode

最近在学黑马程序员Java最新AI若依框架项目开发&#xff0c;里面前端用的是Visual Studio Code 所以我也就下载安装了一下&#xff0c;系统是M1芯片的&#xff0c;安装过程还是有点坑的写下来大家注意一下 1.在appstore中下载 2.在系统终端中输入 clang 显示如下图 那么在终端输…

C++语言相关的常见面试题目(一)

1. const关键字的作用 答&#xff1a; 省流&#xff1a;&#xff08;1&#xff09;定义变量&#xff0c;主要为了防止修改 (2) 修饰函数参数&#xff1a;防止在函数内被改变 &#xff08;3&#xff09;修饰函数的返回值 &#xff08;4&#xff09;修饰类中的成员函数 2. Sta…

并发编程-05AQS原理

并发编程-深入理解AQS之ReentrantLock 一 认识AQS 在讲解AQS原理以及相关同步器之前&#xff0c;我们需要对AQS有一些基本的认识&#xff0c;了解下它有什么样的机制&#xff0c;这样追踪源码的时候就不会太过于迷茫&#xff01; 1.1 什么是AQS java.util.concurrent包中的大…

LabVIEW与OpenCV图像处理对比

LabVIEW和OpenCV在图像处理方面各有特点。LabVIEW擅长图形化编程、实时处理和硬件集成&#xff0c;而OpenCV则提供丰富的算法和多语言支持。通过DLL、Python节点等方式&#xff0c;OpenCV的功能可在LabVIEW中实现。本文将结合具体案例详细分析两者的特点及实现方法。 LabVIEW与…

某大会的影响力正在扩大,吞噬了整个数据库世界!

1.规模空前 你是否曾被那句“上有天堂&#xff0c;下有苏杭”所打动&#xff0c;对杭州的湖光山色心驰神往&#xff1f;7月&#xff0c;正是夏意正浓的时节&#xff0c;也是游览杭州的最佳时期。这座古典与现代交融的城市将迎来了第13届PostgreSQL中国技术大会。作为全球数据库…

LabVIEW从测试曲线中提取特征值

在LabVIEW中开发用于从测试曲线中提取特征值的功能时&#xff0c;可以考虑以下几点&#xff1a; 数据采集与处理&#xff1a; 确保你能够有效地采集和处理测试曲线数据。这可能涉及使用DAQ模块或其他数据采集设备来获取曲线数据&#xff0c;并在LabVIEW中进行处理和分析。 特…

系统级别的原生弹窗窗口

<!DOCTYPE html> <html lang"zh-CN"><head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.0"><title>原生的弹出窗口dialog</title><style>…

【TB作品】51单片机 Proteus仿真 基于单片机的LCD12864万年历及温度监测系统设计

实验报告&#xff1a;基于单片机的LCD12864万年历及温度监测系统设计 背景介绍 本实验旨在设计并实现一个基于STC89C52单片机的LCD12864显示的万年历系统&#xff0c;同时集成温度传感器以实现温度监测功能。系统具备整点报时和闹钟功能&#xff0c;通过蜂鸣器进行提示。该设…

初中物理知识点总结(人教版)

初中物理知识点大全 声现象知识归纳 1 .声音的发生&#xff1a;由物体的振动而产生。振动停止&#xff0c;发声也停止。 2.声音的传播&#xff1a;声音靠介质传播。真空不能传声。通常我们听到的声音是靠空气传来的。 3.声速&#xff1a;在空气中传播速度是&#xff1a;340…

【2024_CUMCM】T检验、F检验、卡方检验

T检验 T检验主要用于比较两组数据的均值差异&#xff0c;适用于小样本数据分析。它可以分为单样本T检验、独立样本T检验和配对样本T检验。 单样本T检验用于比较一个样本与已知的总体均值差异&#xff0c;独立样本T检验用于比较两个独立样本的均值差异&#xff0c;配对样本T检…

【Transformer】transformer模型结构学习笔记

文章目录 1. transformer架构2. transformer子层解析3. transformer注意力机制4. transformer部分释疑 图1 transformer模型架构 图2 transformer主要模块简介 图3 encoder-decoder示意图N6 图4 encoder-decoder子层示意图 1. transformer架构 encoder-decoder框架是一种处理NL…

探索InitializingBean:Spring框架中的隐藏宝藏

​&#x1f308; 个人主页&#xff1a;danci_ &#x1f525; 系列专栏&#xff1a;《设计模式》《MYSQL》 &#x1f4aa;&#x1f3fb; 制定明确可量化的目标&#xff0c;坚持默默的做事。 ✨欢迎加入探索MYSQL索引数据结构之旅✨ &#x1f44b; Spring框架的浩瀚海洋中&#x…

Java里的Arrary详解

DK 中提供了一个专门用于操作数组的工具类&#xff0c;即Arrays 类&#xff0c;位于java.util 包中。该类提供了一些列方法来操作数组&#xff0c;如排序、复制、比较、填充等&#xff0c;用户直接调用这些方法即可不需要自己编码实现&#xff0c;降低了开发难度。 java.util.…

用PlantUML和语雀画UML类图

概述 首先阐述一下几个简单概念&#xff1a; UML&#xff1a;是统一建模语言&#xff08;Unified Modeling Language&#xff09;的缩写&#xff0c;它是一种用于软件工程的标准化建模语言&#xff0c;旨在提供一种通用的方式来可视化软件系统的结构、行为和交互。UML由Grady…

pyrender 离线渲染包安装教程

pyrender 离线渲染包安装教程 安装 安装 官方安装教程:https://pyrender.readthedocs.io/en/latest/install/index.html#installmesa 首先 pip install pyrenderclang6.0安装 下载地址:https://releases.llvm.org/download.html#6.0.0 注意下好是叫&#xff1a;clangllvm-6…

通信协议_C#实现自定义ModbusRTU主站

背景知识&#xff1a;modbus协议介绍 相关工具 mbslave:充当从站。虚拟串口工具:虚拟出一对串口。VS2022。 实现过程以及Demo 打开虚拟串口工具: 打开mbslave: 此处从站连接COM1口。 Demo实现 创建DLL库&#xff0c;创建ModbusRTU类,进行实现&#xff1a; using Syste…