protobuf —— 快速上手

protobuf —— 快速上手

  • 创建 .proto 文件
    • 添加注释
    • 指定proto3语法
    • package 声明符
    • 定义消息(message)
  • 定义消息字段
      • 字段定义基本格式
      • 字段名称命名规范
      • 字段类型
      • 字段唯一编号
      • 示例
    • 转换关系
      • 示例:增加姓名和年龄字段
    • 字段唯一编号
      • 字段编号范围
      • 编码效率
      • 实践指导
  • 编译 contacts.proto 文件,生成 C++ 文件
    • 编译命令
  • 编译 contacts.proto 文件后会生成什么
      • 序列化注意事项
  • 序列化和反序列化

我们今天来快速上手protobuf的用法,我们会用一个简单的通讯录来进行讲解:

如果还没有装好protobuf的小伙伴可以点击这里:

https://blog.csdn.net/qq_67693066/article/details/139215057

如果大家使用Linux的话,并且对版本没有太多要求的,可以直接用Linux的软件包来安装,Centos和Ubuntu都可以,我这里以Centos为例:
在这里插入图片描述
Ubuntu就换成apt,安装的版本是14的,也是21年发布的版本,也不错。

创建 .proto 文件

装好protobuf之后,我们用vscode连接,大家可以下载一个支持protobuf语法的插件,我下的是这一个:
在这里插入图片描述

然后创建一个后缀为.proto的文件:
在这里插入图片描述
创建 .proto 文件时,文件命名应该使用全小写字母命名,多个字母之间用 _ 连接。 例如:lower_snake_case.proto

添加注释

向文件添加注释,可使使用 // 或者 /* ... */
在这里插入图片描述

指定proto3语法

Protocol Buffers 语言版本3,简称 proto3,是 .proto 文件的最新语法版本。Proto3 简化了 Protocol Buffers 语言,既易于使用,又能在更多编程语言中运用自如。它支持使用 Java、C++、Python 等多种语言生成 Protocol Buffer 代码。

.proto 文件里,需使用 syntax = "proto3"; 来指定文件遵循 proto3 语法,这条声明必须位于除去注释内容的首行。如果未作指定,编译器将默认采用 proto2 语法。
例如,在通讯录 1.0 的 contacts.proto 文件中,指定使用 proto3 语法的方式如下所示:
在这里插入图片描述

package 声明符

在 Protocol Buffers(protobuf)的 .proto 文件中,package 是一个可选但强烈推荐的声明语句。它扮演着命名空间的角色,用于组织和分隔不同的消息类型定义,确保即便在导入多个 .proto 文件到同一个项目时,消息类型之间也不会发生名称冲突。简而言之,通过指定独一无二的包名,可以有效避免定义的消息类型重名问题。

以“通讯录 1.0”项目的文件为例,为了明确该文件中消息类型的所属命名空间,可以在文件开头添加如下的 package 声明:
在这里插入图片描述

定义消息(message)

在分布式系统和网络通信中,消息(message)扮演着至关重要的角色,它是定义数据交换格式的基础。为何需要定义消息? 主要原因有二:

  1. 协议定制:网络通信依赖于双方遵守的协议,这些协议定义了数据如何封装和解析。通过在.proto文件中定义消息,您可以精确地描述数据的结构,包括字段类型、名称及其顺序。这样,Protocol Buffers工具就能自动生成对应语言的源代码,实现序列化(将数据结构转换为字节流以便网络传输)和反序列化(将接收到的字节流还原为数据结构)功能。比如TCP/IP协议中的报文头和数据部分,就是一个典型的结构化数据示例。
  1. 数据持久化:当数据需要被存储到诸如数据库这类持久化存储介质时,清晰、统一的数据结构变得尤为重要。消息定义不仅帮助组织数据,还能确保数据的一致性和高效存储。通过将数据封装在消息中,可以更容易地映射到数据库的表结构或文档模型中,简化数据的存取逻辑。

