【Protobuf】Protocol Buffers 深入

Posted by 西维蜀黍 on 2020-07-28, Last Modified on 2021-09-21

更新 message

如果后面发现之前定义 message 需要增加字段了,这个时候就体现出 Protocol Buffer 的优势了,不需要改动之前的代码。不过需要满足以下 10 条规则:

  1. 不要改动原有字段的数据结构。
  2. 如果您添加新字段,则任何由代码使用“旧”消息格式序列化的消息仍然可以通过新生成的代码进行分析。您应该记住这些元素的默认值,以便新代码可以正确地与旧代码生成的消息进行交互。同样,由新代码创建的消息可以由旧代码解析:旧的二进制文件在解析时会简单地忽略新字段。(具体原因见 未知字段 这一章节)
  3. 只要字段号在更新的消息类型中不再使用,字段可以被删除。您可能需要重命名该字段,可能会添加前缀“OBSOLETE_”,或者标记成保留字段号 reserved,以便将来的 .proto 用户不会意外重复使用该号码。
  4. int32,uint32,int64,uint64 和 bool 全都兼容。这意味着您可以将字段从这些类型之一更改为另一个字段而不破坏向前或向后兼容性。如果一个数字从不适合相应类型的线路中解析出来,则会得到与在 C++ 中将该数字转换为该类型相同的效果(例如,如果将 64 位数字读为 int32,它将被截断为 32 位)。
  5. sint32 和 sint64 相互兼容,但与其他整数类型不兼容。
  6. 只要字节是有效的UTF-8,string 和 bytes 是兼容的。
  7. 嵌入式 message 与 bytes 兼容,如果 bytes 包含 message 的 encoded version。
  8. fixed32与sfixed32兼容,而fixed64与sfixed64兼容。
  9. enum 就数组而言,是可以与 int32,uint32,int64 和 uint64 兼容(请注意,如果它们不适合,值将被截断)。但是请注意,当消息反序列化时,客户端代码可能会以不同的方式对待它们:例如,未识别的 proto3 枚举类型将保留在消息中,但消息反序列化时如何表示是与语言相关的。(这点和语言相关,上面提到过了)Int 域始终只保留它们的值。
  10. 将单个更改为新的成员是安全和二进制兼容的。如果您确定一次没有代码设置多个字段,则将多个字段移至新的字段可能是安全的。将任何字段移到现有字段中都是不安全的。(注意字段和值的区别,字段是 field,值是 value)

A Simple Message

Let’s say you have the following very simple message definition:

message Test1 {
  optional int32 a = 1;
}

In an application, you create a Test1 message and set a to 150. You then serialize the message to an output stream. If you were able to examine the encoded message, you’d see three bytes:

08 96 01 (十进制)

So far, so small and numeric – but what does it mean? Read on…

Base 128 Varints 编码

To understand your simple protocol buffer encoding, you first need to understand varints. Varints are a method of serializing integers using one or more bytes. Smaller numbers take a smaller number of bytes (值越小的数字使用越少的字节数).

Each byte in a varint, except the last byte, has the most significant bit (msb) set(设置了最高有效位) – this indicates that there are further bytes to come. The lower 7 bits of each byte are used to store the two’s complement representation of the number in groups of 7 bits, least significant group first(每个字节的低 7 位用于以 7 位组的形式存储数字的二进制补码表示,最低有效组首位).

So, for example, here is the number 1 – it’s a single byte, so the msb is not set(如果用不到 1 个字节,那么最高有效位设为 0 ,如下面这个例子,1 用一个字节就可以表示,所以 msb 为 0):

一个字节(byte)有8位(bit)

0000 0001

And here is 300 – this is a bit more complicated:

1010 1100 0000 0010

How do you figure out that this is 300?

在不使用编码的情况下:

300 = 100101100

由于 300 超过了 7 位(Varint 一个字节只有 7 位能用来表示数字,最高位 msb 用来表示后面是否有更多字节),所以 300 需要用 2 个字节来表示。

// ./go/src/cmd/vendor/github.com/google/pprof/profile/proto.go
func encodeVarint(b *buffer, x uint64) {
  // 
	for x >= 128 {
    // 0x80 的十进制(decimal)是 128,二进制(binary)是 10000000
    // byte(x)|0x80 表示从右往左取7个bit
		b.data = append(b.data, byte(x)|0x80)
		x >>= 7
	}
	b.data = append(b.data, byte(x))
}

// 执行过程
// input 100101100
//   1 0010 1100 | 1000 0000 = 1010 1100
// 将 1 1010 1100 追加到 b.data 中,b.data 是 1010 1100 
// 将 1010 1100 右移 7位,得到 0000 0010 
// input 0000 0010
// 将 input 0000 0010 追加到 b.data 中,b.data 是 1010 1100 0000 0010

First you drop the msb from each byte, as this is just there to tell us whether we’ve reached the end of the number (as you can see, it’s set in the first byte (从左往右数的一个字节)as there is more than one byte in the varint):

300 = 100101100

 1010 1100 0000 0010
 010 1100  000 0010

我们再来看看protobuf是怎么decode的:

# 300 在被 encode后被存储为 1010 1100 0000 0010

