当前位置: 首页 > news >正文

TensorRT(C++)基础代码解析

TensorRT(C++)基础代码解析


文章目录

  • TensorRT(C++)基础代码解析
  • 前言
  • 一、TensorRT工作流程
  • 二、C++ API
    • 2.1 构建阶段
      • 2.1.1 创建builder
      • 2.1.2 创建网络定义
      • 2.1.3 定义网络结构
      • 2.1.4 定义网络输入输出
      • 2.1.5 配置参数
      • 2.1.6 生成Engine
      • 2.1.7 保存为模型文件
      • 2.1.8 释放资源
    • 2.2 运行期
      • 2.2.1 创建一个runtime对象
      • 2.2.2 反序列化生成engine
      • 2.2.3 创建一个执行上下文ExecutionContext
      • 2.2.4 为推理填充输入
      • 2.2.4 调用enqueueV2来执行推理
      • 2.2.5 释放资源
  • 总结


前言


一、TensorRT工作流程

在这里插入图片描述

在这里插入图片描述

二、C++ API

2.1 构建阶段

TensorRT build engine的过程

  1. 创建builder
  2. 创建网络定义:builder —> network
  3. 配置参数:builder —> config
  4. 生成engine:builder —> engine (network, config)
  5. 序列化保存:engine —> serialize
  6. 释放资源:delete

2.1.1 创建builder

nvinfer1 是 NVIDIA TensorRT 的 C++ 接口命名空间。构建阶段的最高级别接口是 Builder。Builder负责优化一个模型,并产生Engine。通过如下接口创建一个Builder。

nvinfer1::IBuilder *builder = nvinfer1::createInferBuilder(logger);

2.1.2 创建网络定义

NetworkDefinition接口被用来定义模型。接口createNetworkV2接受配置参数,参数用按位标记的方式传入。比如上面激活explicitBatch,是通过1U << static_cast<uint32_t (nvinfer1::NetworkDefinitionCreationFlag::kEXPLICIT_BATCH); 将explicitBatch对应的配置位设置为1实现的。在新版本中,请使用createNetworkV2而非其他任何创建NetworkDefinition 的接口。

auto explicitBatch = 1U << static_cast<uint32_t
(nvinfer1::NetworkDefinitionCreationFlag::kEXPLICIT_BATCH);// 调用createNetworkV2创建网络定义,参数是显性batch
nvinfer1::INetworkDefinition *network = builder->createNetworkV2(explicitBatch);

2.1.3 定义网络结构

将模型转移到TensorRT的最常见的方式是以ONNX格式从框架中导出(将在后续课程进行介绍),并使用TensorRT的ONNX解析器来填充网络定义。同时,也可以使用TensorRT的Layer和Tensor等接口一步一步地进行定义。通过接口来定义网络的代码示例如下:

添加输入层

const int input_size = 3;
nvinfer1::ITensor *input = network->addInput("data", nvinfer1::DataType::kFLOAT,nvinfer1::Dims4{1, input_size, 1, 1}) 		 

添加全连接层

nvinfer1::IFullyConnectedLayer* fc1 = network->addFullyConnected(*input, output_size, fc1w, fc1b);

添加激活层

nvinfer1::IActivationLayer* relu1 = network->addActivation(*fc1->getOutput(0), nvinfer1::ActivationType::kRELU);

2.1.4 定义网络输入输出

定义哪些张量是网络的输入和输出。没有被标记为输出的张量被认为是瞬时值,可以被构建者优化掉。输入和输出张量必须被命名,以便在运行时,TensorRT知道如何将输入和输出缓冲区绑定到模型上。

// 设置输出名字
sigmoid->getOutput(0)->setName("output");
// 标记输出,没有标记会被当成顺时针优化掉
network->markOutput(*sigmoid->getOutput(0));

2.1.5 配置参数

添加相关Builder 的配置。createBuilderConfig接口被用来指定TensorRT应该如何优化模型

 nvinfer1::IBuilderConfig *config = builder->createBuilderConfig();// 设置最大工作空间大小,单位是字节
config->setMaxWorkspaceSize(1 << 28); // 256MiB

2.1.6 生成Engine

 nvinfer1::ICudaEngine *engine = builder->buildEngineWithConfig(*network, *config);