以“通讯录1.0”为例,为“联系人”创建一个消息定义,就是预先规划好联系人信息的结构,比如姓名、电话号码、电子邮箱等,使得在实际编码时,可以直接利用Protocol Buffers生成的类来操作这些数据,无需手动处理序列化和解析的细节。这样,无论是网络间的数据交换,还是数据库的存储读取,都能保持高效和一致。
.proto 文件中定义⼀个消息类型的格式为:

message 消息类型名{
}

在这里插入图片描述

简单来说,message有点像C++中的class进行属性的封装。

定义消息字段

字段定义基本格式

字段定义遵循以下基本格式:

字段类型 字段名 = 字段唯一编号;

字段名称命名规范

  • 全小写字母:字段名应全部使用小写字母,以保持一致性并符合protobuf的命名约定。
  • 使用下划线_连接:如果字段名由多个单词组成,单词间应使用下划线分隔,提高可读性。例如,first_name而非firstName

字段类型

字段类型分为标量数据类型和特殊类型两大类:

  • 标量数据类型包括但不限于:

    • int32, int64: 整型
    • float, double: 浮点数
    • bool: 布尔值
    • string: 字符串
    • bytes: 二进制数据
  • 特殊类型

    • 枚举(enum): 自定义的枚举类型,用于限定某个字段的取值范围。
    • 其他消息类型: 引用其他已定义的消息类型作为字段类型,实现复杂数据结构的嵌套。

字段唯一编号

  • 编号要求:每个字段都应分配一个唯一的整数编号,用于在序列化和反序列化过程中识别字段。
  • 编号范围:通常情况下,编号1到15的字段在编码时较为节省空间,因为它们可以用一个字节编码(如果字段没有被省略)。16及以上的编号需要更多的字节来编码。
  • 不可变更性:一旦消息发布并被使用,字段编号就不应该被修改或重新分配,因为这将破坏与旧版本的兼容性。新增字段应分配新的编号,而避免更改现有字段编号。

示例

下面是一个简单的Person消息定义示例:

syntax = "proto3";

message Person {
  int32 id = 1;          // 唯一ID
  string first_name = 2;  // 名
  string last_name = 3;   // 姓
  int32 age = 4;          // 年龄
  bool is_student = 5;    // 是否为学生
}

在这个例子中,Person消息包含了几个基本的标量数据类型的字段,并且每个字段都分配了一个唯一的编号。

转换关系

这个表格更清晰地展示.proto文件中定义的消息字段类型及其与C++语言类型的对应关系:

.proto TypeC++ TypeNotes
doubledouble-
floatfloat-
int32int32_t使用变长编码[1]。负数编码效率较低,如果字段可能为负值,建议使用sint32
int64int64_t使用变长编码[1]。负数编码效率较低,如果字段可能为负值,建议使用sint64
uint32uint32_t使用变长编码[1]
uint64uint64_t使用变长编码[1]
sint32int32_t使用变长编码[1]。符号整型,负值编码效率高于常规int32
sint64int64_t使用变长编码[1]。符号整型,负值编码效率高于常规int64
fixed32uint32_t定长4字节。如果值通常大于2^28,则比uint32更高效。
fixed64uint64_t定长8字节。如果值通常大于2^56,则比uint64更高效。
sfixed32int32_t定长4字节。
sfixed64int64_t定长8字节。
boolbool-
stringstd::string包含UTF-8和ASCII编码的字符串,长度不超过2^32。
bytesstd::string可包含任意字节序列,长度不超过2^32。

[1] 变长编码指的是Protocol Buffers使用一种可变长度的编码方式(如VarInt编码),这种编码方式对于较小数值占用空间较少,但大数值会占用更多字节。对于fixed32fixed64sfixed32sfixed64,它们使用固定长度编码,不论数值大小,始终占用指定的字节数。

示例:增加姓名和年龄字段

假设我们要在一个消息中增加name(字符串类型)和age(假设为非负整数,使用uint32类型)字段,那么在.proto文件中的定义可能是这样的:

message Person {
  string name = 1;  // 姓名
  uint32 age = 2;   // 年龄
}

这里name字段使用了字符串类型(对应C++中的std::string),age字段使用了无符号32位整型(对应C++中的uint32_t),并为每个字段分配了唯一的编号。

