What are protocol buffers?

Protocol Buffers(简称为 Protobuf) 是一种灵活, 高效, 可自动化, 而又不依赖于语言, 不依赖于平台的, 可扩展的用于序列化结构化数据的存储格式. (Google 自己说的, 听完觉得好像很流弊, 不过还是不知道怎么玩.)

使用后缀为 .proto 文件定义一次数据结构以后, 就可以用自带的代码生成工具, 自动生成各种语言版本的读写这些数据结构的代码. v2 版本的 Protocol Buffers 原生只支持 c++, java, python, 现在的 v3 可以支持语言还多了有 Go, Ruby, Objective-C, JavaScript, PHP 和 C#.

Protobuf 甚至还可以不用停掉已经部署到线上的程序直接更新数据结构的定义.

官方的 .proto 简单示例, 定义 Person 类型的 message(在 protobuf 的术语中, 结构化数据被称为 message)

person.proto

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
syntax = "proto3";
import "google/protobuf/timestamp.proto";
message Person {
  string name = 1;
  int32 id = 2;  // Unique ID number for this person.
  string email = 3;

  enum PhoneType {
    MOBILE = 0;
    HOME = 1;
    WORK = 2;
  }

  message PhoneNumber {
    string number = 1;
    PhoneType type = 2;
  }

  repeated PhoneNumber phones = 4;

  google.protobuf.Timestamp last_updated = 5;
}

// Our address book file is just one of these.
message AddressBook {
  repeated Person people = 1;
}

每个 message 类型都有一个或者多个带有唯一编号的字段, 每个字段都有对应的名称和值类型(numbers[integer|floating-point], booleans, strings, raw bytes, 或者其他的 message 类型). proto2 可以指定字段为可选(optional), 必须(required), 或者是重复(repeated), 在 proto3 只剩下 repeated 能用, 默认都是 optional, 没有 required, 而且也不能设置默认值.

默认值有点类似 Go 的零值设定:

  • For strings, the default value is the empty string.
  • For bytes, the default value is empty bytes.
  • For bools, the default value is false.
  • For numeric types, the default value is zero.
  • For enums, the default value is the first defined enum value, which must be 0.
  • For message fields, the field is not set. Its exact value is language-dependent. See the generated code guide for details.

要生成对应语言的数据结构表示, 得先装个 protoc 的编译器. 设置好环境变量以后执行

1
2
3
4
protoc -I ./ person.proto --ruby_out=./

# protoc --proto_path=IMPORT_PATH --cpp_out=DST_DIR --java_out=DST_DIR --python_out=DST_DIR --go_out=DST_DIR --ruby_out=DST_DIR --objc_out=DST_DIR --csharp_out=DST_DIR path/to/file.proto

就会自动生成一个 person_pb.rb 的文件, 好奇试了一下生成语言, Java 生成了差不多三千行代码, Go 和 Py 则都是 两百多行, JS 也有好几百行, 唯独 Ruby 只生成了下面这三十来行(是不是图省事对我大 Ruby 不上心啊喂, 好方啊 Σ( ° △ °|||)︴).

Unlike C++ and Java, Ruby generated code is unaffected by the optimize_for option in the .proto file; in effect, all Ruby code is optimized for code size.

Py 的指南也有这句, 但是 py 生成的代码也不少的啊.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# Generated by the protocol buffer compiler.  DO NOT EDIT!
# source: person.proto

require 'google/protobuf'

require 'google/protobuf/timestamp_pb'
Google::Protobuf::DescriptorPool.generated_pool.build do
  add_message "Person" do
    optional :name, :string, 1
    optional :id, :int32, 2
    optional :email, :string, 3
    repeated :phones, :message, 4, "Person.PhoneNumber"
    optional :last_updated, :message, 5, "google.protobuf.Timestamp"
  end
  add_message "Person.PhoneNumber" do
    optional :number, :string, 1
    optional :type, :enum, 2, "Person.PhoneType"
  end
  add_enum "Person.PhoneType" do
    value :MOBILE, 0
    value :HOME, 1
    value :WORK, 2
  end
  add_message "AddressBook" do
    repeated :people, :message, 1, "Person"
  end