2.1.7 保存为模型文件

nvinfer1::IHostMemory *serialized_engine = engine->serialize();// 存入文件
std::ofstream outfile("model/mlp.engine", std::ios::binary);
assert(outfile.is_open() && "Failed to open file for writing");
outfile.write((char *)serialized_engine->data(), serialized_engine->size());

2.1.8 释放资源

outfile.close();
delete serialized_engine;
delete engine;
delete config;
delete network;

完整代码

/*
TensorRT build engine的过程
7. 创建builder
8. 创建网络定义:builder ---> network
9. 配置参数:builder ---> config
10. 生成engine:builder ---> engine (network, config)
11. 序列化保存:engine ---> serialize
12. 释放资源:delete
*/#include <iostream>
#include <fstream>
#include <cassert>
#include <vector>#include <NvInfer.h>// logger用来管控打印日志级别
// TRTLogger继承自nvinfer1::ILogger
class TRTLogger : public nvinfer1::ILogger
{void log(Severity severity, const char *msg) noexcept override{// 屏蔽INFO级别的日志if (severity != Severity::kINFO)std::cout << msg << std::endl;}
} gLogger;// 保存权重
void saveWeights(const std::string &filename, const float *data, int size)
{std::ofstream outfile(filename, std::ios::binary);assert(outfile.is_open() && "save weights failed");  // assert断言,如果条件不满足,就会报错outfile.write((char *)(&size), sizeof(int));         // 保存权重的大小outfile.write((char *)(data), size * sizeof(float)); // 保存权重的数据outfile.close();
}
// 读取权重
std::vector<float> loadWeights(const std::string &filename)
{std::ifstream infile(filename, std::ios::binary);assert(infile.is_open() && "load weights failed");int size;infile.read((char *)(&size), sizeof(int));                // 读取权重的大小std::vector<float> data(size);                            // 创建一个vector,大小为sizeinfile.read((char *)(data.data()), size * sizeof(float)); // 读取权重的数据infile.close();return data;
}int main()
{// ======= 1. 创建builder =======TRTLogger logger;nvinfer1::IBuilder *builder = nvinfer1::createInferBuilder(logger);// ======= 2. 创建网络定义:builder ---> network =======// 显性batch// 1 << 0 = 1,二进制移位,左移0位,相当于1(y左移x位,相当于y乘以2的x次方)auto explicitBatch = 1U << static_cast<uint32_t>(nvinfer1::NetworkDefinitionCreationFlag::kEXPLICIT_BATCH);// 调用createNetworkV2创建网络定义,参数是显性batchnvinfer1::INetworkDefinition *network = builder->createNetworkV2(explicitBatch);// 定义网络结构// mlp多层感知机:input(1,3,1,1) --> fc1 --> sigmoid --> output (2)// 创建一个input tensor ,参数分别是:name, data type, dimsconst int input_size = 3;nvinfer1::ITensor *input = network->addInput("data", nvinfer1::DataType::kFLOAT, nvinfer1::Dims4{1, input_size, 1, 1});// 创建全连接层fc1// weight and biasconst float *fc1_weight_data = new float[input_size * 2]{0.1, 0.2, 0.3, 0.4, 0.5, 0.6};const float *fc1_bias_data = new float[2]{0.1, 0.5};// 将权重保存到文件中,演示从别的来源加载权重saveWeights("model/fc1.wts", fc1_weight_data, 6);saveWeights("model/fc1.bias", fc1_bias_data, 2);// 读取权重auto fc1_weight_vec = loadWeights("model/fc1.wts");auto fc1_bias_vec = loadWeights("model/fc1.bias");// 转为nvinfer1::Weights类型,参数分别是:data type, data, sizenvinfer1::Weights fc1_weight{nvinfer1::DataType::kFLOAT, fc1_weight_vec.data(), fc1_weight_vec.size()};nvinfer1::Weights fc1_bias{nvinfer1::DataType::kFLOAT, fc1_bias_vec.data(), fc1_bias_vec.size()};const int output_size = 2;// 调用addFullyConnected创建全连接层,参数分别是:input tensor, output size, weight, biasnvinfer1::IFullyConnectedLayer *fc1 = network->addFullyConnected(*input, output_size, fc1_weight, fc1_bias);// 添加sigmoid激活层,参数分别是:input tensor, activation type(激活函数类型)nvinfer1::IActivationLayer *sigmoid = network->addActivation(*fc1->getOutput(0), nvinfer1::ActivationType::kSIGMOID);// 设置输出名字sigmoid->getOutput(0)->setName("output");// 标记输出,没有标记会被当成顺时针优化掉network->markOutput(*sigmoid->getOutput(0));// 设定最大batch sizebuilder->setMaxBatchSize(1);// ====== 3. 配置参数:builder ---> config ======// 添加配置参数,告诉TensorRT应该如何优化网络nvinfer1::IBuilderConfig *config = builder->createBuilderConfig();// 设置最大工作空间大小,单位是字节config->setMaxWorkspaceSize(1 << 28); // 256MiB// ====== 4. 创建engine:builder ---> network ---> config ======nvinfer1::ICudaEngine *engine = builder->buildEngineWithConfig(*network, *config);if (!engine){std::cerr << "Failed to create engine!" << std::endl;return -1;}// ====== 5. 序列化engine ======nvinfer1::IHostMemory *serialized_engine = engine->serialize();// 存入文件std::ofstream outfile("model/mlp.engine", std::ios::binary);assert(outfile.is_open() && "Failed to open file for writing");outfile.write((char *)serialized_engine->data(), serialized_engine->size());// ====== 6. 释放资源 ======// 理论上,这些资源都会在程序结束时自动释放,但是为了演示,这里手动释放部分outfile.close();delete serialized_engine;delete engine;delete config;delete network;delete builder;std::cout << "engine文件生成成功!" << std::endl;return 0;
}

2.2 运行期

在这里插入图片描述
TensorRT runtime 推理过程