字段唯一编号

这里还要说明一下字段唯一编号:

在Protocol Buffers(protobuf)中,字段唯一编号的选取是一个关键设计决策,因为它直接影响到消息的序列化效率和兼容性。下面是关于字段编号范围及编码的一些关键点:

字段编号范围

字段编号的有效范围是从1到536,870,911(即2^29 - 1)。但是,需要注意的是,19000至19999这一区间内的编号是被protobuf协议实现预留的,不允许用户直接使用。尝试使用这些预留编号会导致编译时错误,提醒开发者这些编号不可用,因为它们在protobuf内部有特殊的用途。

编码效率

  • 编号与编码大小:为了优化序列化后的消息体积,protobuf采用了变长编码。其中,编号小于等于15的字段仅需1个字节来编码编号和类型信息;编号在16至2047之间的字段需要2个字节;更大的编号则需要更多的字节。这意味着,编号较小的字段在序列化时更为高效。
  • 优化建议:鉴于此,建议将频繁出现且长度较短的字段分配编号在1至15之间,以减少消息的整体大小。同时,应保留一部分低编号以备未来可能添加的常用字段。

实践指导

  • 避免预留区间:确保在定义.proto文件时,字段编号避开19000至19999这个预留区间。
  • 编号策略:合理规划字段编号,考虑当前及未来的扩展性。频繁使用的字段应给予更低的编号以优化效率,同时留出一定编号空间供后续扩展使用。

通过遵循上述原则,可以确保你的protobuf消息定义既高效又具有良好的向前兼容性,便于维护和升级。

编译 contacts.proto 文件,生成 C++ 文件

编译命令

protoc [--proto_path=IMPORT_PATH] --cpp_out=DST_DIR path/to/file.proto

protoc 是 Protocol Buffers(protobuf)的编译器,用于将 .proto 文件转换为目标编程语言的源代码文件。您给命令行参数示例中,各部分的含义如下:

  • --proto_path=IMPORT_PATH-I=IMPORT_PATH:
  • 作用:指定 .proto 文件的搜索路径。当你在 .proto 文件中使用 import 语句引入其他 .proto 文件时,protoc 会根据这个路径去查找这些被引用的文件。
  • 参数说明IMPORT_PATH 是一个或多个目录路径,可以多次使用该选项指定多个路径。如果不指定,默认会在当前目录下查找。
  • --cpp_out=DST_DIR
  • 作用:指定编译生成的C++源代码输出目录。当您希望将 .proto 文件编译为C++代码时,使用这个选项。
  • 参数说明DST_DIR 是您希望输出C++源代码的目录路径。protoc 会在这个目录下生成对应的 .pb.cc(源文件)和 .pb.h(头文件),这些文件包含了序列化和反序列化消息所需的函数和其他必要的代码。
  • path/to/file.proto
  • 作用:指定需要编译的 .proto 文件路径。
  • 说明:这是您要编译的protobuf定义文件的完整或相对路径。protoc 会读取这个文件并根据指定的输出选项(如 --cpp_out)生成目标语言的代码。

综上,整个命令的含义是:使用 protoc 编译器,从指定的导入路径中查找任何被导入的 .proto 文件,然后将 path/to/file.proto 文件编译为C++源代码,并将生成的文件输出到 DST_DIR 目录下。这样做的目的是为了让开发者能够方便地将protobuf消息类型集成到他们的C++项目中。

举个例子
当然,让我们通过一个具体的例子来进一步说明如何使用这些参数。假设你有一个项目结构如下:

/my_project/
|-- protos/
|   |-- message.proto
|-- src/
|-- CMakeLists.txt
|-- main.cpp

在这个场景中,你有一个名为 message.proto 的 Protocol Buffers 定义文件,位于 protos/ 目录下,你想将它编译成C++代码,并将生成的文件放在 src/ 目录中以便在你的C++项目中使用。

你可以打开终端(命令行界面),并导航到 /my_project/ 目录,然后执行以下命令:

protoc --proto_path=./protos --cpp_out=./src ./protos/message.proto

