protobuf介绍及使用(c++)

1.Protocol Buffers(简称 Protobuf)

是一种语言无关、平台无关的可扩展的序列化结构数据格式,由 Google 开发并开源。它主要用于高效地存储和传输结构化数据,广泛应用于网络通信、数据存储、微服务架构等多个领域。

1. 核心概念

  • 序列化:将数据结构或对象状态转换为可存储或可传输的格式的过程。Protobuf 将数据序列化为二进制格式,这种格式紧凑且高效,适合在网络中传输或存储到磁盘上。

  • 反序列化:将序列化后的数据重新转换为原始数据结构或对象的过程。Protobuf 提供了高效的反序列化机制,能够快速恢复数据的原始状态。

  • 结构化数据:Protobuf 用于处理结构化数据,例如对象、记录、消息等。它通过定义数据的结构(称为 .proto 文件),来指定数据的字段、类型和关系。

2. 主要特点

  • 高效性:Protobuf 的二进制格式比 JSON、XML 等文本格式更加紧凑,占用空间更小,解析速度更快。例如,对于相同的数据,Protobuf 的序列化结果通常只有 JSON 的三分之一大小。

  • 可扩展性:Protobuf 的数据结构定义支持字段的添加、删除和修改,而不会破坏已有的数据格式。这使得它非常适合在快速迭代的系统中使用,例如微服务架构中的接口定义。

  • 语言无关性:Protobuf 提供了多种语言的库支持,包括 C++、Java、Python、Go、C# 等。开发者可以在不同语言的系统之间无缝交换数据。

  • 强类型:Protobuf 在定义数据结构时需要明确指定字段的类型(如整数、浮点数、字符串等),这使得数据的结构更加清晰,减少了因类型不匹配而导致的错误。

3. 工作原理

  • 定义数据结构:开发者通过 .proto 文件定义数据的结构。例如:

    syntax = "proto3";
    
    message Person {
      string name = 1;
      int32 id = 2;
      repeated string email = 3;
    }

    在这个例子中,Person 是一个消息类型,包含三个字段:name(字符串类型)、id(整数类型)和 email(字符串列表)。

  • 生成代码:使用 Protobuf 编译器(protoc)根据 .proto 文件生成目标语言的代码。例如,对于上述 .proto 文件,protoc 可以生成 Java、Python 或 C++ 的代码。

  • 序列化与反序列化:在应用程序中,使用生成的代码将数据对象序列化为二进制格式,或者将二进制格式的数据反序列化为对象。

4. 应用场景

  • 网络通信:在客户端和服务器之间传输数据时,Protobuf 可以显著提高通信效率。例如,Google 的许多内部服务都使用 Protobuf 进行 RPC(远程过程调用)通信。

  • 数据存储:将结构化数据序列化后存储到文件或数据库中,节省存储空间并提高读写效率。

  • 微服务架构:在微服务之间传递数据时,Protobuf 提供了高效且灵活的数据交换格式,支持服务的快速迭代和扩展。

2. 序列化反序列化组件

以下是对 IDL(接口描述语言)IDL编译器Client/ServerStub/Skeleton库以及它们与序列化、反序列化的关系的详细解释:

1. IDL(接口描述语言)

IDL(Interface Definition Language,接口定义语言)是一种用于定义软件组件接口的通用语言。它允许开发者以一种独立于具体编程语言的方式描述接口、数据类型、方法和属性。这些定义可以被转换成多种编程语言的代码,从而实现不同编程语言之间的无缝通信

2. IDL编译器

IDL编译器的作用是将IDL文件转换为特定编程语言的代码。这些代码通常包括:

  • Stub(客户端存根):客户端使用的代码,负责将方法调用和参数序列化后发送到服务端,并接收服务端返回的序列化结果进行反序列化。

  • Skeleton(服务端骨架):服务端使用的代码,负责接收客户端发送的序列化请求,反序列化后调用实际的服务逻辑,并将结果序列化后返回给客户端。

  • 协议库(Protocol Library):定义了接口的公共部分,供客户端和服务端共享。

3. protobuf使用

1. 编写 .proto 文件

首先,需要定义一个 .proto 文件,描述你的数据结构。例如:

syntax = "proto3";

message Person {
  int32 id = 1;
  string name = 2;
  repeated string phone = 3;
}

这个文件定义了一个 Person 消息,包含一个整数 id、一个字符串 name 和一个字符串数组 phone

2. 使用 protoc 生成 C++ 代码

运行以下命令,将 .proto 文件编译为 C++ 代码:

$ protoc  .proto文件路径 --cpp_out=输出路径(存储生成的c++文件)

这会生成两个文件:your_file.pb.hyour_file.pb.cc

3. 在 C++ 代码中使用 Protobuf

