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

news2024/10/6 2:23:20

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 {
    @Override
    public 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 一直带着 bot
            players.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 找不到,返回 null
        Bot 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, botCode
        this.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();
      
          @Override
          public 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 {
          @Override
          public 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 {
    @Autowired
    private 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 {
    @Override
    public 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;

    @Autowired
    public 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.coloradmin.cn/o/1903600.html

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

相关文章

Keysight 是德 DSA91304A 高性能示波器

Keysight 是德 DSA91304A 高性能示波器 DSA91304A Infiniium 高性能示波器&#xff1a;13 GHz 13 GHz4个模拟通道高达 1 Gpts 存储器和 40 GSa/s 采样率可以提供更完整的信号迹线捕获50 mV/格时低至 1.73 mVrms 的本底噪声和深入的抖动分析功能可以确保卓越的测量精度硬件加速…

C语言_数据的存储

数据类型介绍 1. 整形家族 //字符存储的时候&#xff0c;存储的是ASCII值&#xff0c;是整型 //char 默认是unsigned char还是signed char标准没有规定&#xff0c;其他类型都默认是signed char&#xff0c;unsigned char&#xff0c;signed char short&#xff0c;unsigned s…

windows机器免密登录linux主机

1. 正常连接需要输入密码 ssh root1.1.1.1 2. 在Windows上生成SSH密钥对&#xff08;如果你还没有的话&#xff09;&#xff1a; ssh-keygen 3. scp将id_rsa.pub传输到对应的主机 4.对应机器上查看 5.从windows上免密登录

rsyslog日志转发

前言 Rsyslog可用于接受来自各种来源(本地和网络)的输入&#xff0c;转换它们&#xff0c;并将结果输出到不同&#xff08;通过模板和filter过滤&#xff09;的目的地&#xff08;目录文件中&#xff09; rsyslog是一个开源工具&#xff0c;被广泛用于Linux系统以通过TCP/UDP…

cs231n 作业3

使用普通RNN进行图像标注 单个RNN神经元行为 前向传播&#xff1a; 反向传播&#xff1a; def rnn_step_backward(dnext_h, cache):dx, dprev_h, dWx, dWh, db None, None, None, None, Nonex, Wx, Wh, prev_h, next_h cachedtanh 1 - next_h**2dx (dnext_h*dtanh).dot(…

第T4周:使用TensorFlow实现猴痘病识别

&#x1f368; 本文为&#x1f517;365天深度学习训练营 中的学习记录博客&#x1f356; 原作者&#xff1a;K同学啊 文章目录 一、前期工作1.设置GPU&#xff08;如果使用的是CPU可以忽略这步&#xff09;2. 导入数据3. 查看数据 二、数据预处理1、加载数据2、数据可视化3、再…

人脸识别课堂签到系统【PyQt5实现】

人脸识别签到系统 1、运用场景 课堂签到,上班打卡,进出门身份验证。 2、功能类别 人脸录入,打卡签到,声音提醒,打卡信息导出,打包成exe可执行文件 3、技术栈 python3.8,sqlite3,opencv,face_recognition,PyQt5,csv 4、流程图 1、导入库 2、编写UI界面 3、打…

Linux服务器使用总结-不定时更新

# 查看升级日志 cat /var/log/dpkg.log |grep nvidia|grep libnvidia-common

C++ 多态篇

文章目录 1. 多态的概念和实现1.1 概念1.2 实现1.2.1 协变1.2.2 析构函数1.2.3 子类虚函数不加virtual 2. C11 final和override3.1 final3.2 override 3. 函数重载、重写与隐藏4. 多态的原理5. 抽象类6.单继承和多继承的虚表6.1 单继承6.2 多继承 7. 菱形继承的虚表(了解)7.1 菱…

为企业知识库选模型?全球AI大模型知识库RAG场景基准测试排名

大语言模型常见基准测试 大家对于AI模型理解和推理能力的的基准测试一定非常熟悉了&#xff0c;比如MMLU&#xff08;大规模多任务语言理解&#xff09;、GPQA&#xff08;研究生级别知识问答&#xff09;、GSMSK&#xff08;研究生数学知识考察&#xff09;、MATH&#xff08…

NextJs - SSR渲染解决antd首屏加载CSS样式的闪烁问题

NextJs - SSR渲染解决antd首屏加载CSS样式的闪烁问题 闪烁现状解决方案 闪烁现状 我们写一个非常简单的页面&#xff1a; import { Button } from antdexport default async function Page() {return <><Button typeprimary>AAA</Button></> }NextJs…

Linux防火墙使用(firewalld与iptables)

防火墙概述 防火墙是一种由硬件和软件组合而成&#xff0c;在内部网和外部网之间、专有网和公共网之间构造的保护屏障&#xff0c;用以保护用户资料和信息安全的一种技术 防火墙作用在于及时发现并处理计算机网络运行时可能存在的安全风险、数据传输等问题&#xff0c;从而实现…

SSRF靶场通关合集

目录 前言 SSRF总结 1.pikachu 1.1SSRF(curl) 1.1.1http协议 1.1.2 file协议查看本地文件 1.1.3 dict协议扫描内网主机开放端口 1.2 SSRF&#xff08;file_get_content&#xff09; 1.2.1 file读取本地文件 1.2.2 php://filter/读php源代码 2.DoraBox靶场 前言 最近…

[终端安全]-3 移动终端之硬件安全(TEE)

&#xff08;参考资料&#xff1a;TrustZone for V8-A. pdf&#xff0c;来源ARM DEVELOPER官网&#xff09; TEE&#xff08;Trusted Execution Environment&#xff0c;可信执行环境&#xff09;是用于执行敏感代码和处理敏感数据的独立安全区域&#xff1b;以ARM TrustZone为…

一.2.(3)放大电路的图解分析方法和微变等效电路分析方法;

放大电路的主要分析方法:图解法、微变等效电路法 这里以共射放大电路为例 (1) 图解法: 1.静态分析 首先确定静态工作点Q,然后根据电路的特点,做出直流负载线,进而画出交流负载线,最后,画出各极电流电压的波形。求出最大不失真输出电压。 估算IBQ&#xff0c;然后根据数据手册里…

『大模型笔记』《Pytorch实用教程》(第二版)

『大模型笔记』《Pytorch实用教程》(第二版) 文章目录 一. 《Pytorch实用教程》(第二版)1.1 上篇1.2 中篇1.3 下篇1.4 本书亮点1.5 本书内容及结构二. 参考文献🖥️ 配套代码(开源免费):https://github.com/TingsongYu/PyTorch-Tutorial-2nd📚 在线阅读(开源免费)…

WebAssembly场景及未来

引言 从前面的文章中&#xff0c;我们已经了解了 WebAssembly&#xff08;WASM&#xff09; 的基本知识&#xff0c;演进历程&#xff0c;以及简单的使用方法。通过全面了解了WebAssembly的设计初衷和优势&#xff0c;我们接下来要知道在什么样的场景中我们会使用 WASM 呢&…

多表查询sql

概述&#xff1a;项目开发中,在进行数据库表结构设计时,会根据业务需求及业务模块之间的关系,分析并设计表结构,由于业务之间相互关联,所以各个表结构之间也存在着各种联系&#xff0c;分为三种&#xff1a; 一对多多对多一对一 一、多表关系 一对多 案例&#xff1a;部门与…

昇思25天学习打卡营第17天 | K近邻算法实现红酒聚类

内容介绍&#xff1a; K近邻算法&#xff08;K-Nearest-Neighbor, KNN&#xff09;是一种用于分类和回归的非参数统计方法&#xff0c;是机器学习最基础的算法之一。它正是基于以上思想&#xff1a;要确定一个样本的类别&#xff0c;可以计算它与所有训练样本的距离&#xff0…

nacos-sdk-python——Python版本Nacos客户端

Nacos&#xff08;Naming and Configuration Service&#xff09;是阿里巴巴开源的一款动态服务发现、配置管理和服务管理平台。它主要用于解决微服务架构中服务发现和配置管理的问题&#xff0c;提供了一站式解决方案。以下是 Nacos 的几个关键功能&#xff1a; 服务发现和健康…