//或者
protoc -I=./protos --cpp_out=./src ./protos/message.proto

这里发生了什么:

  • --proto_path=./protos 指定了 .proto 文件的搜索路径为当前目录下的 protos/ 目录。如果有 message.protoimport 了其他 .proto 文件,编译器会在这个目录下查找它们。
  • --cpp_out=./src 指定编译生成的C++源代码放置在 src/ 目录中。
  • ./protos/message.proto 是你要编译的具体 .proto 文件路径。

执行完这个命令后, message.proto 定义了一个消息类型,protoc 编译器将在 src/ 目录下生成两个文件:message.pb.ccmessage.pb.h,这些就是你可以包含在C++项目中使用的源代码和头文件,用于处理该消息类型的序列化与反序列化等操作。

编译 contacts.proto 文件后会生成什么

编译 contacts.proto 文件后,会生成所选择语言的代码,我们选择的是C++,所以编译后生成了两个文件: contacts.pb.h contacts.pb.cc 。
在这里插入图片描述

对于编译生成的 C++ 代码,包含了以下内容 :

  • 对于每个 message ,都会生成⼀个对应的消息类。
  • 在消息类中,编译器为每个字段提供了获取和设置⽅法,以及⼀下其他能够操作字段的方法。
  • 编辑器会针对于每个 .proto 文件生成 .h 和 .cc 文件,分别用来存放类的声明与类的实现。

contacts.pb.h 部分代码展示:

 // string name = 1;
  void clear_name();
  const std::string& name() const;
  template <typename ArgT0 = const std::string&, typename... ArgT>
  void set_name(ArgT0&& arg0, ArgT... args);
  std::string* mutable_name();
  PROTOBUF_NODISCARD std::string* release_name();
  void set_allocated_name(std::string* name);
  private:
  const std::string& _internal_name() const;
  inline PROTOBUF_ALWAYS_INLINE void _internal_set_name(const std::string& value);
  std::string* _internal_mutable_name();
  public:

  // int32 age = 2;
  void clear_age();
  int32_t age() const;
  void set_age(int32_t value);
  private:
  int32_t _internal_age() const;
  void _internal_set_age(int32_t value);
  public:

上述的例子中:

  • 每个字段都有设置和获取的方法, getter 的名称与小写字段完全相同,setter 方法以 set_ 开头。
  • 每个字段都有⼀个 clear_ 方法,可以将字段重新设置回 empty 状态。
    contacts.pb.cc 中的代码就是对类声明方法的⼀些实现,在这里就不展开了。

序列化和反序列化方法在哪里呢?在消息类的父类MessageLite 中,提供了读写消息实例的方法,包括序列化方法和反序列化方法:

 bool MergeFromCodedStream(io::CodedInputStream* input);
  bool ParseFromCodedStream(io::CodedInputStream* input);
  bool ParseFromZeroCopyStream(io::ZeroCopyInputStream* input);
  bool ParseFromArray(const void* data, int size);
  inline bool ParseFromString(const std::string& data) {
    return ParseFromArray(data.data(), static_cast<int>(data.size()));
  }

  // Merges this message's unknown field data (if any).  This works whether
  // the message is a lite or full proto (for legacy reasons, lite and full
  // return different types for MessageType::unknown_fields()).
  template <typename MessageType>
  bool MergeFromMessage(const MessageType& message);

  // Serialization.
  bool SerializeToString(std::string* output) const;
  bool SerializeToCodedStream(io::CodedOutputStream* output) const;
  static const UnknownFieldSet& default_instance();

理解您的要求,这里是对于序列化概念及Protocol Buffers(protobuf)中消息序列化方法的解释,稍作调整以供参考:

