protobuf协议简介

本文的大部分内容为ChatGPT生成。感谢GPT大大提升了笔者的学习效率。

Protocol Buffers(简称PB)是一种轻便高效的结构化数据存储格式,可以用于结构化数据串行化,或者说序列化。它可用于通信协议、数据存储等领域的语言无关、平台无关、可扩展的序列化结构数据格式。最初由Google开发并开源,目前已经广泛应用于各种场景,如RPC通信、数据存储、配置文件等。

一、优点

  1. 高效:PB协议采用二进制编码,具有较小的数据体积,节省了网络传输和存储的开销。同时,PB协议的解析速度也相对较快,性能优于XML、JSON等文本格式。
  2. 易于使用:PB协议提供了一种简单的定义语言,用户可以通过定义.proto文件来描述数据结构。通过编译工具,可以自动生成多种编程语言的数据访问代码,如C++、Java、Python等。
  3. 跨平台、跨语言:PB协议支持多种编程语言,可以在不同平台和语言之间进行数据交换。
  4. 可扩展:PB协议支持向前兼容和向后兼容,即使数据结构发生变化,也可以保持兼容性。这使得PB协议在长期的项目中具有较好的可维护性。
  5. 紧凑:PB协议采用了一种高效的编码方式,可以将数据压缩到很小的空间,降低了传输和存储的成本。

二、版本

Protocol Buffers(简称PB)有三个主要版本,分别是Proto1、Proto2和Proto3。这三个版本的语法和功能有所不同,但它们都保持了向前兼容和向后兼容的特性。

2.1 Proto1

这是PB协议的最初版本,已经不再被推荐使用。

2.2 Proto2

这是PB协议的第二个版本,相较于Proto1,Proto2引入了更多的特性,如默认值、可选字段、扩展等。手Q目前用的版本还是pb2。

2.3 Proto3

这是PB协议的最新版本,相较于Proto2,Proto3进行了一些简化和改进,如移除了字段的required/optional标签,新增了映射类型(map)、Any类型等。在Proto3中,所有字段都被视为可选字段。也就是说,虽然Proto3移除了required/optional标签,但仍然支持可选字段的概念。在Proto3中,如果某个字段没有赋值,那么它将被视为具有默认值。这样,Proto3实际上隐式地将所有字段视为可选字段。

对于标量类型(如int32、float、bool等),Proto3为每种类型定义了一个默认值(例如,对于int32类型,默认值为0;对于bool类型,默认值为false)。当解析消息时,如果某个字段没有设置值,那么将使用这些默认值。

对于复合类型(如消息类型、枚举类型等),如果没有设置值,那么将使用一个空的实例或枚举的第一个值作为默认值。

这种设计使得Proto3可以简化语法,同时仍然支持可选字段的需求。

三、编码方式

Protocol Buffers(简称PB)协议是一种轻便高效的结构化数据存储格式,主要用于数据序列化。PB协议的编码方式采用了一种紧凑的二进制格式,可以节省存储空间和网络传输开销。下面详细介绍PB协议的编码方式。

  1. 字段标识符和Wire Type:在PB协议中,每个字段都有一个唯一的标识符(field number),用于区分不同的字段。同时,还有一个Wire Type,表示字段值的编码方式。字段标识符和Wire Type组合成一个整数,使用可变长度编码(Varint)进行编码。这个整数在编码后的消息中充当类型(Type)的角色。
  2. 可变长度编码(Varint):对于整数类型的数据(如int32、uint32等),PB协议采用可变长度编码。可变长度编码是一种与字节序无关的编码方式,可以用较少的字节表示较小的整数。在可变长度编码中,整数的每个字节使用最低7位表示数值,最高位表示是否还有更多字节。这种编码方式适用于表示字段标识符、Wire Type以及整数类型的字段值。
  3. 长度前缀编码:对于长度可变的数据(如字符串、字节数组、嵌套消息等),PB协议采用长度前缀编码。长度前缀编码在值前面添加一个表示数据长度的前缀,以便解码器知道值的长度。这个长度值通常使用可变长度编码(Varint)进行编码。
  4. 固定长度编码:对于固定长度的数据(如float、double等),PB协议采用固定长度编码。这些类型的字段值直接以二进制形式编码,不需要额外的长度信息。
  5. packed编码方式:对于repeated字段的标量数值类型(如整数、浮点数和布尔值),PB协议支持packed编码方式。在packed编码方式下,repeated字段的所有元素将被连续编码为一个字节流,只需要一个共享的类型(Type)和一个总的长度(Length)。这样可以减少编码长度,提高编码效率。