1.序列化反序列化
bool SerializeToString(std::string* output) const;
bool ParseFromString(const std::string& data);
    bool SerializeToArray(void* data, int size) const;

    MyMessage message;
    // 设置消息的字段
    char buffer[1024];
    if (message.SerializeToArray(buffer, sizeof(buffer))) {
      // 序列化成功,buffer包含二进制数据
    }
    bool ParseFromArray(const void* data, int size);

    bool SerializeToOstream(std::ostream* output) const;

    MyMessage message;
    // 设置消息的字段
    std::ofstream output("message.bin", std::ios::binary);
    if (message.SerializeToOstream(&output)) {
      // 序列化成功,数据已写入文件
    }
    bool ParseFromIstream(std::istream* input);

    MyMessage message;
    std::ifstream input("message.bin", std::ios::binary);
    if (message.ParseFromIstream(&input)) {
      // 解析成功,message包含解析后的数据
    }
    序列化

    在 C++ 代码中,包含生成的头文件,并使用 Protobuf 提供的 API 进行序列化。例如:

    #include "your_file.pb.h"
    #include <fstream>
    #include <iostream>
    
    int main() {
        // 创建一个 Person 对象并设置字段
        Person person;
        person.set_id(123);
        person.set_name("Alice");
        person.add_phone("1234567890");
    
        // 序列化到字符串
        std::string serialized_data;
        if (!person.SerializeToString(&serialized_data)) {
            std::cerr << "Failed to serialize data." << std::endl;
            return 1;
        }
    
        // 或者序列化到文件
        std::ofstream output("person_data.bin", std::ios::binary);
        if (!person.SerializeToOstream(&output)) {
            std::cerr << "Failed to serialize data to file." << std::endl;
            return 1;
        }
    
        return 0;
    }
    反序列化

    反序列化是将序列化后的数据还原为 Protobuf 对象。例如:

    #include "your_file.pb.h"
    #include <fstream>
    #include <iostream>
    
    int main() {
        // 从字符串反序列化
        Person person;
        std::string serialized_data = ...; // 获取序列化后的数据
        if (!person.ParseFromString(serialized_data)) {
            std::cerr << "Failed to parse data." << std::endl;
            return 1;
        }
    
        // 或者从文件反序列化
        std::ifstream input("person_data.bin", std::ios::binary);
        if (!person.ParseFromIstream(&input)) {
            std::cerr << "Failed to parse data from file." << std::endl;
            return 1;
        }
    
        // 使用反序列化后的对象
        std::cout << "ID: " << person.id() << std::endl;
        std::cout << "Name: " << person.name() << std::endl;
        for (const auto& phone : person.phone()) {
            std::cout << "Phone: " << phone << std::endl;
        }
    
        return 0;
    }
    2.操作消息字段 
    1. 设置字段值

    ProtoBuf 提供了 set_<field_name> 方法,用于设置字段的值。

    MyMessage message;
    message.set_name("Alice");  // 设置字符串字段
    message.set_age(30);        // 设置整数字段
    message.set_height(1.75);   // 设置浮点字段
    2. 获取字段值

    ProtoBuf 提供了 <field_name> 方法,用于获取字段的值。

    std::string name = message.name();  // 获取字符串字段
    int age = message.age();            // 获取整数字段
    double height = message.height();   // 获取浮点字段
    3. 检查字段是否已设置

    ProtoBuf 提供了 has_<field_name> 方法,用于检查字段是否已设置。

    if (message.has_name()) {
      std::cout << "Name is set: " << message.name() << std::endl;
    }
    4. 清除字段值

    ProtoBuf 提供了 clear_<field_name> 方法,用于清除字段的值。

    message.clear_name();  // 清除字符串字段
    message.clear_age();   // 清除整数字段
    message.clear_height(); // 清除浮点字段
    5. 操作重复字段

    ProtoBuf 提供了多种方法来操作重复字段(数组)。

    int32_t* value_ptr = message.add_values(); // 获取指向新添加元素的指针
    *value_ptr = 10; // 设置新添加元素的值
    int size = message.values_size();  // 获取重复字段的大小
    int first_value = message.values(0);  // 获取第一个元素
    message.set_values(0, 100);  // 设置第一个元素的值
    message.clear_values();  // 清除所有元素
    6. 操作嵌套消息

    ProtoBuf 提供了 mutable_<field_name> 方法通常用于访问并修改一个字段,这个字段的类型是嵌套类型或引用类型(例如一个 message 类型)。mutable_<field_name> 会返回该字段的指针,允许你直接修改其内容。

    void ModifyLoadAvg1(MonitorInfo* monitor_info) {
        // 获取 CpuLoad 字段的可修改引用
        CpuLoad* cpu_load = monitor_info->mutable_cpu_load();
    
        // 使用 set_ 方法修改 load_avg_1
        cpu_load->set_load_avg_1(1.23f);
    }

    4.CMake项目中集成Protobuf和gRPC

    步骤 1: 查找所需的包

    首先,使用 find_package 命令来查找所需的库。这里查找了protobuf和gRPC库,以及可选的c-ares库和线程库(如果需要)。

    find_package(protobuf CONFIG REQUIRED)
    find_package(gRPC CONFIG REQUIRED)

    步骤 2: 定义 .proto 文件列表

    定义一个变量 PROTO_FILES,列出所有需要编译的 .proto 文件。

    set(PROTO_FILES
        monitor_info.proto
        cpu_load.proto
        cpu_softirq.proto
        cpu_stat.proto
        mem_info.proto
        net_info.proto
    )

    步骤 3: 添加库目标

    添加一个库目标 monitor_proto,并将 .proto 文件作为源文件添加到该目标。

    add_library(monitor_proto ${PROTO_FILES})

    步骤 4: 链接库

    为目标 monitor_proto 链接所需的库,包括protobuf和gRPC的库。

    target_link_libraries(monitor_proto
        PUBLIC
            protobuf::libprotobuf
            gRPC::grpc
            gRPC::grpc++
    )

    步骤 5: 设置包含目录

    为目标 monitor_proto 设置包含目录,以便编译器可以找到protobuf和gRPC的头文件,以及生成的头文件。

    target_include_directories(monitor_proto PUBLIC
        ${PROTOBUF_INCLUDE_DIRS} 
        ${CMAKE_CURRENT_BINARY_DIR})

    步骤 6: 生成 protobuf 和 gRPC 代码

    protobuf_generate 是一个在 CMake 脚本中使用的命令,它用于从 .proto 文件生成 C++ 源代码和头文件。这个命令是 protobuf_generate_cppprotobuf_generate_grpc 命令的封装,它根据 .proto 文件的内容自动决定是仅生成 protobuf 代码还是同时生成 gRPC 代码。

    protobuf_generate(
        [TARGET target_name]
        [LANGUAGE <language>]
        [PROTO_SRCS variable]
        [PROTO_HDRS variable]
        [GENERATE_EXTENSIONS extension1 [extension2...]]
        [DESCRIPTOR_SET output_file]
        [PLUGIN plugin]
        [input.proto files...]
    )
    • TARGET target_name:指定一个目标名称,用于接收生成的源文件和头文件。这是可选的,如果不指定,你需要手动处理生成的文件。

    • LANGUAGE <language>:指定要生成代码的语言。对于 C++,通常使用 cpp

    • PROTO_SRCS variable:指定一个变量,用于存储生成的源文件路径。

    • PROTO_HDRS variable:指定一个变量,用于存储生成的头文件路径。

    • GENERATE_EXTENSIONS extension1 [extension2...]:指定生成文件的扩展名。默认情况下,会生成 .pb.h.pb.cc 文件。

    • DESCRIPTOR_SET output_file:指定一个输出文件,用于保存描述符集。

    • PLUGIN plugin:指定一个插件,用于处理特定的生成任务,例如 gRPC。

    • input.proto files...:指定一个或多个 .proto 文件,作为代码生成的输入

    get_target_property 是 CMake 中的一个命令,它用于查询指定目标(target)的属性(property),并将查询结果存储在变量中。目标可以是库(library)、可执行文件(executable)或自定义目标(custom target)等。属性可以是目标的任何元数据,比如位置、源文件、链接库等。

    • get_target_property(<variable>
                         <target>
                         <property>
                         [发电机表达式])
    • <variable>:存储查询结果的变量名称。

    • <target>:要查询的目标名称。

    • <property>:要查询的目标属性名称。

    • [发电机表达式]:(可选)当指定此参数时,属性值将在构建系统生成时计算。

    使用 protobuf_generate 命令为目标 monitor_proto 生成C++源文件和头文件。这包括protobuf的C++代码和gRPC的C++存根代码。

    get_target_property(grpc_cpp_plugin_location gRPC::grpc_cpp_plugin LOCATION)
    protobuf_generate(TARGET monitor_proto LANGUAGE cpp)
    protobuf_generate(TARGET monitor_proto LANGUAGE grpc GENERATE_EXTENSIONS .grpc.pb.h .grpc.pb.cc PLUGIN "protoc-gen-grpc=${grpc_cpp_plugin_location}")

    这里,get_target_property 用于获取gRPC C++插件的位置,然后 protobuf_generate 用于生成C++代码。LANGUAGE cppLANGUAGE grpc 指定了要生成的代码类型,GENERATE_EXTENSIONS 指定了生成文件的扩展名,PLUGIN 指定了用于生成gRPC代码的插件。

    5.在项目中protobuf具体使用

    1.proto文件的导入

    将相关的消息类型定义分散到多个文件中,从而提高代码的可维护性和可读性

    import "要使用的proto文件的名字";

    2.protobuf 包的使用

    package monitor.proto是 Protobuf 文件中定义的一个包名(package)声明,它用于指定当前 Protobuf 文件所属的命名空间。以下是对它的详细解释:

    • C++ 中,package monitor.proto; 会被映射为命名空间 monitor::proto

     3.repeated

    是一个限定修饰符,用于表示某个字段可以包含多个值,类似于数组。repeated 字段可以存储零个或多个相同类型的元素,在 C++ 中,repeated 字段会被映射为 std::vector

    syntax = "proto3";
    package monitor.proto;
    
    message ServerInfo {
      string ip = 1;
      int32 port = 2;
    }
    syntax = "proto3";
    package monitor.proto;
    
    import "ServerInfo.proto"
    
    message MonitorRequest {
      repeated ServerInfo servers = 1; // repeated 字段
      string query = 2;
    }
    #include "monitor.pb.h"
    #include <iostream>
    
    int main() {
        // 创建 MonitorRequest 消息
        monitor::proto::MonitorRequest request;
    
        // 添加服务器信息
        monitor::proto::ServerInfo* server1 = request.add_servers();
        server1->set_ip("192.168.1.1");
        server1->set_port(8080);
    
        monitor::proto::ServerInfo* server2 = request.add_servers();
        server2->set_ip("192.168.1.2");
        server2->set_port(8081);
    
        // 遍历 repeated 字段
        for (int i = 0; i < request.servers_size(); ++i) {
            const monitor::proto::ServerInfo& server = request.servers(i);
            std::cout << "Server IP: " << server.ip() << ", Port: " << server.port() << std::endl;
        }
        
        // 清空 repeated 字段
        request.clear_servers();
    
        return 0;
    }
    React Hooks 是 React 16.8 中新增的特性,它可以让你在函数组件中使用 state、生命周期钩子等 React 特性。使用 Hooks 可以让你写出更简洁、可复用且易于测试的代码。 React Hooks 提供了一系列的 Hook 函数,包括 useState、useEffect、useContext、useReducer、useCallback、useMemo、useRef、useImperativeHandle、useLayoutEffect 和 useDebugValue。每个 Hook 都有特定的用途,可以帮助你处理不同的问题。 下面是 React Hooks 的一些常用 Hook 函数: 1. useState useState 是最常用的 Hook 之一,它可以让你在函数组件中使用 state。useState 接受一个初始状态值,并返回一个数组,数组的第一个值是当前 state 值,第二个值是更新 state 值的函数。 ``` const [count, setCount] = useState(0); ``` 2. useEffect useEffect 可以让你在组件渲染后执行一些副作用操作,比如订阅事件、异步请求数据等。useEffect 接受两个参数,第一个参数是一个回调函数,第二个参数是一个数组,用于控制 useEffect 的执行时机。 ``` useEffect(() => { // 这里可以执行副作用操作 }, [dependencies]); ``` 3. useContext useContext 可以让你在组件树中获取 context 的值。它接受一个 context 对象,并返回该 context 的当前值。 ``` const value = useContext(MyContext); ``` 4. useRef useRef 可以让你在组件之间共享一个可变的引用。它返回一个对象,该对象的 current 属性可以存储任何值,并在组件的生命周期中保持不变。 ``` const ref = useRef(initialValue); ref.current = value; ``` 5. useCallback useCallback 可以让你缓存一个函数,以避免在每次渲染时都创建一个新的函数实例。它接受一个回调函数和一个依赖数组,并返回一个 memoized 的回调函数。 ``` const memoizedCallback = useCallback(() => { // 这里是回调函数的逻辑 }, [dependencies]); ``` 6. useMemo useMemo 可以让你缓存一个计算结果,以避免在每次渲染时都重新计算。它接受一个计算函数和一个依赖数组,并返回一个 memoized 的计算结果。 ``` const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]); ``` 以上就是 React Hooks 的一些常用 Hook 函数,它们可以帮助你更好地处理组件状态、副作用、上下文和性能优化等问题。
    评论
    添加红包

    请填写红包祝福语或标题

    红包个数最小为10个

    红包金额最低5元

    当前余额3.43前往充值 >
    需支付:10.00
    成就一亿技术人!
    领取后你会自动成为博主和红包主的粉丝 规则
    hope_wisdom
    发出的红包
    实付
    使用余额支付
    点击重新获取
    扫码支付
    钱包余额 0

    抵扣说明:

    1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
    2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

    余额充值