# You reverse the two groups of 7 bits because, as you remember, varints store numbers with the least significant group first. 
 1010 1100 0000 0010
010 1100  000 0010

# Then you concatenate them to get your final value:
000 0010  010 1100
000 0010 ++ 010 1100
100101100
256 + 32 + 8 + 4 = 300
// ./go/src/cmd/vendor/github.com/google/pprof/profile/proto.go
func decodeVarint(data []byte) (uint64, []byte, error) {
	var u uint64
	for i := 0; ; i++ {
		if i >= 10 || i >= len(data) {
			return 0, nil, errors.New("bad varint")
		}
    // 0X7F -> 127 (decimal)
		u |= uint64(data[i]&0x7F) << uint(7*i)
    // 当读到 mbs(most significant bit)时,结束
		if data[i]&0x80 == 0 {
			return u, data[i+1:], nil
		}
	}
}

// 300 在被 encode后被存储为 1010 1100 0000 0010
// i = 0,即取出第一个byte:1010 1100
// 1010 1100 & 111 1111 -> 1010 1100
// u 设为 1010 1100

// i =1,即取出第二个byte:0000 0010
// 0000 0010 & 111 1111 -> 0000 0010
// 将 0000 0010 右移 7 位,变成 1 0000 0000
// 将 1 0000 0000 和 u 做或运算,即 1 0000 0000 | 1010 1100 -> 1 1010 1100
// 1 0010 1100
// 256 + 32 + 8 + 4 = 300

读到这里可能有读者会问了,Varint 不是为了紧凑 int 的么?那 300 本来可以用 2 个字节表示,现在还是 2 个字节了,哪里紧凑了,花费的空间没有变啊?!

Varint 确实是一种紧凑的表示数字的方法。它用一个或多个字节来表示一个数字,值越小的数字使用越少的字节数。这能减少用来表示数字的字节数。比如对于 int32 类型的数字,一般需要 4 个 byte 来表示。但是采用 Varint,对于很小的 int32 类型的数字,则可以用 1 个 byte 来表示。当然凡事都有好的也有不好的一面,采用 Varint 表示法,大的数字则需要 5 个 byte 来表示。从统计的角度来说,一般不会所有的消息中的数字都是大数,因此大多数情况下,采用 Varint 后,可以用更少的字节数来表示数字信息。

300 如果用 int32 表示,需要 4 个字节,现在用 Varint 表示,只需要 2 个字节了。缩小了一半!

Message Structure

As you know, a protocol buffer message is a series of key-value pairs. The binary version of a message just uses the field’s number(字段号) as the key – the name and declared type for each field can only be determined on the decoding end by referencing the message type’s definition (i.e. the .proto file).

When a message is encoded, the keys and values are concatenated into a byte stream. When the message is being decoded, the parser needs to be able to skip fields that it doesn’t recognize. This way, new fields can be added to a message without breaking old programs that do not know about them.

这就是所谓的 “向后”兼容性(backwards compatibility)

To this end, the “key” for each pair in a wire-format message is actually two values

  • the field number from your .proto file,
  • a wire type that provides just enough information to find the length of the following value. In most language implementations this key is referred to as a tag.

The available wire types are as follows:

Type Meaning Used For
0 Varint int32, int64, uint32, uint64, sint32, sint64, bool, enum
1 64-bit fixed64, sfixed64, double
2 Length-delimited string, bytes, embedded messages, packed repeated fields
3 Start group groups (deprecated)
4 End group groups (deprecated)
5 32-bit fixed32, sfixed32, float

注意上图中,3 和 4 已经被废弃了,所以 wire_type 取值目前只有 0、1、2、5

Each key in the streamed message is a varint with the value (field_number << 3) | wire_type – in other words, the last three bits of the number store the wire type.

Now let’s look at our simple example again. You now know that the first number in the stream is always a varint key, and here it’s 08, or (dropping the msb):

08 = 000 1000 

You take the last three bits(最右边的三位) to get the wire type (0) and then right-shift by three to get the field number (1). So you now know that the field number is 1 and the following value is a varint. Using your varint-decoding knowledge from the previous section, you can see that the next two bytes store the value 150(即 96 01 就是150经过 Proto Buffer encode后存储的结果).

96 01 = 1001 0110  0000 0001
       → 000 0001  ++  001 0110 (drop the msb and reverse the groups of 7 bits)
       → 10010110
       → 128 + 16 + 4 + 2 = 150
message Test1 {
  required int32 a = 1;
}

如果存在上面这样的一个 message 的结构,如果存入 150,在 Protocol Buffer 中显示的二进制应该为 08 96 01 。

我们可以试验一下:

// .proto
syntax = "proto3";
package proto;

message Test1 {
  int32 a = 1;
}
s := &myProto.Test1{
	A: 150,
}
// Write the new address book back to disk.
out, err := proto.Marshal(s)
if err != nil {
	log.Fatalln("Failed to encode address book:", err)
}
if err := ioutil.WriteFile("test", out, 0644); err != nil {
	log.Fatalln("Failed to write address book:", err)
}

执行后,用Hex Fiend打开这个 test 文件:

额外说一句,type 需要注意的是 type = 2 的情况,tag 里面除了包含 field number 和 wire_type ,还需要再包含一个 length,决定 value 从那一段取出来。

Reference