以下是一个简单的消息定义和编码示例:

syntax = "proto3"; 
message MyMessage { 
    int32 id = 1; 
    string name = 2; 
}

假设我们要编码一个MyMessage消息,其中id字段的值为1,name字段的值为"John":

id字段:标识符为1,Wire Type为0(表示可变长度编码),组合得到整数1 * 8 + 0 = 8,使用可变长度编码得到字节0x08。字段值1使用可变长度编码得到字节0x01。因此,id字段的编码为0x08 0x01。

name字段:标识符为2,Wire Type为2(表示长度前缀编码),组合得到整数2 * 8 + 2 = 18,使用可变长度编码得到字节0x12。字段值"John"的长度为4,使用可变长度编码得到字节0x04。字段值"John"的UTF-8编码为0x4A 0x6F 0x68 0x6E。因此,name字段的编码为0x12 0x04 0x4A 0x6F 0x68 0x6E。

将id和name字段的编码组合起来,得到整个消息的编码:0x08 0x01 0x12 0x04 0x4A 0x6F 0x68 0x6E。

3.1 字段标识符

Protocol Buffers(简称PB)协议中的Wire Type表示字段值的编码方式。在PB协议中,有以下5种Wire Type:

  1. Varint(Wire Type为0):用于编码整数类型的数据(如int32、uint32、int64、uint64、sint32、sint64、bool等)。Varint采用可变长度编码,较小的整数可以用较少的字节表示。
  2. Fixed64(Wire Type为1):用于编码固定64位长度的数据(如fixed64、sfixed64、double等)。Fixed64类型的字段值直接以二进制形式编码,不需要额外的长度信息。
  3. Length-delimited(Wire Type为2):用于编码长度可变的数据(如字符串、字节数组、嵌套消息等)。Length-delimited类型的字段值使用长度前缀编码,需要额外的长度信息来表示值的长度。
  4. Start group(Wire Type为3)和End group(Wire Type为4):这两种Wire Type用于编码分组(Group)类型的字段。分组类型已经在Proto3中被废弃,不再推荐使用。

需要注意的是,字段标识符(field number)和Wire Type会组合成一个整数,用于表示字段的类型(Type)。这个整数在编码后的消息中充当类型(Type)的角色。计算公式为:Type = field_number * 8 + wire_type。

总之,在PB协议中,Wire Type表示字段值的编码方式,共有5种Wire Type。不同类型的字段值根据其数据特点采用不同的Wire Type进行编码。

3.2 可变长度编码Varint

3.2.1 正数编码

在Protocol Buffers(简称PB)协议中,可变长度编码(Varint)是一种用于编码整数类型数据的编码方式。可变长度编码的特点是,较小的整数可以用较少的字节表示,从而节省存储空间和传输开销。可变长度编码主要用于编码整数类型的字段值(如int32、uint32等),以及字段标识符(field number)和Wire Type的组合。

可变长度编码的原理如下:

  1. 将整数表示为二进制形式,然后将二进制位分组,每组7位。如果二进制位不足7的倍数,需要在高位补0。
  2. 对于每组7位二进制位,将其编码为一个字节。字节的最低7位存储这组二进制位,最高位表示是否还有更多字节。如果当前字节是最后一个字节(即没有更多字节),最高位设置为0;否则,最高位设置为1。
  3. 将所有编码后的字节按顺序组合起来,形成最终的可变长度编码。

以下是一个将整数123编码为可变长度编码的示例:

  1. 将整数123表示为二进制形式:1111011。
  2. 将二进制位分组,每组7位:0000001 01111011。注意,我们在高位补了一个0,使得二进制位数为7的倍数。
  3. 对于每组7位二进制位,将其编码为一个字节:10000001 001111011。注意,第一个字节的最高位设置为1,表示还有更多字节;第二个字节的最高位设置为0,表示没有更多字节。
  4. 将所有编码后的字节组合起来,形成最终的可变长度编码:10000001 001111011(十六进制表示为0x81 0x7B)。

