protobuf快速上手

protobuf快速上手

什么是protobuf

  • 在了解 Protobuf 之前,我们先来搞清楚什么是序列化和反序列化。
  • 不管是面向对象还是面向过程,程序里的数据最终都是存储在变量中的,也就是在内存里。
    如果我们想把这些数据发送到另一台主机上,直接传输原始的内存块会带来很多问题 :
    • 不同主机的硬件可能不一样 ,比如字节序(大小端)不同、内存对齐方式也不同;
    • 更重要的是,你的内存块只在当前机器的内存上下文中有意义,换到另一台机器上就完全没用了;
    • 此外,网络传输本身用的是字节流,你**发出的数据必须让对方能够"看懂"**才行。
  • 那么序列化和反序列化 的作用就出来了: 序列化就是把本机的数据转换为字节序列的过程 , 反序列化就是把字节序列转换为实际数据的的过程
  • Protocol Buffers(简称 Protobuf)是 Google 开发的一种语言中立、平台中立、可扩展 的序列化 结构数据机制。你可以把它想象成更小、更快、更简单 的 JSON 或 XML,但它是二进制格式。
  • 核心用途
    • 数据序列化 :将结构化数据转换为紧凑的二进制格式,便于存储或网络传输。
    • 服务接口定义 :常与 gRPC 搭配,定义 RPC 服务的请求和响应类型。

快速配置环境

  • 参考README文档进行配置
  • 本篇文章并不涉及到具体原理, 只介绍如何上手和proto文件的各种数据类型

快速开始

  • 首先创建一个contacts.proto的文件, 向其中写入以下消息, 语法我们稍后介绍
proto 复制代码
syntax = "proto3";
// 可选的
package con;
message PeopleInfo {
	string name = 1;
	int32 id = 2;
}
  • 接着使用protoc --cpp_output=. contacts.proto生成对应的文件, 目前支持的语言有 cpp, c#, java, Rust, python等, 命令执行完成后, 当前目录下会多出来两个文件contacts.pb.h, contacts.pb.cc, contacts.pb.h内部会为PeopleInfo这个message生成一个类和各种操作字段的方法, cc文件中存储的则是实现
  • 下面我们来写一个简单的程序
cpp 复制代码
#include "contacts.pb.h"
#include <iostream>
#include <string>
int main()
{
    int id = 10;
    std::string name = "blog";

    // 先把数据设置好
    con::PeopleInfo pi;
    pi.set_id(id);
    pi.set_name(name);

    // 序列化
    std::string str = pi.SerializeAsString();
    if (str.empty())
    {
        std::cerr << "序列化失败\n";
        return -1;
    }
    else
    {
        std::cout << "序列化成功, 结果是: " << str << "\n";
    }

    con::PeopleInfo pi1;
    // 反序列化
    if (!pi1.ParseFromString(str))
    {
        std::cerr << "反序列化失败\n";
        return -1;
    }
    else
    {
        std::cout << "id: " << pi1.id() << "\nname: " << name << "\n";
    }
    return 0;
}
  • 使用g++ main.cc contacts.pb.cc -o main $(pkg-config --libs protobuf) -std=c++17 -pthread编译, 运行结果如下图, 可以看到我们直接对序列化后的结果进行输出, 并不是完整的, 原因前面也说了, protobuf序列化采用的是二进制格式 , 读取到什么都不意外

语法介绍

  • 我并不会具体介绍protobuf生成的文件, 我只讲解一下常用的接口, 接下来我们学习一下proto文件
  • 我们可以把proto文件看作是一种协议, 我们按照规定的语法写, protoc按照规定进行代码的生成. proto文件使用双斜杠进行注释 , 首行syntax="proto3"指定语法版本 , 还有其他的版本, 这里我们以proto3讲解, package可以看做是cpp内的命名空间 , 用来防止名字冲突 , message 用来定义一个消息, 命名风格推荐使用大驼峰 , 每个消息里的字段都有其明确的类型 , 其后的序号用来唯一标识该字段 , 这个数字[19000, 19999]是保留的, 我们不可以使用, 使用了是会编译报错的, 当然你也可以使用reserved保留一些数字, , 这个数字最大是229 - 1, 实际建议不要超过228- 1
  • 数据类型还分为标量类型 和自定义类型 , 自定义类型就是我们自己写的message之类的, 还有一些是google官方提供的, 比如枚举类型 , Any类型等. 下图包含了标量的一些信息
  • protobuf还支持还有数组类型 , 使用repeated修饰, 不需要像我们使用的编程语言, 需要使用[]声明

