第一行是固定的定义,声明当前文件使用 proto3 的语法,可选内容只有 proto3 和 proto2,默认值是 proto2 (但默认值未来可能被 protoc 版本影响,强烈建议明确写出来)。

message 的定义有点像 Java 的 class,构成方式是:

message MessageName {
    field_type field_name = field_number; 

field_name 是我们需要的变量名,field_type 和 field_number 则可以展开说说。

  • field type
  • Field type 是数据类型,Protobuf 虽然不是编程语言,但确实是「强类型」的,在定义结构的阶段就必须声明每一个字段的类型,运行时错误的类型会无法解析。

    数据类型可以简单分成两种,分别是基本数据类型复合数据类型

    基本数据类型就是大家熟悉的那些,官方文档给了一个详细的表格,包含了类型的说明和编译成其他语言之后的对应类型,我复制过来展示一下。

    .proto TypeNotesC++ TypeJava/Kotlin TypePython TypeGo TypeRuby TypeC# TypePHP TypeDart Type
    doubledoubledoublefloatfloat64Floatdoublefloatdouble
    floatfloatfloatfloatfloat32Floatfloatfloatdouble
    int32可变长编码。负数需要更多字节,如果倾向于负值,建议使用 sint32。int32intintint32Fixnum or Bignum (as required)intintegerint
    int64可变长编码。负数需要更多字节,如果倾向于负值,建议使用 sint64。int64longint/long[4]int64Bignumlonginteger/string[6]Int64
    uint32可变长编码。uint32int[2]int/long[4]uint32Fixnum or Bignum (as required)uintintegerint
    uint64可变长编码。uint64long[2]int/long[4]uint64Bignumulonginteger/string[6]Int64
    sint32可变长编码,有符号整数。int32intintint32Fixnum or Bignum (as required)intintegerint
    sint64可变长编码,有符号整数。int64longint/long[4]int64Bignumlonginteger/string[6]Int64
    fixed32固定 4 字节编码,大于2^28的数值比 uint32 效率高。uint32int[2]int/long[4]uint32Fixnum or Bignum (as required)uintintegerint
    fixed64固定 8 字节编码,大于2^56的数值比 uint64 效率高。uint64long[2]int/long[4]uint64Bignumulonginteger/string[6]Int64
    sfixed32固定 4 字节编码。int32intintint32Fixnum or Bignum (as required)intintegerint
    sfixed64固定 8 字节编码。int64longint/long[4]int64Bignumlonginteger/string[6]Int64
    boolboolbooleanboolboolTrueClass/FalseClassboolbooleanbool
    stringutf-8编码或者7-bit ASCII文本的字符串,长度小于2^32。stringStringstr/unicode[5]stringString (UTF-8)stringstringString
    bytes长度小于2^32。stringByteStringstr (Python 2)string
    bytes (Python 3)[]byteString (ASCII-8BIT)ByteStringstringList

    由于 Protobuf 用于通信,有时也可以先开发一侧的业务逻辑,再根据业务中需要的数据结构反过来编写 .proto 文件,那时候就可以按照上面的表把业务类型转换成 proto 的类型。

    复合数据类型就不是具体的类型了,也可以叫做其他数据类型,可能是由修饰符加类型的方式构成,也可能是引用其他的 message

    enum 的定义跟 message 有两个区别,分别是关键字要使用 enum 和 field number 要求从 0 开始。

    举个栗子:

    enum Type {
        A = 0;
        B = 1;
    message Example {
        Type type = 1;
        string content = 2;
    

    编译后在其他语言中使用就是普通的枚举类型了。

  • repeated
  • repeated 关键字用在 field type 之前,表示有 0 到多个该类型的内容,换句话说就是「列表」或者「数组」。

    repeated 不能连续使用,也就是 Protobuf 不支持二维数组,但通过 repeated - message - repeated 的结构也能实现类似的结构,功能上并没有欠缺。

    map 可以理解成 Java 的 HashMap 或者 C++ 的 std::map 之类的数据结构,可以这样定义:

    map<key_type, value_type> map_field_name = field_number;
    

    map 有一些特点:

  • key_type 只能是浮点数之外的基本数据类型,比如 int32、string、bytes 等
  • value_type 可以是任意类型
  • map 不可以被 repeated 修饰,map 的本身就可以有 0 到多个
  • map 中的内容是无序的
  • 重复的 key 只会序列化最后一个,如果从文本解析出现重复的 key 则可能失败
  • 在 Protobuf 的使用场景中,map 和 repeated message 经常是都能实现功能的,通过 Protobuf 序列化的过程保持消息的唯一性大概率不如语言本身实现的 hashmap 效果更好。横向对比的话,功能相似的 JSON 就没有类似的结构,总的来说不是很推荐用。

  • oneof
  • oneof 是熟悉了 Protobuf 之后非常常用的结构,含义是「其中之一」,可以实现类似于泛型的效果。直接看例子:

    message ConnectMessage {
        string id = 1;
        oneof content {
            string text_message = 2;
            ImageMessage image_message = 3;
            LinkMessage link_message = 4;
    

    IM 工具通信的时候,每一条消息只能有一种类型,对应着一种 UI 的显示,消息的内容就是天然的 oneof 结构。

    oneof 的语法如上所示,field number 是在当前 message 顺延的,花括号并不意味着是独立的作用域(.proto 也没有作用域的概念)。

    oneof 同样不可以 repeated,并且只能有一个值。比如先 set_image_message 在 set_text_message,之前的 image_message 就会被删除,如果是在 C++ 这种语言中,再使用之前拿到的 image_message 指针就会出 bug。

  • field number
  • Field number 在等号的右边,乍一看可能会被当成默认值,但实际上毫无关系,基础数据类型的默认值是跟类型绑定的。

    实际上 field number 是一个「序号」,表示这个字段在序列化之后的二进制数据中的位置。序号是一个数字,取值范围是 [1, 19000) 和 (19999, 2^29 - 1],中间消失的数字是 Protobuf 保留的。实际上也很难用 19000 到这么大的数字,对于 message 来说,序列化之后的序号也会占用空间,而 [1, 15] 的序号只占用 1 字节,推荐优先使用。

    「序号」机制带来的另一个问题是兼容性,随着版本迭代,.proto 文件中的字段内容也可能变化,如果某一个序号在新旧版本中有不同的类型定义,解析数据的时候就会出问题,序号和数据类型都相同而内容含义变化了的话,则可能产生更加离谱的问题。所以在更新 .proto 文件的时候,我们需要 保留(reserved) 已经使用的「序号」。

    // 一个官方的 sample
    message Foo {
      reserved 2, 15, 9 to 11;
      reserved "foo", "bar";
    

    reserved 关键字可以标记 field number 或者 field name,被标记的 field number 或者 field name 重新被使用的时候,用 protoc 编译会提示错误以及错误原因。

    实际使用时为了 .proto 文件的可读性,我们不会完全按顺序使用 field number,而是在不同的逻辑分组之间留出足够多的可用数值,reserved 不算常用。

    到此为止,使用 Protobuf 通信已经不成问题了,基本语法缺少了一些能力的支持,官方在 github 上给了一些补充类型。

  • 官方补充类型:Any
  • 链接: google/protobuf/any.proto

    前面提到过,Protobuf 是「强类型」的,每个字段都需要一个类型。Any 提供了一种额外的思路,将不确定类型的内容按字节流嵌入一个 message。对于 Any 类型的生成和解析需要特殊的 API,在 Java 中是 packunpack,在 C++ 中是 PackFromUnpackTo

    这里的 Any 跟 Java 的 Object 或者 Kotlin 的 Any 并不相同,特别是使用场景上,Protobuf 支持 oneOf,并不需要一个 Any 来实现一个字段统合不同类型。

  • 官方补充类型:Wrappers
  • 链接: google/protobuf/wrappers.proto

    Wrappers 的目标是解决基本类型存在不为空的默认值的问题,比如 int32 的默认值是 0,当我解析出一个 0 的时候,无法确定是真的 0 还是这个字段不存在。要解决也很简单,因为复合类型中的 message 是存在「空」的,只要把空判断交给 message 类型就可以了。Wrappers 提供了所有基本类型的「包装」message:

    // 举个栗子
    message Int32Value {
        int32 value = 1;
    

    使用 Int32Value 代替 int32 就可以通过 has 系列的方法先判断是否存在,存在时取值得到的 0 就是真正的 0 了。

    proto2 补充说明

    完整文档:protobuf.dev/programming…

    proto3 是 proto2 之后出现的升级版,但能力上并不是全都包含的,比如 proto3 删除了 optional 关键字、自定义默认值功能等等,不是很推荐在同一个项目中同时使用。

    但是 proto3 中可以 import proto2 写好的文件(反过来则不行),升级可以不一步到位,工作量比较可控。新项目中建议直接使用 proto3,因为 proto3 更加简单明确,也有更标准的 JSON 支持。

    表格如下:

    特性XMLJSONProtobuf
    格式文本文本二进制
    体积更小
    序列化速度较快更快
    反序列化速度较快更快
    数据类型字符串、数值、布尔值等字符串、数值、布尔值等强类型定义
    运行时检查
    跨平台支持支持支持支持

    感觉好像都很对,但没有测试过,下一篇会增加序列化速度和文件体积的实测对比。

  • 【法医奇遇记】法医破案之HTTP协议状态码探秘
  • 开发者故事 #8 微软 New Bing AI 申请与使用保姆级教程
  • ChatGPT保姆级教程,一分钟学会使用ChatGPT!
  • 【超详细】Zod 入门教程
  • 私信