• 忘掉天地
  • 仿佛也想不起自己
bingliaolongBingliaolong  2022-01-24 15:05 Aet 隐藏边栏 |   抢沙发  35 
文章评分 3 次,平均分 5.0

理解

  1. protobuf允许不同编程语言的程序员 以自己熟悉的方式在.proto文件里定义消息结构
  2. 然后protobuf的引擎把这个.proto文件里描述的消息结构进行解析,最后生成对应语言的代码,这些代码里描述了之前定义的消息结构
  3. 然后在项目中,需要用到这些消息结构的模块,只需引入这些代码,就可以使用生成的代码中提供的一些接口来序列化或反序列化

解决的问题

序列化方法

  1. 原始内存数据结构以二进制形式发送或保存。
    1. 缺陷是必须使用完全相同的内存布局、字节序等来编译接收/读取代码
  2. 发明一种特殊方式将数据项编码为单个字符串。
    1. 适合编码非常简单的数据
  3. 将数据序列化为 XML
    1. 问题是XML 是空间密集型的,编码/解码它会给应用程序带来巨大的性能损失
    2. 导航 XML DOM 树比导航类中的简单字段通常要复杂得多

protobuf

  1. 解决上面的问题。
  2. 编写.proto要存储的数据结构的描述
  3. 协议缓冲区编译器创建了一个类:
    1. 该类以高效的二进制格式实现协议缓冲区数据的自动编码和解析
    2. 生成的类为构成协议缓冲区的字段提供了 gettersetter
    3. 并将读写协议缓冲区的细节作为一个单元处理
    4. 重要的是,协议缓冲区格式支持随着时间的推移扩展格式的想法,这样代码仍然可以读取使用旧格式编码的数据

共同基础语法

本质上做两件事

  1. .proto 文件描述数据结构,也就是“协议格式”
  2. protoc 生成 C++、Java、Python 等语言的序列化/反序列化代码

syntax

  1. syntax 必须是文件中第一条非空、非注释语句
  2. 如果不写,protoc 会按 proto2 解释
    1. 因此最好永远显式写出来

字段的基本结构

  1. 字段通常由以下部分组成
    1. string:字段类型
    2. name:字段名称
    3. 1:字段编号,也叫 field number、tag number
    4. optional:字段存在性规则

  1. 字段编号不是数组下标,而是实际参与二进制编码的协议标识

字段编号规则

  1. 合法范围

  1. Protobuf 内部保留范围,不能使用

  1. 建议:
    1. 常用字段使用 1~15
      因为字段编号 1~15 的 tag 通常只占一个字节,而 16~2047 通常占两个字节
    2. 次常用字段使用 16~2047
    3. 字段一旦发布,编号不要改变
    4. 删除字段后,不要把编号分配给新字段

常用字段类型

Protobuf 类型 常见 C++ 类型 用途
double double 双精度浮点数
float float 单精度浮点数
int32 int32_t 有符号整数
int64 int64_t 有符号整数
uint32 uint32_t 无符号整数
uint64 uint64_t 无符号整数
sint32 int32_t 适合经常出现负数的整数
sint64 int64_t 适合经常出现负数的整数
fixed32 uint32_t 固定4字节
fixed64 uint64_t 固定8字节
sfixed32 int32_t 有符号固定4字节
sfixed64 int64_t 有符号固定8字节
bool bool 布尔值
string std::string UTF-8字符串
bytes std::string 任意二进制数据
  1. 需要特别注意
    1. int32 使用 varint 编码,负数通常需要10字节
    2. sint32 使用 ZigZag 编码,如果正负小数值都很常见,一般更节省空间

  1. 图片、压缩数据、加密数据等任意二进制内容应该使用
    1. 不要用 string

示例