end

Person = Google::Protobuf::DescriptorPool.generated_pool.lookup("Person").msgclass
Person::PhoneNumber = Google::Protobuf::DescriptorPool.generated_pool.lookup("Person.PhoneNumber").msgclass
Person::PhoneType = Google::Protobuf::DescriptorPool.generated_pool.lookup("Person.PhoneType").enummodule
AddressBook = Google::Protobuf::DescriptorPool.generated_pool.lookup("AddressBook").msgclass


要在 Ruby 中使用这个自动生成的数据结构还得 gem install google-protobuf 一下

强烈不建议给 message 创建自己的子类

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
require './person_pb.rb'

person = Person.new({
  name: "XguoX",
  id: 99,
  email: "xguox@xguox.xguox",
  last_updated: Google::Protobuf::Timestamp.new({seconds: Time.now.to_i, nanos: 0}),
  phones: [Person::PhoneNumber.new({number: '10086', type: Person::PhoneType::MOBILE})]
})
encoded = Person.encode(person)
person = Person.decode(encoded)

puts person.last_updated

实例方法:

  • Message#dup, Message#clone: Performs a shallow copy of this message and returns the new copy.
  • Message#==: Performs a deep equality comparison between two messages.
  • Message#hash: Computes a shallow hash of the message’s value.
  • Message#to_hash, Message#to_h: Converts the object to a ruby Hash object. Only the top-level message is converted.
  • Message#inspect: Returns a human-readable string representing this message.
  • Message#[], Message#[]=: Gets or sets a field by string name. In the future this will probably also be used to get/set extensions.

类方法:

  • Message.decode(str): Decodes a binary protobuf for this message and returns it in a new instance.
  • Message.encode(proto): Serializes a message object of this class to a binary string.
  • Message.decode_json(str): Decodes a JSON text string for this message and returns it in a new instance.
  • Message.encode_json(proto): Serializes a message object of this class to a JSON text string.
  • Message.descriptor: Returns the Google::Protobuf::Descriptor object for this message.

其他一些语法

Oneof
1
2
3
4
5
6
message SampleMessage {
  oneof test_oneof {
    string name = 4;
    SubMessage sub_message = 9;
  }
}

oneof 中的字段可以是任意不带 repeated 关键字的类型, 设置 oneof 会自动清除其它 oneof 字段的值.

1
2
3
4
5
SampleMessage message;
message.set_name("name");
CHECK(message.has_name());
message.mutable_sub_message();   // Will clear name field.
CHECK(!message.has_name());
Map Fields

Google::Protobuf::Map 类似 Ruby 的 Hash, 与常规的 Hash 不同的是, Map 是由特定类型的键和值构造成的, 所有映射的键和值都必须是指定的的类型.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
int_string_map = Google::Protobuf::Map.new(:int32, :string)

# Returns nil; items is not in the map.
print int_string_map[5]

# Raises TypeError, value should be a string
int_string_map[11] = 200

# Ok.
int_string_map[123] = "abc"

message.int32_string_map_field = int_string_map
分配标识号(转)

在一个 Message 中的每个字段都有类似 " = 1”, " = 2” 的唯一标识号, 这些标识号是用来在消息的二进制格式中识别各个字段的, 一旦开始使用就不能够再改变. 注: [1,15]之内的标识号在编码的时候会占用一个字节. [16,2047]之内的标识号则占用2个字节. 所以应该为那些频繁出现的消息元素保留 [1,15]之内的标识号. 切记:要为将来有可能添加的、频繁出现的标识号预留一些标识号.

最小的标识号可以从 1 开始, 最大到 2^29 - 1, or 536,870,911. 不可以使用其中的**[19000-19999] (从FieldDescriptor::kFirstReservedNumber 到 FieldDescriptor::kLastReservedNumber)**的标识号, Protobuf协议实现中对这些进行了预留. 如果非要在.proto文件中使用这些预留标识号, 编译时就会报警.