总之,在PB协议中,可变长度编码(Varint)是一种用于编码整数类型数据的编码方式。可变长度编码的特点是,较小的整数可以用较少的字节表示,从而节省存储空间和传输开销。可变长度编码的原理是将整数表示为二进制形式,然后将二进制位分组,每组7位,并将每组二进制位编码为一个字节。

3.2.2 负数编码

对于负数,Protocol Buffers(PB)协议中的可变长度编码(Varint)采用ZigZag编码来处理。ZigZag编码的目的是将有符号整数映射到无符号整数,使得较小的负数也可以用较少的字节表示,从而节省存储空间和传输开销。

以下是ZigZag编码的原理:

  1. 对于一个有符号整数n,将其映射到一个无符号整数z,计算公式为:z = (n > 31)(对于32位整数)或z = (n > 63)(对于64位整数)。这里>表示算术右移,^表示异或。
  2. 对于映射后的无符号整数z,使用可变长度编码(Varint)进行编码。

以下是一个将有符号整数-123编码为ZigZag编码的示例:

  1. 对于有符号整数-123,将其映射到无符号整数:z = (-123 > 31) = 245。
  2. 对于无符号整数245,使用可变长度编码(Varint)进行编码:11110101(分组)→ 10111010 00000101(编码为字节)→ 0xBA 0x05(十六进制表示)。

需要注意的是,ZigZag编码只适用于sint32和sint64这两种有符号整数类型。对于int32和int64这两种有符号整数类型,它们直接使用可变长度编码(Varint)进行编码,而不使用ZigZag编码。这意味着对于这两种类型,负数的编码长度可能会非常长(接近最大长度)。因此,在实际应用中,如果需要对负数进行紧凑编码,建议使用sint32和sint64这两种类型。

总之,在PB协议中,对于负数,可变长度编码(Varint)采用ZigZag编码来处理。ZigZag编码的目的是将有符号整数映射到无符号整数,使得较小的负数也可以用较少的字节表示。ZigZag编码的原理是先对有符号整数进行映射,然后对映射后的无符号整数使用可变长度编码(Varint)进行编码。

3.3 长度前缀编码

在Protocol Buffers(简称PB)协议中,长度前缀编码是一种用于编码长度可变的数据的编码方式。长度前缀编码适用于字符串、字节数组、嵌套消息等类型的字段。长度前缀编码通过在值前面添加一个表示数据长度的前缀,以便解码器知道值的长度。这个长度值通常使用可变长度编码(Varint)进行编码。

以下是长度前缀编码的步骤:

  1. 计算数据的长度。对于字符串,需要先将其转换为字节序列(例如使用UTF-8编码),然后计算字节序列的长度;对于字节数组和嵌套消息,直接计算字节序列的长度。
  2. 使用可变长度编码(Varint)对长度值进行编码,得到长度前缀。
  3. 将长度前缀和数据的字节序列按顺序组合起来,形成最终的长度前缀编码。

以下是一个将字符串"Hello"编码为长度前缀编码的示例:

  1. 将字符串"Hello"转换为字节序列(使用UTF-8编码):0x48 0x65 0x6C 0x6C 0x6F。
  2. 计算字节序列的长度:5。
  3. 使用可变长度编码(Varint)对长度值进行编码,得到长度前缀:0x05。
  4. 将长度前缀和数据的字节序列组合起来,形成最终的长度前缀编码:0x05 0x48 0x65 0x6C 0x6C 0x6F。

总之,在PB协议中,长度前缀编码是一种用于编码长度可变的数据的编码方式。长度前缀编码通过在值前面添加一个表示数据长度的前缀,以便解码器知道值的长度。这个长度值通常使用可变长度编码(Varint)进行编码。长度前缀编码适用于字符串、字节数组、嵌套消息等类型的字段。

3.4 固定长度编码

在Protocol Buffers(简称PB)协议中,固定长度编码(Fixed-length encoding)是一种用于编码固定长度的数据的编码方式。固定长度编码适用于固定长度的数据类型,如fixed32、sfixed32、fixed64、sfixed64、float和double等。对于这些数据类型,字段值的长度是固定的,分别为32位(4字节)或64位(8字节)。