序列化注意事项

  • 二进制输出:序列化过程将结构化数据转换成紧凑的二进制形式,而非易于阅读的文本格式。这有助于减小程序体积,提高网络传输效率和存储效率。
  • 多样化的序列化方法:Protocol Buffers提供了多种序列化API,包括但不限于SerializeToString()SerializeToOstream()SerializeToArray()等。这些方法虽然输出形式各异——字符串、输出流或字节数组,但核心目的相同:将消息对象转化为二进制数据,适应不同应用场景的需要。
  • 不变性保证:序列化操作通过const成员函数实现,意味着调用序列化函数不会修改消息对象本身的内在状态。数据的转换发生在序列化过程中,并将结果输出到指定的目标(如内存、字符串或流),而不影响原始对象。
  • 深入探索API:欲了解更多关于protobuf消息对象的序列化及其他功能,可查阅protobuf官方文档中的消息API完整列表,那里详尽地介绍了每种方法的使用方法和适用场景,帮助开发者高效利用protobuf的强大功能。

序列化和反序列化

创建⼀个测试文件 main.cc,方法中我们实现:

  • 对⼀个联系人的信息使用 PB 进行序列化,并将结果打印出来。
  • 对序列化后的内容使用 PB 进行反序列,解析出联系人信息并打印出来。
#include <iostream> 
#include "contacts.pb.h" // 引⼊编译⽣成的头⽂件
using namespace std; 
 
int main() 
{ 
    string people_str; 
    {
        // .proto⽂件声明的package,通过protoc编译后,会为编译⽣成的C++代码声明同名的
        // 其范围是在.proto ⽂件中定义的内容
        contacts::PeopleInfo people; 
        people.set_age(20); 
        people.set_name("张珊"); 
        // 调⽤序列化⽅法,将序列化后的⼆进制序列存⼊string中
        if (!people.SerializeToString(&people_str)) { 
        cout << "序列化联系⼈失败." << endl; 
        }
        // 打印序列化结果
        cout << "序列化后的 people_str: " << people_str << endl; 
    }
    
    {
        contacts::PeopleInfo people; 
        // 调⽤反序列化⽅法,读取string中存放的⼆进制序列,并反序列化出对象
        if (!people.ParseFromString(people_str)) { 
        cout << "反序列化出联系⼈失败." << endl; 
        } 
        // 打印结果
        cout << "Parse age: " << people.age() << endl; 
        cout << "Parse name: " << people.name() << endl; 
    }
}

代码书写完成后,编译 main.cc,生成可执行程序 TestProtoBuf :

g++ main.cc contacts.pb.cc -o TestProtoBuf -std=c++11 -lprotobuf

在这里插入图片描述

其中-lprotobuf必加,否则会有连接错误,-std=c++11可以为更高级。

执行TestProtoBuf ,可以看见 people 经过序列化和反序列化后的结果:
在这里插入图片描述

由于 ProtoBuf 是把联系⼈对象序列化成了⼆进制序列,这里用 string 来作为接收二进制序列的容器。所以在终端打印的时候会有换行等⼀些乱码显示。

所以相对于 xml 和 JSON 来说,因为被编码成⼆进制,破解成本增本,ProtoBuf 编码是相对安全的。

最后,总结一下:
在这里插入图片描述

  1. 创建 .proto 文件:通过定义message类型,我们详细规划了数据对象的结构和组成部分,包括字段名称、类型及其编码规则,为后续的通信协议奠定基础。
  2. 通过 protoc 工具将 .proto 文件转换为编程语言绑定代码protoc 编译器读取.proto文件并生成目标语言(如C++、Java、Python等)的接口和实现代码。这些代码被组织在头文件(.h)中声明接口,源文件(.cc或其他后缀)中实现细节,方便开发者在项目中直接调用。
  3. 整合生成的接口到项目中,实现数据操作和消息的编解码:将编译得到的头文件包含到项目源代码中,即可利用预生成的类和方法来实例化消息对象,设置和检索字段值。同时,利用内置的序列化方法(如SerializeToStringParseFromString等)轻松地在二进制格式与消息对象之间转换,支撑了数据在网络间的高效传输与存储需求。这样,开发者便能集中精力于业务逻辑,而不必关注底层的数据序列化和协议细节。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

短视频真人配音:成都科成博通文化传媒公司

短视频真人配音&#xff1a;情感传递的新维度 随着数字化媒体的飞速发展&#xff0c;短视频已经成为人们日常生活中不可或缺的一部分。而在这个视觉盛宴的时代&#xff0c;真人配音的加入为短视频注入了新的活力&#xff0c;不仅丰富了内容形式&#xff0c;更使得情感传递达到…