语法

  1. 还可以定义enum类型,让某些字段具有预定义的值(如上述PhoneType
  2. 每个元素后面的= 1,= 2标记,标识该字段在二进制编码中使用的唯一“标签”。
    1. 标签编号 1-15 比更高的编号需要少一个字节来编码
    2. 因此作为一种优化,您可以决定将这些标签用于常用或重复的元素,而将标签 16 和更高的标签用于不太常用的可选元素
  3. optional
    1. 该字段可以设置也可以不设置
    2. 如果未设置可选字段值,则使用默认值
      对于简单类型,您可以指定自己的默认值,就像上面的type那样
      否则,使用系统默认值:数字类型为零,字符串为空字符串,布尔值为 false
      对于message,默认值始终是消息的“默认实例”或“原型”,没有设置任何字段
      调用访问器以获取未显式设置的可选(或必需)字段的值始终返回该字段的默认值
  4. repeated
    1. 该字段可以重复任意次数(包括零次)
    2. 可以将重复字段视为动态大小的数组
  5. required
    1. 必须提供该字段的值,否则该消息将被视为“未初始化”
    2. 如果libprotobuf在调试模式下编译,序列化未初始化的message将导致断言失败
      在优化的构建中,会跳过检查并且无论如何都会写入消息
      但是,解析未初始化的消息总是会失败(从Parse

proto2语法

一个 proto2 文件

required

  1. 含义是字段必须被设置
    1. 如果缺少 required 字段,该消息被视为未初始化,序列化通常会失败
  2. 但现在一般不推荐使用 required,因为协议发布以后,很难再安全取消这个要求
    1. 例如旧客户端永远不知道新增加的 required 字段
    2. 官方最佳实践也明确建议不要新增 required 字段

optional

  1. 表示字段可以存在,也可以不存在

  1. 生成的 C++ 类通常提供

自定义默认值

  1. proto2允许定义默认值

  1. 如果字段不存在,访问器会返回默认值
    1. 注意:默认值通常不会被写入序列化结果,只是读取未设置字段时返回的值

repeated

  1. 表示一个数组

  1. proto2中,数值类型的 repeated 字段默认不使用 packed 编码,但可以主动开启

proto3语法

一个 proto3 文件

proto3去掉了 required

  1. 普通字段直接定义

optional

  1. 现代 proto3 已经支持 optional

  1. 生成的 C++ 接口可以检查

  1. 所以

不允许自定义默认值

  1. 下面的写法在 proto3 中不合法

  1. 只能由业务代码处理

枚举 enum

  1. proto3要求第一个枚举值的编号必须为 0,因为 0 是枚举字段的默认值

  1. 所以推荐把第一个值定义成
    1. 这样可以区分“没有得到有效状态”和“确实成功”

  1. 枚举名称建议加枚举类型前缀,因为 Protobuf 的枚举值作用域规则和 C++ enum class 不完全一样,同一作用域中的枚举值可能发生名称冲突

嵌套消息

  1. 也可以把消息单独定义
    1. 如果多个消息都需要使用 Address,一般建议单独定义

消息字段

  1. 一个消息可以作为另一个消息的字段类型

  1. proto3 中,消息类型字段本身具有 presence

repeated

  1. proto2proto3都支持

  1. 它相当于集合/数组,但不等同于直接生成一个公开的 std::vector,应通过生成的接口操作

  1. proto3中,可打包的 repeated 数值字段默认使用 packed 编码
    1. proto2默认使用非 packed 编码

map

  1. mapkey只能使用整数、布尔或字符串等标量类型,不能使用
    1. 浮点数
    2. bytes
    3. message
    4. enum

  1. map字段不能同时声明为 repeated

oneof

  1. 设置 logout_request 后,之前的 login_request 会自动被清除

  1. 可以通过生成的 case 接口判断当前字段

  1. 示例
    1. oneof 很适合替代之前协议里的DataHeader
    2. 它能够保证消息类型和消息体在 schema 层面匹配,不容易出现
    3. cmd说这是Login,但body实际放了Logout

导入其他 .proto

  1. common.proto

  1. login.proto

  1. 如果引用其他 package

package

  1. 它的主要作用是
    1. 避免消息名称冲突
    2. 在生成代码中形成命名空间
    3. 给协议增加版本边界

  1. C++ 中通常对应
    1. 在实际项目里,推荐一开始就带版本
    2. 而不是等协议发布以后再修改 package

reserved

  1. 假设原来有

  1. 删除 old_address 后,应当这样写

  1. 也可以保留一段编号

  1. 绝对不要重新使用已经发布过的字段编号
    1. 官方最佳实践要求删除字段后保留其编号和名称

定义 RPC 服务

  1. Protobuf还可以描述服务接口

  1. 流式 RPC

  1. 四种 gRPC 形式

  1. 需要注意
    1. .proto 中的 service 只是接口定义,真正通过网络调用,通常还需要 gRPC 和对应代码生成插件

proto3完整协议示例

相同协议的 proto2 写法

  1. 语法上可以这么写,但新协议不建议大量使用 required
  2. 更稳妥的 proto2 设计也是以 optional 为主,然后在业务代码中验证必填字段

编译

  1. 下载生成工具:地址
  2. Windows下下载Win32压缩包
  3. proto文件定义

  1. 命令如下图:

API

解析和序列化

  1. 每个协议缓冲区类都有使用协议缓冲区二进制格式写入和读取您选择的类型的消息的方法
  2. bool SerializeToString(string* output) const;
    1. 序列化消息并将字节存储在给定的字符串中
    2. 需要注意的是,字节是二进制的,而不是文本;使用string类只是作为一个方便的容器
  3. bool ParseFromString(const string& data);
    1. 从给定的字符串解析消息
  4. bool SerializeToOstream(ostream* output) const;
    1. 将消息写入给定的 C++ ostream
  5. bool ParseFromIstream(istream* input);
    1. 解析来自给定 C++ 的消息istream

使用

示例

proto 定义

数据

  1. id = 150name = "Bob"

字节流

  1. 08 96 01 12 03 42 6f 62
    1. 每个字段都是 Tag + Value
    2. 如果是 length-delimited 类型则是 Tag + Length + Value

Tag 的编码公式

解析字段1

  1. 08Tag08 的二进制是 0000 1000

    1. 3000 = wire_type = 0(varint)
    2. 剩下的位右移 30000 1 = 1 = field_number
    3. 所以这个 tag 告诉解析器:"字段编号 1,用 varint 方式读取后面的数据"
    4. 对照 proto 定义,field 1 正是 int32 id,类型对得上(int32varint 编码)
  2. 96 01Valuevarint 解码 150

    1. varint 是变长编码,每个字节最高位(MSB)是"续位标志"
    2. 若最高位是 1,表示后面还有字节
    3. 若最高位是 0,表示这是最后一个字节
  3. 拆解

    1. 96 的最高位是 1 → 还有后续字节;去掉标志位后剩 001 0110</li> <li>01 的最高位是 0 → 这是最后一个字节;去掉标志位后剩 000 0001
    2. varint 是小端序拼接(先出现的字节是低位),所以拼接顺序要把第二个字节放在高位</li> </ol> </li> </ol> <p> <br /> </p> <h3>解析字段<code>2

      1. 12Tag12 的二进制是 0001 0010
        1. 3010 = wire_type = 2length-delimited,用于 string/bytes/嵌套 message
        2. 剩余位 0000 10 = 2 = field_number
        3. 对照 protofield 2 正是 string namewire type 2 也吻合(string 属于 length-delimited
      2. 03 是长度
        1. 因为 wire type 2 的字段格式是 Tag + Length(varint) + 原始字节,所以下一个字节 03 表示:后面跟着 3 个字节的数据
      3. 42 6f 62 是实际内容

声明:本文为原创文章,版权归所有,欢迎分享本文,转载请保留出处!

bingliaolong
Bingliaolong 关注:0    粉丝:0 最后编辑于:2026-08-04
Everything will be better.

发表评论

表情 格式 链接 私密 签到
扫一扫二维码分享