固定长度编码的原理非常简单,直接将字段值的二进制表示按照固定的长度(32位或64位)进行编码。不需要额外的长度信息,因为解码器已知字段值的长度是固定的。

以下是一个将float类型的值3.14编码为固定长度编码的示例:

  1. 将float值3.14转换为32位二进制表示(IEEE 754单精度浮点数表示):01000000 01001000 11110101 11000011。
  2. 将32位二进制表示按字节顺序组合起来,形成最终的固定长度编码:0x40 0x49 0xF5 0xC3。

对于fixed32、sfixed32、fixed64和sfixed64类型的字段,它们分别表示32位和64位的无符号整数和有符号整数。这些类型的字段值直接以二进制形式编码,不需要进行额外的处理。

总之,在PB协议中,固定长度编码是一种用于编码固定长度的数据的编码方式。固定长度编码适用于固定长度的数据类型,如fixed32、sfixed32、fixed64、sfixed64、float和double等。对于这些数据类型,字段值的长度是固定的,分别为32位(4字节)或64位(8字节)。固定长度编码直接将字段值的二进制表示按照固定的长度进行编码,不需要额外的长度信息。

3.5 packed编码方式

在Protocol Buffers(简称PB)协议中,packed是一个修饰符,用于表示repeated(可重复)字段的编码方式。当一个repeated字段被标记为packed时,该字段的所有值将被连续编码为一个字节流,而不是分散编码。这可以减少编码长度,提高编码效率。

在Proto2中,packed需要显式指定,例如:

message MyMessage {
    repeated int32 numbers = 1 [packed=true];
}

在Proto3中,所有repeated字段的标量数值类型(如int32、float等)默认都是packed编码。例如:

syntax = "proto3"; 
message MyMessage { 
    repeated int32 numbers = 1; 
}

packed编码方式的优点在于:

  1. 紧凑:将所有值连续编码为一个字节流,可以减少编码长度,降低传输和存储的成本。
  2. 高效:连续编码的值可以一次性解码,提高了解码效率。

需要注意的是,packed编码方式仅适用于repeated字段的标量数值类型。对于其他类型(如字符串、字节数组、嵌套消息等),需要使用普通的编码方式。

总之,packed是PB协议中用于表示repeated字段编码方式的修饰符,可以提高编码效率和解码效率。在Proto2中,需要显式指定packed;在Proto3中,repeated字段的标量数值类型默认采用packed编码。

3.5 pb2的扩展

扩展(Extensions)是一种允许在不修改原始消息定义的情况下,动态地为消息添加新字段的机制。扩展可以让你在不破坏现有代码兼容性的前提下,灵活地扩展和修改消息结构。pb3已经不再支持扩展了。

扩展的使用主要包括以下几个步骤:

1、在原始消息定义中,使用extensions      

syntax = "proto2"; 
message MyMessage { 
    required int32 id = 1; 
    extensions 100 to 199; // 声明扩展范围,允许使用100到199的字段标识符 
}

2、在另一个.proto文件中,定义扩展字段。扩展字段需要使用 extend

import "my_message.proto"; 
extend MyMessage { 
    optional string name = 100; // 定义扩展字段,使用在扩展范围内的字段标识符 
}

3、在代码中,使用扩展字段时,需要先导入扩展字段的定义,然后使用特定于编程语言的API来设置、获取和检查扩展字段的值。这些API通常与访问普通字段的API不同,因为扩展字段是动态添加的。

以下是一个使用C++ API访问扩展字段的示例:

#include <iostream> 
#include "my_message.pb.h" 
int main() { // 创建一个MyMessage实例 
    my_package::MyMessage msg; 
    msg.set_id(1); // 设置扩展字段的值 
    msg.SetExtension(my_package::name, "John"); // 获取扩展字段的值 
    std::cout << msg.GetExtension(my_package::name) << std::endl; // 检查扩展字段是否已设置 
    std::cout << msg.HasExtension(my_package::name) << std::endl; 
    return 0;
}

总之,在Proto2中,扩展是一种允许动态添加新字段到现有消息的机制,可以灵活地扩展和修改消息结构,同时保持现有代码的兼容性。使用扩展时,需要在原始消息定义中声明扩展范围,然后在另一个.proto文件中定义扩展字段,并在代码中使用特定的API来访问扩展字段的值。