enum规则

  • 定义语法
proto 复制代码
enum PhoneType {
	MP = 0;
	TEL = 1;
}
  • 使用注意: 即使在同一个文件下, 不同的enum类型中也是不允许出现相同的名称的枚举值 的, 同一文件里的两个不同的枚举类型中不能出现相同的枚举字段 , 在不同文件中的枚举类型如果要出现同名的枚举字段, 需要保证至少有n-1个文件声明了命名空间 . 当然, 并不建议使用相同的枚举字段, 枚举类型也是支持嵌套定义在message内的
  • 枚举类型的首个字段设置为0的原因是因为可能反序列化时并没有设置这个字段, 那么就需要使用序号为0的字段作为默认填充, 保证程序的健壮性
  • 枚举常量值在32位整数范围内, 不建议使用负值
proto 复制代码
// cong.proto
syntax = "proto3";
package cong;
enum PhoneType {
    MP = 0;
}
// contacts.proto
syntax = "proto3";
package con;
import "cong.proto";
enum ph {
	MP = 0;
}
message PeopleInfo {
	string name = 1;
	int32 id = 2;
	cong.PhoneType pt = 3;
}
  • 这是生成的枚举类型的定义, 这里不知道为什么没有命名空间+枚举类型名的前缀, 读者可以写一个程序然后验证一下

Any类型

  • cpp中可以使用std::any来表示任意类型, 同样, protobuf也是支持的, 这个类型是声明好的, 我们直接导入google/protobuf/any.proto即可.可以看到这个目录下有很多google提供的类型
  • 要使用这个类型可以使用import "google/protobuf/any.proto";导入, 使用google.protobuf.Any可以定义一个字段
  • 使用PackFrom()可以把任意消息类型转换为Any类型 , 使用UnPackTo()可以把**Any类型转换为之前的类型**, Is()方法可以用来判断存放的消息类型是否为typename T

oneof类型

  • 可以用来申明一个集合, 这个集合里只有一个字段可以使用, 类似联合体, 同一时刻只能有一个活跃成员
  • 定义方式, oneof内的成员的字段编号接着最近的那个字段继续向下编写就可以, 和枚举类型不同
proto 复制代码
message exam {
	int32 id = 1;
	oneof other {
		string name = 2;
		string nick = 3;
	}
}

map类型

  • 类似常见的编程语言里的map, key-val结构, key除了float和bytes都可以, val可以是任意字段这个字段不可以使用repeated修饰, 无序存储
proto 复制代码
map<string, string> m =  1;

默认值

  • 有时一个message里的字段我们可能不需要设置或者忘记设置, 序列化可能并不会失败, 但可能其值是随机的, 非常危险, 为了保证程序的健壮性, 会在反序列时对未设置的字段给予默认值 , 下图是不同类型对应的默认值, 枚举类型我们前面讲了, 其默认值是编号为0的字段

proto文件更新规则

  • 有时随着代码和软件规模的增加, 原有的字段的数据类型可能无法满足具体的需求, 这时就需要对proto文件进行更新, 因为涉及到数据的变化, 我们不能随便更改一个字段的数据类型, 如果需要更新需要遵循以下规则
  • 不能修改任何已有字段的字段编号
  • 如果移除旧的字段, 需要保证不在使用移除字段的字段编号, 可以使用reserved进行保留编号
  • int32, uint32, int64, uint64和bool是完全兼容的, 可以直接改变, 如果出现数据溢出的问题, 会截断
  • sint32和sint64相互兼容但是不兼容其他类型
  • string 和bytes在合法utf-8字节情况下也是兼容的
  • fixed32和sfixed32兼容, fixed64和sfixed64兼容
  • enum和int32, uint32, int64, uint64兼容, 也存在数据截断问题

