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

Protobuf序列化协议使用指南

简介

        在本篇博客中,将会介绍protobuf的理论及使用方法。该文章仅做分享使用及自我复习使用,使用的图片来自百度,无法找到作者,如若侵权请联系删除。

目录

简介

概述

1.protobuf是什么?

2.序列化/反序列是什么?

2.1 序列化与反序列化:定义与概念

2.2 序列化协议的特性

2.3 序列化的实现组件

2.4 常见序列化协议及选型建议

Protobuf:高效的序列化协议

典型应用场景与局限性

适用场景

局限性

Protobuf 的 IDL 文件示例

使用 Protocol Buffers 的完整流程

1. 定义 .proto 文件

2. 使用 protoc 编译 .proto 文件

命令示例

编译结果

3. 在项目中使用生成的代码

3.1 初始化消息对象并设置值

3.2 序列化消息

3.3 反序列化消息

4. 使用 CMake 集成

5. Protobuf 常用序列化与反序列化 API

protobuf消息格式

1. Protobuf 编码基础

1.1 Varints 编码规则

1.2 Varints 编码示例

2. 字段编号(Field Number)

3. 传输类型(Wire Type

4. Field 结构

5. Protobuf 编解码的关键点

示例解析

示例 1:简单整数编码

示例 2:嵌套消息编码

Protobuf 的负数编码

示例:


概述

1.protobuf是什么?

protobuf是 Google 的语言中立、平台中立、可扩展的序列化结构化数据机制

  • 类似于 XML,json,但更小、更快、更简单。

  • 只需定义一次数据的结构化方式,然后就可以使用特殊生成的源代码,使用各种语言轻松地在各种数据流中写入和读取结构化数据。

Protoc 自己的编译器,c++/python

2.序列化/反序列是什么?

        在了解protobuf,之前我们需要知道序列化和反序列是什么。

2.1 序列化与反序列化:定义与概念

        随着互联网的普及,机器间通信的需求迅速增长。为了实现设备间的信息交换,必须使用预先约定的通讯协议,而序列化和反序列化正是其中的重要组成部分。

        通讯协议通常采用分层模型,例如 TCP/IP 协议的四层模型和 OSI 的七层模型。根据 OSI 模型,序列化和反序列化位于展现层(Presentation Layer),其作用是:

  • 序列化:将应用层中的数据结构或对象(如 C++ 的类)转换为连续的二进制数据流。
  • 反序列化:将二进制数据流还原为应用层的数据结构或对象。

通常,序列化协议属于 TCP/IP 协议的应用层,而本文的讨论基于 OSI 七层模型。

2.2 序列化协议的特性

        每种序列化协议都有其独特的设计目标和使用场景,在技术选型时需要综合考虑以下几个维度:

  1. 通用性

    • 跨平台与跨语言支持:协议的技术通用性越强,使用范围就越广。
    • 流行度:协议的使用范围直接影响学习成本和支持的广度。
  2. 鲁棒性

    • 成熟度高的协议通常经历了全面的测试,能够提供稳定可靠的支持。
    • 支持多种语言或平台时,可能需要在功能与通用性之间作出权衡。
  3. 可读性与调试性

    • 像 JSON 和 XML 这种文本格式的序列化数据具有人类可读性,便于调试。
    • 二进制协议(如 Protobuf)虽然性能优越,但调试成本较高,尤其在跨团队或跨公司环境下。
  4. 性能

    • 序列化和反序列化过程的空间与时间开销会直接影响系统的性能。
    • 空间开销(如描述字段的冗余)可能导致存储和传输成本增加。
  5. 可扩展性与兼容性

    • 在业务需求快速变化的情况下,协议需要支持字段的动态扩展,避免对现有服务造成影响。
  6. 安全性

    • 在跨网络通信场景中,协议是否支持安全传输(如通过 HTTP/HTTPS)是一个关键考虑因素。

2.3 序列化的实现组件

一个典型的序列化协议实现包含以下几个核心组件:

  1. 接口描述语言(IDL)文件

    • IDL 是用于描述通讯双方数据结构及接口的语言,具有跨平台、跨语言的特点。
    • 协议通过 IDL 文件约定数据结构和接口。
  2. IDL 编译器

    • IDL 编译器将 IDL 文件转换为目标语言的代码或动态库,便于客户端和服务端使用。
    • 示例:Protobuf 的 protoc 工具和 ROS 的 genmsg 工具。
  3. Stub 和 Skeleton 库

    • Stub:位于客户端,负责序列化请求并将其发送至服务端,接收服务端的响应并反序列化。
    • Skeleton:位于服务端,负责接收客户端的序列化请求并反序列化,将结果发送回客户端。
  4. 应用层(Client/Server)

    • 应用层代码通过调用由 IDL 生成的类或结构体,与 Stub 和 Skeleton 进行交互。
  5. 底层协议栈

    • 序列化后的数据通过传输层、网络层等被发送至远端,实现设备间的通信。

2.4 常见序列化协议及选型建议

目前主流的序列化协议包括 Protobuf、JSON、XML、Thrift 等。以下是它们的特点及适用场景:

  • Protobuf:高性能的二进制协议,适用于高效数据传输的场景,但调试成本较高。
  • JSON:易读易用,适合需要频繁调试和高可读性的场景。
  • XML:虽然冗余较大,但在一些老旧系统中仍然常见。
  • Thrift:功能丰富,适用于多语言、多平台的大规模分布式系统。

技术选型时需要权衡性能、可读性和可扩展性,并结合具体场景和需求。

 由于本文主要介绍protobuf,所以其他序列化协议暂且不深入讨论。

Protobuf:高效的序列化协议

Protobuf 是一款性能卓越的序列化协议,具备许多优秀协议应有的典型特性:

  1. 标准化的 IDL 和编译器

    • Protobuf 提供了完善的接口描述语言(IDL)和对应的编译器,使其在开发中对工程师十分友好。
  2. 紧凑的序列化数据

    • 与 XML 相比,Protobuf 序列化后的数据量仅为 XML 的 1/3 至 1/10,极大减少了存储和传输开销。
  3. 超快的解析速度

    • 在解析效率上,Protobuf 的性能比 XML 快 20 到 100 倍,非常适合对响应时间有严格要求的场景。
  4. 简单易用的动态库

    • Protobuf 提供了易用的动态库,反序列化操作甚至只需一行代码即可完成。

作为一个纯粹的展示层协议,Protobuf 可以灵活地与各种传输层协议(如 HTTP 或 TCP)搭配使用。同时,其文档完善,对开发者非常友好。然而,作为由 Google 推出的协议,目前 Protobuf 的语言支持较为有限,仅覆盖 Java、C++ 和 Python 三种语言。

典型应用场景与局限性

适用场景
  1. 高性能 RPC 调用

    • Protobuf 凭借其低空间开销和高解析速度,非常适合公司内部对性能要求较高的远程过程调用(RPC)。
  2. 跨公司通信

    • 得益于其标准化的 IDL 文件和与传输层协议的解耦,Protobuf 非常适用于跨公司场景。通过与 HTTP 协议结合,还可以实现良好的防火墙穿透能力。
  3. 应用层数据持久化

    • Protobuf 序列化后的数据体积小,适合用于持久化存储应用层对象的场景。
局限性
  • 有限的语言支持
    当前仅支持 Java、C++ 和 Python,限制了其在多语言生态中的应用。

  • 传输协议调试的复杂性
    由于 Protobuf 并未绑定特定的传输层协议,在跨公司通信中,传输协议的调试可能会带来额外的麻烦。

Protobuf 的 IDL 文件示例

以下是一个简单的 Protobuf IDL 文件,它定义了 UserInfo 和嵌套的 Address 数据结构:

message Product {
    required string product_id = 1;      // 商品唯一标识
    required string product_name = 2;   // 商品名称
    optional string description = 3;    // 商品描述
    optional float price = 4;           // 商品价格
}

message Order {
    required string order_id = 1;            // 订单唯一标识
    required string customer_name = 2;       // 客户姓名
    repeated Product product_list = 3;       // 订单中的商品列表
    optional string order_date = 4;          // 订单日期
}

  

使用 Protocol Buffers 的完整流程

Protocol Buffers(简称 Protobuf)是一种高效的序列化协议,用于定义数据结构并在多种语言和平台间进行数据传输。以下是从定义到使用 Protobuf 的完整流程:

1. 定义 .proto 文件

Proto 文件定义了需要存储或传输的数据结构及其字段。以下是一个示例:

syntax = "proto3";
package shopping;

message CartItem {
    string product_id = 1;   // 商品唯一标识
    int32 quantity = 2;      // 商品数量
    float price = 3;         // 单价
}

message User {
    string user_id = 1;          // 用户唯一标识
    string user_name = 2;        // 用户姓名
    repeated CartItem cart = 3;  // 购物车中商品列表
}

proto的基本数据类型如下图所示

2. 使用 protoc 编译 .proto 文件

运行 Protocol Buffers 编译器 (protoc) 将 .proto 文件编译为目标语言代码(如 C++ 或 Java)。

命令示例
protoc -I. --cpp_out=./generated user.proto
编译结果
  • 对于 C++,会生成以下文件:
    • test.pb.h:包含消息类型的头文件。
    • test.pb.cc:实现消息类型的方法。

3. 在项目中使用生成的代码

3.1 初始化消息对象并设置值

使用生成的类来操作定义好的消息类型:

#include "user.pb.h"
#include <iostream>
#include <string>

int main() {
    // 创建用户对象
    shopping::User user;
    user.set_user_id("u12345");
    user.set_user_name("Alice");

    // 添加购物车中的商品
    auto* item1 = user.add_cart();
    item1->set_product_id("p67890");
    item1->set_quantity(2);
    item1->set_price(15.99);

    auto* item2 = user.add_cart();
    item2->set_product_id("p12345");
    item2->set_quantity(1);
    item2->set_price(42.50);

    // 输出用户信息
    std::cout << "User ID: " << user.user_id() << std::endl;
    std::cout << "User Name: " << user.user_name() << std::endl;

    for (int i = 0; i < user.cart_size(); i++) {
        const auto& item = user.cart(i);
        std::cout << "Product ID: " << item.product_id()
                  << ", Quantity: " << item.quantity()
                  << ", Price: " << item.price() << std::endl;
    }

    return 0;
}
3.2 序列化消息

将消息对象序列化为二进制数据,以便存储或传输:

std::string serialized_data;
if (user.SerializeToString(&serialized_data)) {
    std::cout << "Serialization successful!" << std::endl;
} else {
    std::cerr << "Serialization failed!" << std::endl;
}

3.3 反序列化消息

从二进制数据解析消息对象:

shopping::User deserialized_user;
if (deserialized_user.ParseFromString(serialized_data)) {
    std::cout << "Deserialization successful!" << std::endl;

    // 输出反序列化后的数据
    std::cout << "User ID: " << deserialized_user.user_id() << std::endl;
    std::cout << "User Name: " << deserialized_user.user_name() << std::endl;

    for (int i = 0; i < deserialized_user.cart_size(); i++) {
        const auto& item = deserialized_user.cart(i);
        std::cout << "Product ID: " << item.product_id()
                  << ", Quantity: " << item.quantity()
                  << ", Price: " << item.price() << std::endl;
    }
} else {
    std::cerr << "Deserialization failed!" << std::endl;
}

4. 使用 CMake 集成

在 CMake 项目中,可以使用 protobuf_generate 指令生成代码。以下是一个示例配置:

find_package(Protobuf REQUIRED)

set(PROTO_FILES user.proto)

protobuf_generate_cpp(PROTO_SRCS PROTO_HDRS ${PROTO_FILES})

add_executable(shopping_app main.cpp ${PROTO_SRCS} ${PROTO_HDRS})
target_link_libraries(shopping_app PRIVATE protobuf::libprotobuf)
  • PROTO_FILES:定义需要编译的 .proto 文件列表。
  • protobuf_generate_cpp:生成 .pb.h.pb.cc 文件。
  • target_link_libraries:链接 Protobuf 的动态库。

5. Protobuf 常用序列化与反序列化 API

方法功能说明
SerializeToString(std::string*)将消息序列化为字符串(二进制内容)。
ParseFromString(const std::string&)从字符串反序列化为消息对象。
SerializeToArray(void*, int)将消息序列化到数组中。
ParseFromArray(const void*, int)从数组解析消息对象。
SerializeToOstream(std::ostream*)将消息写入输出流(如文件)。
ParseFromIstream(std::istream*)从输入流解析消息对象。

protobuf消息格式

1. Protobuf 编码基础

1.1 Varints 编码规则

        Varints 是一种高效的整数序列化方法,通过使用一个或多个字节表示整数,数值越小,占用的字节数越少。

编码规则:

  • 每个字节的最高位(msb)是标志位
    • 若为 1,表示后续还有字节。
    • 若为 0,表示这是最后一个字节。
  • 每个字节的低 7 位用于存储数值。
  • 使用小端字节序(低位字节在前)。

1.2 Varints 编码示例

以下是几个具体示例,帮助理解 Varints 编码过程:

  1. 数值 1 的编码

    • 二进制表示:0000 0001
    • 最高位为 0,表示只有一个字节。
    • 编码结果:0000 0001(十六进制为 0x01
  2. 数值 255 的编码

    • 二进制表示:1111 1111(需要两个字节)
    • 第一个字节:1111 1111(最高位为 1,表示有后续字节)
    • 第二个字节:0000 0001(最高位为 0,表示结束)
    • 编码结果:1111 1111 0000 0001(十六进制为 0xFF 0x01
  3. 数值 200 的编码

    • 二进制表示:1100 1000(需要两个字节)
    • 第一个字节:1100 1000(最高位为 1,表示有后续字节)
    • 第二个字节:0000 0001(最高位为 0,表示结束)
    • 编码结果:1100 1000 0000 0001(十六进制为 0xC8 0x01

2. 字段编号(Field Number)

        在 .proto 文件中,每个字段都被分配一个唯一的编号,用来标识字段。Protobuf 使用字段编号而不是字段名称进行传输,这显著降低了传输数据的大小。

3. 传输类型(Wire Type)

每个字段都有一个对应的传输类型,用于指示数据的编码方式。常见的传输类型包括:

4. Field 结构

        message由一个个字段组成,一个字段的完整的二进制描述即<<编号,传输类型>,值>通常称为一个field,如下图。

  • 具体而言每个field的构成为Tag-[Length]-Value;这里的[Length]是否需要是依据Tag最后三位的wire_type来决定的。

  • Tag:由字段编号和传输类型组成。
    • 编码规则:Tag = (field_num << 3) | wire_type
    • Tag 也是 Varints 编码,三位低位表示传输类型,其他位表示字段编号。
  • Length:可选项,仅当字段类型为 Length-delimited 时需要。
  • Value:字段值,根据字段类型采用不同的编码方式。

5. Protobuf 编解码的关键点

  • 在消息流中每个Tag(key/键)都是varint,编码方式为:field_num << 3 | wire_type。即,Tag(key/键)由 .proto文件中字段的编号(field_num) 和 传输类型(wire_type)两部分组成。 注:Tag也是Varints编码,其后三位是传输类型(wire_type),之前的数值为是字段编号(field_num)。 使用Varint编码方式几千/几万的字段序号(field_num)都是可以被表示的。

  • 在对一条消息(message)进行编码的时候是把该消息中所有的key-value对序列化成二进制字节流;key和value分别采用不同的编码方式。

  • 消息的二进制格式只使用消息字段的字段编号(field_num)作为Tag(key/键)的一部分,字段名和声名类型只能在解析端通过引用参考消息类型的定义(即.proto文件)才能确定。

  • 解码的时候解码程序(解码器)读入二进制的字节流,解析出每一个key-value对;如果解码过程中遇到识别不出来的filed_num就直接跳过。这样的机制保证了即使该消息(message)添加了新的字段,也不会影响旧的编/解码程序正常工作。

示例解析

示例 1:简单整数编码

定义一个字段:

int32 age = 1;
  • 字段编号1
  • 传输类型Varint(类型为 0)
  • Tag 编码
    • Tag = (1 << 3) | 0 = 8(十进制)
    • Tag 的 Varints 编码为:0000 1000(十六进制 0x08
    • 假设值为 25,Varints 编码为:0001 1001(十六进制 0x19
  • 完整消息
    • 二进制格式:0x08 0x19

示例 2:嵌套消息编码

定义一个嵌套的 Protobuf 消息:

message User {
    int32 id = 1;
    string name = 2;
}

message Group {
    int32 group_id = 1;
    repeated User members = 2;
}

编码过程:

  1. Group 消息的字段编号与类型
    • group_id1,类型为 Varint
    • members2,类型为 Length-delimited
  2. 嵌套消息编码
    • 每个 User 消息会被编码成 Length-delimited 类型,先序列化 User 的字段,然后将其作为 members 的值。

完整编码输出将是多个 Tag-Value 对的组合。

Protobuf 的负数编码

对于负数,Protobuf 不直接使用 Varints,而是采用 ZigZag 编码 优化:

  • ZigZag 编码将有符号整数映射为无符号整数,使得负数占用的字节数与正数一致。
  • 编码规则:(n << 1) ^ (n >> 31)(假设整数为 32 位)

示例:

  • -1 的 ZigZag 编码为:1
  • -2 的 ZigZag 编码为:3


http://www.kler.cn/a/520921.html

相关文章:

  • Vue3 provide/inject用法总结
  • 【SpringBoot教程】Spring Boot + MySQL + HikariCP 连接池整合教程
  • 实战 | 域环境下通过anydesk进入生产网
  • Elasticsearch+kibana安装(简单易上手)
  • 梯度下降优化算法-Adam
  • Unity阿里云OpenAPI 获取 Token的C#【记录】
  • 基于微信小程序的移动学习平台的设计与实现 移动学习平台(源码+文档)
  • Ubuntu20.04 运行 Cartographer demo bag
  • Ubuntu-手动安装 SBT
  • 物联网MQTT协议及本地化部署测试
  • doris:ORC
  • 每日 Java 面试题分享【第 9 天】
  • HTTP(1)
  • Golang Gin系列-9:Gin 集成Swagger生成文档
  • GPT 结束语设计 以nanogpt为例
  • VScode+Latex (Recipe terminated with fatal error: spawn xelatex ENOENT)
  • doris:Insert Into Values
  • 深入理解若依RuoYi-Vue数据字典设计与实现
  • 关于使用微服务的注意要点总结
  • LongLoRA:高效扩展大语言模型上下文长度的微调方法
  • 机器学习周报-文献阅读
  • 算法知识补充2
  • 软键盘显示/交互问题
  • 基于vscode的cppcmake调试环境配置
  • 【后端面试总结】mysql的group by怎么用
  • 二叉树的所有路径(力扣257)