gRPC结合vcpkg在x64-windows平台visual studio2019 cmake工程里面的应用

news2024/11/18 9:31:51

这里我们运用vcpkg去下载安装gRPC,进入vcpkg目录后,执行命令:.\vcpkg.exe install grpc:x64-windows

grpc在vcpkg里面安装完成后,我们就来使用grpc做一个简单的例子。

gRPC顾名思义,就是google的RPC方案,基于protobuf数据传输,其中proto文件的定义约定了服务器端和客户端的服务接口协议。

这里我们就用加法和乘法作为服务器端提供的服务,让客户端去调用(RPC),我们建立三个文件夹CPP_Server, CPP_Client, proto 来分别存储服务器端代码,客户端代码,以及proto文件。项目配置选用cmakelist.txt和cmake来管理。

1.  服务器端和客户端的proto定义(calculation.proto文件):

syntax = "proto3";
package data_handler;

service CalculationInterface{
    // Add operation
    rpc Add(AddRequest) returns (AddReply){}
    // Multiply operation
    rpc Multiply(MultiplyRequest) returns (MultiplyReply){}
}

message AddReply{
    int32 result = 1;
}

message AddRequest{
    int32 param1 = 1;
    int32 param2 = 2;
}

message MultiplyReply{
    int32 result = 1;
}

message MultiplyRequest{
    int32 param1 = 1;
    int32 param2 = 2;
}

 2. 服务器端代码

在服务器端,我们要在cmakelist里面进行proto文件的解析执行成相应的.pb.cc,.pb.h,.grpc.pb.cc,.grpc.pb.h文件,同时对项目文件的配置。

那么我们必然要先找到grpc, protobuf库和执行文件。这时候就需要用到vcpkg这套包管理器,

而下面这句话就是让vcpkg的包管理起作用的关键:

set(CMAKE_TOOLCHAIN_FILE "$ENV{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake" CACHE STRING "Vcpkg toolchain file")

注意这句话一定要在定义project名字之前,本例子是:project(CalculationInGrpcServer)

这样子后面的find_package, find_program, target_link_libraries等都会去vckpg里面找到。

cmake_minimum_required(VERSION 3.20)

# Note: 8 target(s) were omitted.


message("--------" $ENV{VCPKG_ROOT})

set(CMAKE_TOOLCHAIN_FILE "$ENV{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake" CACHE STRING "Vcpkg toolchain file")
project(CalculationInGrpcServer)

set(_GRPC_GRPCPP gRPC::grpc++)
set(_PROTOBUF_LIBPROTOBUF protobuf::libprotobuf)
set(_REFLECTION gRPC::grpc++_reflection)

set(_PROTOBUF_LIBPROTOBUF_D libprotobufd)
find_package(gRPC CONFIG REQUIRED)
find_program(_PROTOBUF_PROTOC protoc REQUIRED)
find_program(_GRPC_CPP_PLUGIN_EXECUTABLE grpc_cpp_plugin REQUIRED)

# Generated proto sources
get_filename_component(proto "../proto/calculation.proto" ABSOLUTE)
get_filename_component(proto_name "../proto/calculation.proto" NAME_WE)
get_filename_component(proto_path "${proto}" PATH)

set(proto_srcs "${proto_path}/${proto_name}.pb.cc")
set(proto_hdrs "${proto_path}/${proto_name}.pb.h")
set(grpc_srcs "${proto_path}/${proto_name}.grpc.pb.cc")
set(grpc_hdrs "${proto_path}/${proto_name}.grpc.pb.h")
message("------------------------------------------------")
message(${_PROTOBUF_PROTOC})
message(${_GRPC_CPP_PLUGIN_EXECUTABLE})
message(${proto_path})

message("-------------------------------------------------")
add_custom_command(
      OUTPUT "${proto_srcs}" "${proto_hdrs}" "${grpc_srcs}" "${grpc_hdrs}"
      COMMAND ${_PROTOBUF_PROTOC}
      ARGS --grpc_out "${proto_path}"
        --cpp_out "${proto_path}"
        -I "${proto_path}"
        --plugin=protoc-gen-grpc="${_GRPC_CPP_PLUGIN_EXECUTABLE}"
        "${proto}"
      DEPENDS "${proto}")

# Include generated *.pb.h files
include_directories(
    "${proto_path}"
    )