未知字段

  • 如果我们更改了某些字段后, 但是只有我们自己知道, 对方并不知道, 当对方收到数据进行反序列化时, 就会出现数据错误的问题, 对方可能还按照原来的格式进行反序列化数据, 那么自然得到的数据就是错误的, protobuf也提供了解决方案, 就是未知字段
  • 生成的文件中的PeopleInfo是继承了Message这个类
  • PeopleInfo内有一个GetReflection()函数,他会返回一个Reflection对象, 内部可以使用UnknownFieldSet获取未知字段的集合 , 通过其内部定义的一些工具函数进行未知字段的获取 , Descriptor类用来对message类型定义的描述, 包括**message的名字, 所有字段的描述, 以及原始的proto文件内容等**

protobuf的兼容性

  • protobuf支持向前和向后兼容, 也就是当proto文件改变后, 他也能够正常反序列化 , 不存在的字段就当做是未知字段
  • 这样的好处是便于维护 , 一个庞大的项目, 我们不可能完全知道有哪些地方依赖了我们定义的message, 我们也不可能同时升级所有的模块, 为了保证系统的功能不受影响, 就需要通信协议向前或向后兼容

protobuf缺点

  • 说了这么多, 一个东西不可能只有优点没有缺点, 下面介绍一下它的缺点
    • 序列化的数据是二进制的, 不方便人类阅读, 必须借助特定的工具查看
    • 配置麻烦, 无法开箱即用, 还需要编写proto文件和message, 如果要调用接口, 就得先查询由protoc生成的代码文件

总结

  • 除了使用protobuf外, 还可以使用json进行序列化, 我们来对比二者
  • JSON 和 Protobuf 的对比,可以清晰地看到这种权衡:
    • 调试性 :JSON 可以直接阅读和维护,而 Protobuf 必须依赖 .proto 文件和工具。
    • 数据结构 :JSON 灵活,但 ProtoBuf 可避免数据错配,然而强制 Schema 也带来了额外的编排工作。
    • 传输效率 :JSON 有重复的字段名,内容大;ProtoBuf 则通过省略字段名,做到了极致精简,性能出色。
  • 面对两者,选择的关键在于你所面对的场景:
    • 选择 Protocol Buffers:当数据大小敏感,性能是首要目标,且团队能接受严格的规范管理。
    • 选择 JSON:当开发速度和调试便利性更重要 ,对外公开 API,且不希望通过强制契约来增加复杂性。

这篇文章就到这里了, 觉得写的还不错的话欢迎点赞关注, 有问题的地方还请批评指正.

相关推荐
纪念 2291 小时前
C++ 算法基础 (差分)
开发语言·c++·算法
ShineWinsu6 小时前
对于Qt:QTreeWidget常见属性以及QTreeWidgetItem类的解析
开发语言·c++·qt·gui·客户端·多叉树·qtreewidget
女神下凡6 小时前
芯参谋(107):杰理AC_中科蓝讯_开发指南
linux·服务器·网络·单片机·嵌入式硬件
落羽的落羽6 小时前
【C++】C++17的主要特性
linux·服务器·开发语言·c++·算法·机器学习
爱和冰阔落11 小时前
【Linux网络】TCP 半包粘包怎么解决?从长度报头、JSON 拆包到网络计算器完整实战
linux·运维·网络·tcp/ip
汉克老师12 小时前
《从C++小白到算法高手:GESP 1—8级成长地图》第一讲:认识GESP——从编程新手到算法探索者
c++·gesp·信息学竞赛·小学生学c++
say_fall13 小时前
【Linux网络编程】UDP套接字编程:系统编程组件、基础回显服务、InetAddr与地址转换、翻译服务与线程池聊天室
linux·服务器·网络·udp·vr
晴天的雨.99213 小时前
【C++】继承篇
java·开发语言·c++
wuyk55513 小时前
Spider-第09讲-三格式落盘CSV-Excel-JSON
爬虫·python·json·excel