Oracle EBS API创建AP发票报错:ZX_TAX_STATUS_NOT_EFFECTIVE和ZX_REGIME_NOT_EFF_IN_SUBSCR-

背景 由创建国外业务实体财务未能提供具体国家地区会计税制&#xff0c;而是实施人员随便选择其它国外国家地区会计税制。导致客户化创建AP发票程序报错&#xff1a;UNEXPECTED TAX ERROR-导入时出现意外的税务错误ZX_TAX_STATUS_NOT_EFFECTIVE-ZX_REGIME_NOT_EFF_IN_SUBSCR-ZX…

基于双向长短期记忆BiLSTM对消费者投诉进行多类分类

前言 系列专栏:【深度学习:算法项目实战】✨︎ 涉及医疗健康、财经金融、商业零售、食品饮料、运动健身、交通运输、环境科学、社交媒体以及文本和图像处理等诸多领域,讨论了各种复杂的深度神经网络思想,如卷积神经网络、循环神经网络、生成对抗网络、门控循环单元、长短期记…

ssm150旅游网站的设计与实现+jsp

旅游网站设计与实现 摘 要 现代经济快节奏发展以及不断完善升级的信息化技术&#xff0c;让传统数据信息的管理升级为软件存储&#xff0c;归纳&#xff0c;集中处理数据信息的管理方式。本旅游网站就是在这样的大环境下诞生&#xff0c;其可以帮助管理者在短时间内处理完毕庞…

STM32学习和实践笔记(30):窗口看门狗(WWDG)实验

1.WWDG介绍 1.1 WWDG简介 上一章我们已经介绍了IWDG&#xff0c;知道它的工作原理就是一个12位递减计数器不断递减计数&#xff0c;当减到0之前还未进行喂狗的话&#xff0c;产生一个MCU复位。 窗口看门狗WWDG其实和独立看门狗类似&#xff0c;它是一个7位递减计数器不断的往…

学至乎没而后止也

开场白 学至后没而后止也这个题目的原话来自与荀子《劝学》。大家知道什么意思吗&#xff1f;学习要学到你人没了&#xff0c;才算停止了。通俗点说就是只要没学死就往死里学&#xff0c;高雅点说就是要保持终身学习。 在以前说终身学习好像是一种良好习惯或品德&#xff0c;…

Android NDK系列(一)手动搭建Native Project

使用NDK编写的本地代码具有高性能等特性&#xff0c;在游戏、图形处理等领域有广泛应用&#xff0c;下面介绍如何手动搭建一个纯C版的Android项目&#xff0c;通过该项目可以理解Android的项目结构。 一、创建settings.gradle Android项目是基于Gradle构建的&#xff0c;首先得…

【go项目01_学习记录15】

重构MVC 1 Article 模型1.1 首先创建 Article 模型文件1.2 接下来创建获取文章的方法1.3 新增 types.StringToUint64()函数1.4 修改控制器的调用1.5 重构 route 包1.6 通过 SetRoute 来传参对象变量1.7 新增方法&#xff1a;1.8 控制器将 Int64ToString 改为 Uint64ToString1.9…

ubuntu24.04LVM扩容问题

目录 一、 开机前设置&#xff1a;扩展 二、 开机后设置&#xff1a;分区管理 通过gparted管理分区有效做法。 一、 开机前设置&#xff1a;扩展 虚拟机关机。打开虚拟机设置。 挂起状态是不能扩容的 这里选择扩容到40G 二、 开机后设置&#xff1a;分区管理 使用gpar…

基于Matlab的车道线检测系统 (文末有代码获取链接)【含Matlab源码 MX_001期】

运行环境&#xff1a;Matlab2014b 部分代码&#xff1a; %% 视频流循环处理 % 创建一个循环过程来对给定视频进行车道线检测 % 该循环使用之前初始化的系统对象 warningTextColors {[1 0 0], [1 0 0], [0 0 0], [0 0 0]}; while ~isDone(hVideoSrc) RGB step(hVideoSrc);% …