3.6 pb3的Any和oneof

在Proto3中,扩展(Extensions)功能已经被移除。Proto3推荐使用Any类型或者oneof关键字来实现类似的灵活性和扩展性。

3.6.1 Any

syntax = "proto3"; import "google/protobuf/any.proto"; message MyMessage { int32 id = 1; google.protobuf.Any data = 2; // 使用Any类型可以嵌入任意类型的消息 }

3.6.2 oneof

syntax = "proto3"; message MyMessage { int32 id = 1; oneof data { // 使用oneof关键字定义一组可选字段 string name = 2; int32 age = 3; // 可以在未来添加更多可选字段 } }

总之,在Proto3中,扩展功能已经被移除。作为替代,你可以使用Any类型或者oneof关键字来实现类似的灵活性和扩展性。这两种方法都允许你在不破坏现有代码兼容性的前提下,动态地为消息添加新字段。

3.7 兼容性

在Protocol Buffers(PB)中,保持代码的兼容性意味着在消息结构发生变化时(例如添加、修改或删除字段),现有的代码仍然可以正常工作,不需要进行修改或重新编译。这对于分布式系统和长期维护的项目来说非常重要,因为在这些场景下,不同版本的代码和数据可能需要同时共存和交互。

PB2的扩展(Extensions)、PB3的Any类型和oneof关键字都可以在一定程度上保持代码的兼容性:

  1. PB2的扩展(Extensions):通过扩展,你可以在不修改原始消息定义的情况下,动态地为消息添加新字段。这意味着现有的代码不需要知道扩展字段的存在,仍然可以正常解析和处理原始消息。同时,支持扩展的代码可以访问和操作扩展字段,实现新功能。
  2. PB3的Any类型:Any类型允许你在消息中嵌入任意类型的消息,而无需预先定义具体的字段。这意味着现有的代码可以处理包含Any类型字段的消息,而不需要知道具体的消息类型。同时,支持新消息类型的代码可以将Any类型字段解析为具体的消息类型,实现新功能。
  3. PB3的oneof关键字:oneof关键字允许你在消息中定义一组可选字段,但是这组字段中最多只能设置一个字段的值。通过在原始消息定义中预留一组可选字段,你可以在未来添加新功能时使用这些字段,而无需修改现有的代码。同时,现有的代码可以忽略它们不关心的可选字段,只处理它们关心的字段。

需要注意的是,保持代码的兼容性并不意味着所有情况下都不需要修改代码。在某些情况下(例如删除或修改字段的数据类型),你可能需要修改现有的代码以适应新的消息结构。但是,在PB中,扩展(Extensions)、Any类型和oneof关键字提供了一定程度的灵活性和扩展性,可以帮助你在很多情况下保持代码的兼容性。

当客户端版本不同,同一个PB协议的字段也不同的时候,服务端可以在新版本的协议中添加扩展(对于Proto2)或使用Any类型(对于Proto3),从而用同一台服务器服务不同版本的客户端。这种做法可以保持一定程度的向后兼容性,使得新版本的服务端能够处理旧版本客户端发送的消息,同时也支持新版本客户端的特性。

  1. 对于Proto2,可以在新版本的协议中添加扩展字段。这样,旧版本客户端发送的消息仍然可以被新版本的服务端解析和处理,因为扩展字段是可选的。同时,新版本客户端可以发送包含扩展字段的消息,服务端可以根据需要处理这些扩展字段。
  2. 对于Proto3,可以在新版本的协议中使用Any类型。将新字段定义为Any类型,可以使得旧版本客户端发送的消息仍然可以被新版本的服务端解析和处理。同时,新版本客户端可以发送包含Any类型字段的消息,服务端可以根据需要解析和处理这些字段。

需要注意的是,保持向后兼容性的同时,也要确保向前兼容性。这意味着在新版本的协议中,不要删除或修改旧版本协议中已有的字段。这样,新版本的服务端发送的消息仍然可以被旧版本的客户端解析和处理。

总之,通过在新版本的协议中添加扩展(对于Proto2)或使用Any类型(对于Proto3),服务端可以用同一台服务器服务不同版本的客户端。这种做法可以保持一定程度的兼容性,使得新旧版本的客户端和服务端能够正常交互。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值