  1. 创建一个runtime对象
  2. 反序列化生成engine:runtime —> engine
  3. 创建一个执行上下文ExecutionContext:engine —> context
  4. 填充数据
  5. 执行推理:context —> enqueueV2
  6. 释放资源:delete

2.2.1 创建一个runtime对象

TensorRT运行时的最高层级接口是Runtime

 nvinfer1::IRuntime *runtime = nvinfer1::createInferRuntime(logger);

2.2.2 反序列化生成engine

通过读取模型文件并反序列化,我们可以利用runtime生成Engine。

nvinfer1::ICudaEngine *engine = runtime->deserializeCudaEngine(engine_data.data(), engine_data.size(), nullptr);

2.2.3 创建一个执行上下文ExecutionContext

从Engine创建的ExecutionContext接口是调用推理的主要接口。ExecutionContext包含与特定调用相关的所有状态,因此可以有多个与单个引擎相关的上下文,且并行运行它们。

nvinfer1::IExecutionContext *context = engine->createExecutionContext();

2.2.4 为推理填充输入

首先创建CUDA Stream用于推理的执行。

cudaStream_t stream = nullptr;
cudaStreamCreate(&stream);

同时在CPU和GPU上分配输入输出内存,并将输入数据从CPU拷贝到GPU上。

// 输入数据
float* h_in_data = new float[3]{1.4, 3.2, 1.1};
int in_data_size = sizeof(float) * 3;
float* d_in_data = nullptr;
// 输出数据
float* h_out_data = new float[2]{0.0, 0.0};
int out_data_size = sizeof(float) * 2;
float* d_out_data = nullptr;
// 申请GPU上的内存
cudaMalloc(&d_in_data, in_data_size);
cudaMalloc(&d_out_data, out_data_size);
// 拷贝数据
cudaMemcpyAsync(d_in_data, h_in_data, in_data_size, cudaMemcpyHostToDevice, stream);
// enqueueV2中是把输入输出的内存地址放到bindings这个数组中,需要写代码时确定这些输入输出的顺序(这样容易出错,而且不好定位bug,所以新的接口取消了这样的方式,不过目前很多官方 sample 也在用v2)
float* bindings[] = {d_in_data, d_out_data};

2.2.4 调用enqueueV2来执行推理

bool success = context -> enqueueV2((void **) bindings, stream, nullptr);
// 数据从device --> host
cudaMemcpyAsync(host_output_data, device_output_data, output_data_size, cudaMemcpyDeviceToHost, stream);
// 等待流执行完毕
cudaStreamSynchronize(stream);
// 输出结果
std::cout << "输出结果: " << host_output_data[0] << " " << host_output_data[1] << std::endl;

2.2.5 释放资源

cudaStreamDestroy(stream);
cudaFree(device_input_data_address);
cudaFree(device_output_data_address);   
delete[] host_input_data;
delete[] host_output_data;delete context;
delete engine;
delete runtime;

完整代码

/*
使用.cu是希望使用CUDA的编译器NVCC,会自动连接cuda库TensorRT runtime 推理过程1. 创建一个runtime对象
2. 反序列化生成engine:runtime ---> engine
3. 创建一个执行上下文ExecutionContext:engine ---> context4. 填充数据5. 执行推理:context ---> enqueueV26. 释放资源:delete*/
#include <iostream>
#include <vector>
#include <fstream>
#include <cassert>#include "cuda_runtime.h"
#include "NvInfer.h"// logger用来管控打印日志级别
// TRTLogger继承自nvinfer1::ILogger
class TRTLogger : public nvinfer1::ILogger
{void log(Severity severity, const char *msg) noexcept override{// 屏蔽INFO级别的日志if (severity != Severity::kINFO)std::cout << msg << std::endl;}
} gLogger;// 加载模型
std::vector<unsigned char> loadEngineModel(const std::string &fileName)
{std::ifstream file(fileName, std::ios::binary);        // 以二进制方式读取assert(file.is_open() && "load engine model failed!"); // 断言file.seekg(0, std::ios::end); // 定位到文件末尾size_t size = file.tellg();   // 获取文件大小std::vector<unsigned char> data(size); // 创建一个vector,大小为sizefile.seekg(0, std::ios::beg);          // 定位到文件开头file.read((char *)data.data(), size);  // 读取文件内容到data中file.close();return data;
}int main()
{// ==================== 1. 创建一个runtime对象 ====================TRTLogger logger;nvinfer1::IRuntime *runtime = nvinfer1::createInferRuntime(logger);// ==================== 2. 反序列化生成engine ====================// 读取文件auto engineModel = loadEngineModel("./model/mlp.engine");// 调用runtime的反序列化方法,生成engine,参数分别是:模型数据地址,模型大小,pluginFactorynvinfer1::ICudaEngine *engine = runtime->deserializeCudaEngine(engineModel.data(), engineModel.size(), nullptr);if (!engine){std::cout << "deserialize engine failed!" << std::endl;return -1;}// ==================== 3. 创建一个执行上下文 ====================nvinfer1::IExecutionContext *context = engine->createExecutionContext();// ==================== 4. 填充数据 ====================// 设置stream 流cudaStream_t stream = nullptr;cudaStreamCreate(&stream);// 数据流转:host --> device ---> inference ---> host// 输入数据float *host_input_data = new float[3]{2, 4, 8}; // host 输入数据int input_data_size = 3 * sizeof(float);        // 输入数据大小float *device_input_data = nullptr;             // device 输入数据// 输出数据float *host_output_data = new float[2]{0, 0}; // host 输出数据int output_data_size = 2 * sizeof(float);     // 输出数据大小float *device_output_data = nullptr;          // device 输出数据// 申请device内存cudaMalloc((void **)&device_input_data, input_data_size);cudaMalloc((void **)&device_output_data, output_data_size);// host --> device// 参数分别是:目标地址,源地址,数据大小,拷贝方向cudaMemcpyAsync(device_input_data, host_input_data, input_data_size, cudaMemcpyHostToDevice, stream);// bindings告诉Context输入输出数据的位置float *bindings[] = {device_input_data, device_output_data};// ==================== 5. 执行推理 ====================bool success = context -> enqueueV2((void **) bindings, stream, nullptr);// 数据从device --> hostcudaMemcpyAsync(host_output_data, device_output_data, output_data_size, cudaMemcpyDeviceToHost, stream);// 等待流执行完毕cudaStreamSynchronize(stream);// 输出结果std::cout << "输出结果: " << host_output_data[0] << " " << host_output_data[1] << std::endl;// ==================== 6. 释放资源 ====================cudaStreamDestroy(stream);cudaFree(device_input_data); cudaFree(device_output_data);delete host_input_data;delete host_output_data;delete context;delete engine;delete runtime;return 0;
}

总结

TensorRT(C++)基础代码解析

相关文章:

TensorRT(C++)基础代码解析

TensorRT(C)基础代码解析 文章目录 TensorRT(C)基础代码解析前言一、TensorRT工作流程二、C API2.1 构建阶段2.1.1 创建builder2.1.2 创建网络定义2.1.3 定义网络结构2.1.4 定义网络输入输出2.1.5 配置参数2.1.6 生成Engine2.1.7 保存为模型文件2.1.8 释放资源 2.2 运行期2.2.1…...

如何查询MySQL中的树型表

在 MySQL 中查询树型表&#xff08;即具有层级结构的表&#xff09;可以使用递归查询或者使用嵌套集模型。下面分别介绍这两种方法&#xff1a; 递归查询&#xff1a;递归查询是通过自连接来实现的&#xff0c;可以使用 WITH RECURSIVE 关键字进行递归查询。假设有一个 catego…...

Programming Abstractions in C阅读笔记:p246-p247

《Programming Abstractions in C》学习第68天&#xff0c;p246-p247总结&#xff0c;总计2页。 一、技术总结 本章通过“the game of nim(尼姆游戏)”&#xff0c;这类以现实生活中事物作为例子进行讲解的情况&#xff0c;往往对学习者要求比较高&#xff0c;需要学习者具备…...

智能寻迹避障清障机器人设计(电路图附件+代码)

附 录 智能小车原理图 智能小车拓展板原理图 智能小车拓展板PCB 智能小车底板PCB Arduino UNO原理图 Arduino UNO PCB 程序部分 void Robot_Traction() //机器人循迹子程序{//有信号为LOW 没有信号为HIGHSR digitalRead(SensorRight);//有信号表明在白…...

设计模式-- 3.适配器模式

适配器模式 将一个类的接口转换成客户希望的另外一个接口。使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。 角色和职责 请求者&#xff08;client&#xff09;&#xff1a;客户端角色,需要使用适配器的对象&#xff0c;不需要关心适配器内部的实现&#xff0c;…...

设计一个简单的规则引擎

&#x1f44f;作者简介&#xff1a;大家好&#xff0c;我是爱吃芝士的土豆倪&#xff0c;24届校招生Java选手&#xff0c;很高兴认识大家&#x1f4d5;系列专栏&#xff1a;Spring原理、JUC原理、Kafka原理、分布式技术原理、数据库技术&#x1f525;如果感觉博主的文章还不错的…...

openssl3.2 - 官方demo学习 - digest - EVP_MD_stdin.c

文章目录 openssl3.2 - 官方demo学习 - digest - EVP_MD_stdin.c概述笔记END openssl3.2 - 官方demo学习 - digest - EVP_MD_stdin.c 概述 使用 SHA3-512 对stdin输入做摘要, 并输出摘要值. 笔记 /*! \file EVP_MD_stdin.c \note openssl3.2 - 官方demo学习 - digest - EVP…...

浅谈 Raft 分布式一致性协议|图解 Raft

前言 大家好&#xff0c;这里是白泽。本文是一年多前参加字节训练营针对 Raft 自我整理的笔记。 本篇文章将模拟一个KV数据读写服务&#xff0c;从提供单一节点读写服务&#xff0c;到结合分布式一致性协议&#xff08;Raft&#xff09;后&#xff0c;逐步扩展为一个分布式的…...

4_【Linux版】重装数据库问题处理记录

1、卸载已安装的oracle数据库。 2、知识点补充&#xff1a; 3、调整/dev/shm/的大小 【linux下修改/dev/shm tmpfs文件系统大小 - saratearing - 博客园 (cnblogs.com)】 mount -o remount,size100g /dev/shm 4、重装oracle后没有orainstRoot.sh 【重装oracle后没有orains…...

隧道应用2-netsh端口转发监听Meterpreter

流程介绍&#xff1a; 跳板机 A 和目标靶机 B 是可以互相访问到的&#xff0c;在服务器 A 上可以通过配置 netsh 端口映射访问 B 服务器。如果要拿 B 服务器的权限通常是生成正向后门&#xff0c;使用 kali 的 msf 正向连接B服务器&#xff0c;进而得到 Meterpreter&#xff0c…...

《Spring》--使用application.yml特性提供多环境开发解决方案/开发/测试/线上--方案1

阿丹有话说&#xff1a; 有不少同志有疑问说我正常开发的时候&#xff0c;需要自己搭建项目的时候。总是出现配置文件环境切换出现问题。多环境系列会出两个文章解决给搭建重点解决一下这个问题。给与两种解决的方案。正确让大家只需要按照步骤操作就可以完成。 原理&#xf…...

统计项目5000+,出具报表5分钟......捷顺科技数据中台怎么做?

捷顺创立于1992年&#xff0c;以智慧车行、人行出入口软硬件产品为依托&#xff0c;致力于智慧停车生态建设和运营&#xff0c;是出入口智能管理和智慧生态环境建设的开创者和引领者。 历经近三十年的发展&#xff0c;已经成为国内智慧停车领域的领军企业。公司集研、产、销一…...

力扣(105. 从前序与中序遍历序列构造二叉树,106. 从中序与后序遍历序列构造二叉树)

题目1链接 题目1&#xff1a; 思路&#xff1a;使用前序确定根&#xff0c;使用中序分左右子树&#xff0c;分治法。 难点&#xff1a;如何控制递归确定左右子树。 /*** Definition for a binary tree node.* struct TreeNode {* int val;* TreeNode *left;* T…...

网络安全技术新手入门:在docker上安装dvwa靶场

前言 准备工作&#xff1a;1.已经安装好kali linux 步骤总览&#xff1a;1.安装好docker 2.拖取镜像&#xff0c;安装dvwa 一、安装docker 输入命令&#xff1a;sudo su 输入命令&#xff1a;curl -fsSL https://download.docker.com/linux/debian/gpg | sudo apt-key …...

Docker 介绍 及 支持的操作系统

Docker组成&#xff1a; Docker主机(Host)&#xff1a; 一个物理机或虚拟机, 用于运行Docker服务进程和容器, 也成为宿主机, node节点。 Docker服务器端(Server)&#xff1a; Docker守护进程, 运行Docker容器。 Docker客户端(Client)&#xff1a; 客户端使用docker命令或其他工…...

大模型实战营Day5 LMDeploy大模型量化部署实践

模型部署 定义 产品形态 计算设备 大模型特点 内存开销大 动态shape 结构简单 部署挑战 设备存储 推理速度 服务质量 部署方案&#xff1a;技术点 &#xff08;模型并行 transformer计算和访存优化 低比特量化 Continuous Batch Page Attention&#xff09;方案&#xff08;…...

py连接sqlserver数据库报错问题处理。20009

报错 pymssql模块连接sqlserver出现如下错误&#xff1a; pymssql._pymssql.OperationalError) (20009, bDB-Lib error message 20009, severity 9:\nUnable to connect: Adaptive Server is unavailable or does not exist (passwordlocalhost)\n) 解决办法&#xff1a; 打…...

LTESniffer:一款功能强大的LTE上下行链路安全监控工具

关于LTESniffer LTESniffer是一款功能强大的LTE上下行链路安全监控工具&#xff0c;该工具是一款针对LTE的安全开源工具。 该工具首先可以解码物理下行控制信道&#xff08;PDCCH&#xff09;并获取所有活动用户的下行链路控制信息&#xff08;DCI&#xff09;和无线网络临时…...

SQL语句详解二-DDL(数据定义语言)

文章目录 操作数据库创建&#xff1a;Create查询&#xff1a;Retrieve修改&#xff1a;Update删除&#xff1a;Delete使用数据库 操作表常见的几种数据类型创建&#xff1a;Create复制表 查询&#xff1a;Retrieve修改&#xff1a;Update删除&#xff1a;Delete 操作数据库 创…...

web前端算法简介之链表

链表 链表 VS 数组链表类型链表基本操作 创建链表&#xff1a;插入操作&#xff1a;删除操作&#xff1a;查找操作&#xff1a;显示/打印链表&#xff1a;反转链表&#xff1a;合并两个有序链表&#xff1a;链表基本操作示例 JavaScript中&#xff0c;instanceof环形链表 判断…...

C++函数对象

任何定义了函数调用操作符的对象都是函数对象。C 支持创建、操作新的函数对象&#xff0c;同时也提供了许多内置的函数对象。 函数包装器 std::function 提供存储任意类型函数对象的支持。 function (C11) 包装具有指定函数调用签名的任意类型的可调用对象 (类模板) bad_funct…...

插件化简单介绍

关于作者&#xff1a;CSDN内容合伙人、技术专家&#xff0c; 从零开始做日活千万级APP。 专注于分享各领域原创系列文章 &#xff0c;擅长java后端、移动开发、商业变现、人工智能等&#xff0c;希望大家多多支持。 未经允许不得转载 目录 一、导读二、概览三、常见的插件化方案…...

[Beego]1.Beego简介以及beego环境搭建,bee脚手架的使用,创建,运行项目

一.Beego介绍 Beego是一个开源的基于Golang的MVC框架&#xff0c;主要用于Golang Web开发,Beego可以用来快速开发API、Web、后端服务等各种应用。 Golang 的Web开发框架有很多,从 github star 数量来看Gin>Beego>lris>Echo>Revel>Buffalo 目前国内用的比较多的就…...

Tomcat 静态资源访问与项目根路径设置(AI问答)

一个静态文件&#xff0c;放在Tomcat中&#xff0c;希望能够通过网络访问&#xff0c;应该放在哪里&#xff1f; 在Apache Tomcat中&#xff0c;如果想要部署静态文件&#xff08;例如HTML、CSS、JavaScript、图片等&#xff09;并能通过网络访问&#xff0c;通常需要将这些文…...

Docker实战09|使用AUFS包装busybox

前几篇文章中&#xff0c;重点讲解了如何实现构建容器&#xff0c;需要回顾的小伙伴可以看以下文章&#xff1a; 《Docker实战06&#xff5c;深入剖析Docker Run命令》《Docker实战07&#xff5c;Docker增加容器资源限制》《Docker实战08&#xff5c;Docker管道及环境变量识别…...

什么是uni.request()?如何使用它?

uni.request()是uni-app提供的一个用于发起网络请求的API。 使用uni.request()的步骤如下&#xff1a; 在需要发起网络请求的页面中引入uni.request()方法。 调用uni.request()方法&#xff0c;并传入相应的参数&#xff0c;包括请求地址、请求方法、请求头部和请求数据等。 …...

用React给XXL-JOB开发一个新皮肤(一):环境搭建和项目初始化

目录 一. 简述二. Fork 项目三. 搭建开发环境四. 初始化皮肤项目五. 添加相关依赖六. 预览 一. 简述 大名鼎鼎的 xxl-job 任务调度中心我们应该都使用过&#xff0c;项目地址&#xff1a;xxl-job。它是一个分布式任务调度平台&#xff0c;其核心设计目标是开发迅速、学习简单…...

华为常用的命令——display,记得点赞收藏!

华为设备提供了多条display命令用于查看硬件部件、接口及软件的状态信息。通常这些状态信息可以为用户故障处理提供定位思路。 常用的故障信息搜集的命令如下&#xff1a; 路由器常用维护命令表 交换机常用的故障信息搜集 关注 工 仲 好&#xff1a;IT运维大本营&#xff0c;获…...

Costco攻入山姆大本营

01 Costco深圳店开业火爆 “我今天不去Costco&#xff0c;早上还没开业&#xff0c;路上就已经堵车了&#xff0c;看来今天人很多&#xff0c;过几天再去”&#xff0c;原本计划在Costco开业当天去逛逛的张芸&#xff08;化名&#xff09;无奈只能放弃。 家住在Costco深圳店旁…...

什么是常量?如何区分常量和变量?

一、问题 什么是常量&#xff0c;什么是变量&#xff1f;怎样区分⼆者&#xff1f; 二、解答 1. 常量与变量 &#xff08;1&#xff09;常量即其值在程序运⾏的过程中是不可以改变的&#xff0c;如123&#xff0c;-4567 为数值常量&#xff1b; &#xff08;2&#xff09;变量…...