Java入门基础学习笔记43——包

什么是包&#xff1f; 包是用来分门别类的管理各种不同程序的&#xff0c;类似文件夹&#xff0c;建包有利于程序的管理和维护。 建包的语法规则&#xff1a; package cn.ensource.javabean;public class Car() {} 在自己的程序中调用其他包下的程序的注意事项&#xff1a; 1…

Web3探索加密世界:空投常见类型有哪些?附操作教程

每种空投类型都有独特的特征和目的&#xff0c;我们需要了解不同类型的加密空投。本文给大家介绍的是流行的加密货币空投类型&#xff0c;以及一般空投是如何做的。感兴趣的话请看下去。 一、空投常见类型 1、持有者空投 持有者空投向钱包中持有一定数量数字货币的人免费发放…

探索Python的包与模块:构建项目的基石

新书上架~&#x1f447;全国包邮奥~ python实用小工具开发教程http://pythontoolsteach.com/3 欢迎关注我&#x1f446;&#xff0c;收藏下次不迷路┗|&#xff40;O′|┛ 嗷~~ 目录 一、模块与包的基础认知 1. 模块的定义与创建 2. 包的组织与管理 二、模块与包的进阶使用…

【测评】香橙派 AIpro上手初体验

AI毋庸置疑是近年来&#xff0c;热度最高的技术之一&#xff0c;作为一名工程师拥抱新技术的同时不可或缺的需要一块强悍的开发板&#xff0c;香橙派 AIpro除了拥有好看的皮囊之外&#xff0c;还拥有一个有趣且充满魅力的灵魂。作为一位长期活跃在嵌入式开发领域的工程师&#…

SQL刷题笔记day5

SQL218题目 我的错误代码&#xff1a; select de.dept_no,de.emp_no,s.salary from employees e join dept_emp de on de.emp_no e.emp_no join salaries s on s.emp_no e.emp_no where de.dept_no not in dept_manager.dept_no #not in 好像不能直接这样用 这里报错 正确代…

在树莓派3B+中下载opencv(遇到的各种问题及解决)

目录 前言 1、删除原版本下新版本 2、python虚拟环境 3、python版本共存换链接——给版本降低 4、烧录之前版本的文件&#xff08;在清华源中可以找&#xff0c;不用官网的烧录文件就行&#xff1b; 比如&#xff1a;&#xff08;balenaEtcher&#xff09;重新烧录有问题…

面试二十六、c++语言级别的多线程编程

一、 多线程编程 ​​​​​ 这里的c语言级别的多线程和linux的有一定的区别&#xff0c;c语言级别提供的多线程比较严格&#xff0c;如果主线程结束了&#xff0c;但是子线程没有结束&#xff0c;进程就会异常终止&#xff0c;而linux不会&#xff0c;会继续执行。 二、模拟卖…

三十、openlayers官网示例解析Double click, Drag and Zoom——第二次点击鼠标拖拽缩放地图效果、取消地图双击放大事件

这篇展示了如何在地图上添加第二次按下鼠标移动鼠标实现拖拽缩放地图效果。 官网demo地址&#xff1a; Double click, Drag and Zoom 官网介绍文字的翻译如下&#xff1a; 示例比较简单&#xff0c;直接贴代码&#xff1a; const map new Map({//添加第二次点击拖拽缩放地图i…

es安装错误Exception in thread “main“ java.nio.file.NoSuchFileException解决方案

docker 启动es出现一下错误的解决方案 Exception in thread “main” java.nio.file.NoSuchFileException: /usr/share/elasticsearch/config/jvm.options Exception in thread "main" java.nio.file.NoSuchFileException: /usr/share/elasticsearch/config/jvm.op…

React@16.x(11)ref

目录 1&#xff0c;介绍1.1&#xff0c;得到的结果 2&#xff0c;参数类型2.1&#xff0c;字符串&#xff08;不再推荐&#xff09;2.2&#xff0c;对象2.3&#xff0c;函数函数调用时机 3&#xff0c;注意点 1&#xff0c;介绍 reference 引用。和 vue 中的 refs 类似&#x…