file(GLOB PUBLIC_HEADER ${CMAKE_CURRENT_BINARY_DIR}/*.h
                        ${PROJECT_SOURCE_DIR}/../proto/*.h)


add_executable(${PROJECT_NAME} CalculationServer.cc ${proto_srcs} ${grpc_srcs})
target_link_libraries(${PROJECT_NAME} PRIVATE gRPC::gpr gRPC::upb gRPC::grpc gRPC::grpc++)

message("protobuf libs are:")
message(${_PROTOBUF_LIBPROTOBUF_D})

在我们的服务器端代码里面着重用到的是::data_handler::CalculationInterface::Service,这个是proto解释器帮我们对proto文件解析成cc文件后,里面的一个Service接口,我们代码里面最主要是去实现这个接口,来看看吧:


#include <grpcpp/grpcpp.h>
#include <grpcpp/security/server_credentials.h>
#include <grpcpp/server.h>
#include <grpcpp/server_builder.h>
#include <grpcpp/server_context.h>

#include <calculation.grpc.pb.h>
#include <calculation.pb.h>
#include <thread>

using grpc::Server;
using grpc::ServerBuilder;
using ::grpc::ServerContext;
using grpc::ServerReader;
using grpc::ServerReaderWriter;
using grpc::ServerWriter;
using grpc::Status;

class CalculationInGrpcServerImpl final
    : public ::data_handler::CalculationInterface::Service {
public:
  virtual ~CalculationInGrpcServerImpl(){};

  // Add operation
  ::grpc::Status Add(::grpc::ServerContext* context,
                             const ::data_handler::AddRequest* request,
                             ::data_handler::AddReply* response) override;
  // Multiply operation
  ::grpc::Status Multiply(
      ::grpc::ServerContext* context,
      const ::data_handler::MultiplyRequest* request,
      ::data_handler::MultiplyReply* response) override;
};

::grpc::Status CalculationInGrpcServerImpl::Add(
    ::grpc::ServerContext* context,
    const ::data_handler::AddRequest* request,
    ::data_handler::AddReply* response) {
  if (!context || !request || !response) {
    return ::grpc::Status::CANCELLED;
  }
  int32_t a = request->param1();
  int32_t b = request->param2();
  int32_t result = a + b;
  response->set_result(result);
  std::cout << "Add operation: " << a << " + " << b << std::endl;
  std::cout << "The result is: " << result << std::endl;
  return ::grpc::Status::OK;
}

::grpc::Status CalculationInGrpcServerImpl::Multiply(
    ::grpc::ServerContext* context,
    const ::data_handler::MultiplyRequest* request,
    ::data_handler::MultiplyReply* response) {
  if (!context || !request || !response) {
    return ::grpc::Status::CANCELLED;
  }
  int32_t a = request->param1();
  int32_t b = request->param2();
  int32_t result = a * b;
  response->set_result(result);
  std::cout << "Multiply operation: " << a << " * " << b << std::endl;
  std::cout << "The result is: " << result << std::endl;
  return ::grpc::Status::OK;
}

// define the gRPC server
std::unique_ptr<Server> server_ptr;
CalculationInGrpcServerImpl service;

void RunServer(const std::string& server_address) {
  ServerBuilder builder;
  builder.AddListeningPort(server_address, grpc::InsecureServerCredentials());
  builder.RegisterService(&service);

  server_ptr = builder.BuildAndStart();
  std::cout << "Server(" << server_address << ") is listening on ..." << std::endl;
  std::cout << "Press 'q' to exit the server" << std::endl;

  server_ptr->Wait();
}

int main() { 

    std::string server_address("0.0.0.0:50051");
    std::thread server_thread(RunServer, server_address);

    bool running = true;
    while (running) {
      char c = getchar();
      if (c == '\n' || c == EOF) continue;
      if (c == 'q') {
        // reset running flag and shutdown server
        running = false;
        server_ptr->Shutdown();
      }
    }
    server_thread.join();
    return 0; 


}

大家有可能看到了main函数,本人偷懒,将其一起写在一个文件里了,最好还是将main函数实现放到另外的文件。当然我们重点是将grpc的运用,大家可以借鉴一下里面server是怎样绑定IP和port口,运行起来server的。

生成一下,看看是不是和预想的一样啊?

3. 客户端代码

客户端代码主要是调用服务器端的接口,就是上面写的接口,grpc通过一个stub代理来实现,这样我们就象调用本地的函数一样去远程调用函数接口了,从而达到访问服务的目的。

客户端的cmakelist.txt和服务器端的有点类似,我贴出来,大家看看就行:

cmake_minimum_required(VERSION 3.20)

# Note: 8 target(s) were omitted.


message("--------" $ENV{VCPKG_ROOT})

set(CMAKE_TOOLCHAIN_FILE "$ENV{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake" CACHE STRING "Vcpkg toolchain file")
project(CalculationInGrpcClient)

set(_GRPC_GRPCPP gRPC::grpc++)
set(_PROTOBUF_LIBPROTOBUF protobuf::libprotobuf)
set(_REFLECTION gRPC::grpc++_reflection)

set(_PROTOBUF_LIBPROTOBUF_D libprotobufd)
find_package(gRPC CONFIG REQUIRED)
find_program(_PROTOBUF_PROTOC protoc REQUIRED)
find_program(_GRPC_CPP_PLUGIN_EXECUTABLE grpc_cpp_plugin REQUIRED)

# Generated proto sources
get_filename_component(proto "../proto/calculation.proto" ABSOLUTE)
get_filename_component(proto_name "../proto/calculation.proto" NAME_WE)
get_filename_component(proto_path "${proto}" PATH)

set(proto_srcs "${proto_path}/${proto_name}.pb.cc")
set(proto_hdrs "${proto_path}/${proto_name}.pb.h")
set(grpc_srcs "${proto_path}/${proto_name}.grpc.pb.cc")
set(grpc_hdrs "${proto_path}/${proto_name}.grpc.pb.h")
message("------------------------------------------------")
message(${_PROTOBUF_PROTOC})
message(${_GRPC_CPP_PLUGIN_EXECUTABLE})
message(${proto_path})

message("-------------------------------------------------")
add_custom_command(
      OUTPUT "${proto_srcs}" "${proto_hdrs}" "${grpc_srcs}" "${grpc_hdrs}"
      COMMAND ${_PROTOBUF_PROTOC}
      ARGS --grpc_out "${proto_path}"
        --cpp_out "${proto_path}"
        -I "${proto_path}"
        --plugin=protoc-gen-grpc="${_GRPC_CPP_PLUGIN_EXECUTABLE}"
        "${proto}"
      DEPENDS "${proto}")

# Include generated *.pb.h files
include_directories(
    "${proto_path}"
    )

file(GLOB PUBLIC_HEADER ${CMAKE_CURRENT_BINARY_DIR}/*.h
                        ${PROJECT_SOURCE_DIR}/../proto/*.h)

add_executable(${PROJECT_NAME} CalculationClient.cc ${proto_srcs} ${grpc_srcs})
target_link_libraries(${PROJECT_NAME} PRIVATE gRPC::gpr gRPC::upb gRPC::grpc gRPC::grpc++)

message("protobuf libs are:")
message(${_PROTOBUF_LIBPROTOBUF_D})

 下面就是要介绍客户端的代码模块了,我这边简单封装了一个客户端类去调用服务,代码如下,

大家看看简单的request/reply调用方式。


#include <grpcpp/grpcpp.h>
#include <grpcpp/security/server_credentials.h>
#include <grpcpp/server.h>
#include <grpcpp/server_builder.h>
#include <grpcpp/server_context.h>

#include <calculation.grpc.pb.h>
#include <calculation.pb.h>
#include <thread>

using grpc::Server;
using grpc::ServerBuilder;
using ::grpc::ServerContext;
using grpc::ServerReader;
using grpc::ServerReaderWriter;
using grpc::ServerWriter;
using grpc::Status;

class CalculationInGrpcClient final {
public:
    CalculationInGrpcClient(CalculationInGrpcClient& param) = delete;
    CalculationInGrpcClient& operator=(CalculationInGrpcClient& param) = delete;

    CalculationInGrpcClient(std::shared_ptr<grpc::Channel> channelPtr);
    ~CalculationInGrpcClient(){};

    bool RequestAddOperation(const int32_t a, const int32_t b, int32_t& result);
    bool RequestMultiplyOperation(const int32_t a, const int32_t b, int32_t& result);

private:

    std::unique_ptr<data_handler::CalculationInterface::Stub> mStub;
};

CalculationInGrpcClient::CalculationInGrpcClient(
    std::shared_ptr<grpc::Channel> channel)
    : mStub(data_handler::CalculationInterface::NewStub(channel)) {}

bool CalculationInGrpcClient::RequestAddOperation(int32_t a, int32_t b,
                                                  int32_t& result) {
    grpc::Status grcpStatus;
    grpc::ClientContext context;
    data_handler::AddReply reply;
    data_handler::AddRequest request;
    request.set_param1(a);
    request.set_param2(b);
    grcpStatus = mStub->Add(&context, request, &reply);

    if (grcpStatus.error_code() == ::grpc::StatusCode::OK) {
        result = static_cast<int32_t>(reply.result());
        std::cout << "After adding operation, the result is: " 
                    << result
                    << std::endl;
        return true;
    } else {
        std::cout << "Server not running..." << std::endl;
    }
    return false;
}

bool CalculationInGrpcClient::RequestMultiplyOperation(int32_t a, int32_t b,
                                                       int32_t& result) {
    grpc::Status grcpStatus;
    grpc::ClientContext context;
    data_handler::MultiplyReply reply;
    data_handler::MultiplyRequest request;
    request.set_param1(a);
    request.set_param2(b);
    grcpStatus = mStub->Multiply(&context, request, &reply);

    if (grcpStatus.error_code() == ::grpc::StatusCode::OK) {
        result = static_cast<int32_t>(reply.result());
        std::cout << "After Multiplication operation, the result is: " 
                    << result
                    << std::endl;
        return true;
    } else {
        std::cout << "Server not running..." << std::endl;
    }
    return false;
}

void showHelp() {
    std::cout << "Calculation starts : \r\n\
            Press 'q' to exit the calculator. \r\n\
            "
            << std::endl;
}

bool FindParamters(const std::string& src, const char operation, int32_t& left, int32_t& right) {
  auto it = src.find(operation);
  if (it != std::string::npos) {
    std::string leftParam = src.substr(0, it);
    std::string rightParam = src.substr(it + 1, src.length() - it - 1);
    left = atoi(leftParam.c_str());
    right = atoi(rightParam.c_str());
    return true;
  }
  return false;
}

int main() { 

    showHelp();
    auto grpcChannel = grpc::CreateChannel("127.0.0.1:50051",
                                         grpc::InsecureChannelCredentials());
    if (!grpcChannel) {
        printf("Failed to create gRPC channel\n");
        return 0;
    }

    std::unique_ptr<CalculationInGrpcClient> clientPtr =
        std::make_unique<CalculationInGrpcClient>(grpcChannel);

    bool running = true;
    while (running) {
        std::string strTmp;
        std::getline(std::cin, strTmp);
        int32_t a = 0;
        int32_t b = 0;
        int32_t result = 0;
        if (FindParamters(strTmp, '+', a, b)) {
            if (clientPtr) {
            clientPtr->RequestAddOperation(a, b, result);
            }
        } else if (FindParamters(strTmp, '*', a, b)) {
            if (clientPtr) {
            clientPtr->RequestMultiplyOperation(a, b, result);
            }
        } else {
            // reserve
        }

        if (strTmp.find('q') != std::string::npos) {
            // reset running flag and shutdown server
            running = false;
        }
    }

    return 0; 


}

代码里面的request, reply基本上是固定格式:

    grpc::Status grcpStatus;
    grpc::ClientContext context;
    data_handler::MultiplyReply reply;
    data_handler::MultiplyRequest request;
    request.set_param1(a);
    request.set_param2(b);
    grcpStatus = mStub->Multiply(&context, request, &reply);

主要是stub去调用服务器端的接口,而前的context, request, reply都是准备工作。

grpc的createChannel绑定了服务器端的IP和port,进行服务器端和客户端通信,grpc都封装好了,固定格式调用就行。

4. 编译生成后,运行服务器端后,在运行客户端

 好了,就先到这里吧,代码只是demo,大家看看就行,里面有些不严谨的地方,多多担担!

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

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

相关文章

ProtoBuf 编码原理

因为涉及到分布式集群之间的通信&#xff0c;所以来学习了下 ProtoBuf&#xff0c;为什么选择 ProtoBuf 呢&#xff1f;主要还是因为相对于 json , xml 来说&#xff0c;ProtoBuf 传输效率更快&#xff0c;故需要了解下它的编码设计。 首先&#xff0c;每一个 message 进行编码…

科研人必看入门攻略(收藏版)

来源&#xff1a;投稿 作者&#xff1a;小灰灰 编辑&#xff1a;学姐 本文主要以如何做科研&#xff0c;日常内功修炼&#xff0c;常见科研误区&#xff0c;整理日常‘好论文’四个部分做以介绍&#xff0c;方便刚入门的科研者进行很好的规划。 1.如何做科研 1.1 选方向 当我…

【2023年第十一届泰迪杯数据挖掘挑战赛】A题:新冠疫情防控数据的分析 32页和40页论文及实现代码

【2023年第十一届泰迪杯数据挖掘挑战赛】A题&#xff1a;新冠疫情防控数据的分析 32页和40页论文及实现代码 相关链接 &#xff08;1&#xff09;建模方案 【2023年第十一届泰迪杯数据挖掘挑战赛】A题&#xff1a;新冠疫情防控数据的分析 建模方案及python代码详解 &#x…

【初学人工智能原理】【4】梯度下降和反向传播:能改(下)

前言 本文教程均来自b站【小白也能听懂的人工智能原理】&#xff0c;感兴趣的可自行到b站观看。 本文【原文】章节来自课程的对白&#xff0c;由于缺少图片可能无法理解&#xff0c;故放到了最后&#xff0c;建议直接看代码&#xff08;代码放到了前面&#xff09;。 代码实…

《LearnUE——基础指南:开篇—2》——准备工作

目录 0.2.1 UE的获取与创建项目 0.2.2 UE4编辑器界面布局 1. 编辑器介绍 2. 新建蓝图与蓝图拖动 3. 菜单介绍 4. 工具栏介绍 0.2.3 学习资料 0.2.1 UE的获取与创建项目 登录UE官网&#xff1a;www.unrealengine.com 点击“登录”&#xff0c;如果没有账号&#xff0…

自动驾驶——离散系统LQR的黎卡提方程Riccati公式推导与LQR工程化

1.LQR Question Background 之前写过连续系统的黎卡提方程Riccati推导,但是考虑到实际工程落地使用的是离散系统,于是又进行了离散黎卡提方程Riccati的公式推导。 2.Proof of Riccati Equation Formula for Discrete Systems 工程化落地,就是使用公式(2-14)实时计算控制率…

Windows编译安装AzerothCore魔兽世界开源服务端Lua脚本引擎Eluna和防作弊anticheat模块教程

Windows编译安装AzerothCore魔兽世界开源服务端Lua脚本引擎Eluna和防作弊anticheat模块教程 大家好&#xff0c;我是艾西今天和大家聊聊魔兽世界游戏内的脚步以及防作弊模块 Eluna是azerothcore服务端的Lua脚本引擎&#xff0c;可以在原有azerothcore的基础上实现很多拓展以及…

Linux操作系统指令(1)

目录 一、什么是Linux操作系统&#xff1f;二、Linux基本指令12.1 ls指令2.2 pwd指令2.3 cd指令2.4 touch指令2.5 mkdir指令&#xff08;非常重要&#xff09;2.6 rmdir指令 && rm 指令&#xff08;十分重要&#xff09;2.7 man指令&#xff08;非常重要&#xff09;2.…

基于松鼠算法的极限学习机(ELM)回归预测-附代码

基于松鼠算法的极限学习机(ELM)回归预测 文章目录 基于松鼠算法的极限学习机(ELM)回归预测1.极限学习机原理概述2.ELM学习算法3.回归问题数据处理4.基于松鼠算法优化的ELM5.测试结果6.参考文献7.Matlab代码 摘要&#xff1a;本文利用松鼠算法对极限学习机进行优化&#xff0c;并…

设计模式-创建型模式-(工厂、简单工厂、抽象工厂)

一、简单工厂模式 上代码 public class FoodFactory {public static Food makeFood(String name) {if (name.equals("noodle")) {Food noodle new LanZhouNoodle();noodle.addSpicy("more");return noodle;} else if (name.equals("chicken")…

Java中的注解和反射

注解 在Java程序中&#xff0c;我们可以在很多地方看到注解&#xff0c;如一下情况: 注解有检查和约束的作用 内置注解 当被Deprecated注解修饰的方法被使用的时候&#xff0c;方法会被画上杠&#xff1a; 元注解 当我们打开一个注解的时候&#xff0c;可以看到以下这些信…

一份标准的软件测试方案模板

第一章 概述 ​ 软件的错误是不可避免的&#xff0c;所以必须经过严格的测试。通过对本软件的测试&#xff0c;尽可能的发现软件中的错误&#xff0c;借以减少系统内部各模块的逻辑&#xff0c;功能上的缺陷和错误&#xff0c;保证每个单元能正确地实现其预期的功能。检测和排…

ROS 视觉建图不显示点云

I 乐视摄像头找不到彩色输出&#xff08;供参考&#xff09; 1.安装依赖 sudo apt install ros-$ROS_DISTRO-rgbd-launch ros-$ROS_DISTRO-libuvc ros-$ROS_DISTRO-libuvc-camera ros-$ROS_DISTRO-libuvc-ros2.进入ROS摄像机目录 cd /home/wheeltec/wheeltec_robot/src/ros_…

[计算机图形学]动画与模拟:关键帧动画、质点弹簧系统、运动学与绑定(前瞻预习/复习回顾)

一、动画的简要概念 动画和语言一样&#xff0c;一开始都是作为传达信息的工具。什么是动画呢&#xff1f;简单的理解就是让画面变成“活的”&#xff0c;也就是让它们能够动起来&#xff0c;其次需要一定的美观。在图形学上&#xff0c;我们可以把动画理解为建模或者是几何的一…

1.1 n阶行列式子的定义

学习目标&#xff1a; 掌握n阶行列式的定义和计算方法&#xff0c;并能够解决相关的数学问题。 学习步骤&#xff1a; 学习n阶行列式的定义&#xff0c;需要一定的抽象思维能力和数学基础。 了解基本概念和性质&#xff1a;在学习n阶行列式之前&#xff0c;需要先了解行列式…

设计模式——工厂模式(简单工厂、工厂方法、抽象工厂)

是什么&#xff1f; 工厂模式的目的是将创建对象的具体过程隐藏起来&#xff0c;从而达到更高的灵活性 工厂模式分为&#xff1a;简单工厂模式、工厂方法模式、抽象工厂模式&#xff1b; 为什么&#xff1f; 在Java中&#xff0c;万物皆是对象&#xff0c;我们在使用的时候…

(8) 支持向量机(下)(模型评估指标、ROC曲线)

文章目录 1 二分类SVC的进阶1.1 参数C的理解进阶1.2 二分类SVC中的样本不均衡问题&#xff1a;重要参数class_weight 2 SVC的模型评估指标2.1 混淆矩阵2.1.1 模型整体效果&#xff1a;准确率2.1.2 捕捉少数类的艺术&#xff1a;精确度&#xff0c;召回率和F1 score2.1.3 判错多…

【fluent】axial、radial、tangentia的含义和区别,axial/radial/tangentia coordination表达的意义

Reference 本文主要参考fluent的官方文档。 ANSYS FLUENT 12.0 User’s Guide - 31.2 Velocity Reporting Options 笛卡尔坐标系——Cartesian coordinate system 不管什么坐标系&#xff0c;都要讲究维数。这个维数根据问题难度确定&#xff0c;对于3D问题需要用三维坐…

魔兽服务端编译部署NPCBots和 Al机器人模块教程

魔兽服务端编译部署NPCBots和 Al机器人模块教程 大家好&#xff0c;我是艾西。在平时自己一个人玩魔兽的时候是不是会比较无聊&#xff0c;因为游戏机制或副本难度自己一个人无法进行快乐的玩耍。今天艾西教大家编译部署NPCBots和 Al机器人模块&#xff0c;直接一个人玩魔兽也…

Python研究生组蓝桥杯(省二)参赛感受

为什么参加蓝桥杯&#xff1f; 今年是读研的第一年&#xff0c;看着我简历上的获奖经历“优秀学生干部”“优秀志愿者”“优秀毕业生”......大学四年&#xff0c;我竟然没有一次竞赛类的经历&#xff0c;也没有拿得出手的项目&#xff0c;我陷入了深深的焦虑。 听说蓝桥杯的…