Protocol Buffers详解

参考  Protocol Buffers - 云+社区 - 腾讯云

目录

What are protocol buffers?

How do they work?

Why not just use XML?

Sounds like the solution for me! How do I get started?

Introducing proto3

A bit of history

Protocol Buffer Basics: Python

Why Use Protocol Buffers?

Where to Find the Example Code

Defining Your Protocol Format

Compiling Your Protocol Buffers

The Protocol Buffer API

Enums

Standard Message Methods

Parsing and Serialization

Writing A Message

Reading A Message

Extending a Protocol Buffer

Advanced Usage


What are protocol buffers?

协议缓冲区是一种灵活、高效、自动化的序列化结构化数据的机制——请考虑XML,但它更小、更快、更简单。一旦定义了希望的数据如何结构化,就可以使用特殊生成的源代码轻松地在各种数据流之间和之间编写和读取结构化数据,并使用各种语言。你甚至可以在不破坏已部署的程序的情况下更新数据结构,这些程序是根据“旧”格式编译的。

How do they work?

通过在.proto文件中定义协议缓冲区消息类型,可以指定要如何结构化序列化的信息。每个协议缓冲区消息都是一个很小的信息逻辑记录,包含一系列名称-值对。下面是.proto文件的一个非常基本的例子,它定义了一个包含关于一个人的信息的消息:

message Person {
  required string name = 1;
  required int32 id = 2;
  optional string email = 3;

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

  message PhoneNumber {
    required string number = 1;
    optional PhoneType type = 2 [default = HOME];
  }

  repeated PhoneNumber phone = 4;
}

正如你所看到的,消息格式很简单——每个消息类型都有一个或多个独特的编号字段,每个字段都有一个名称和一个值类型、值类型可以是数字(整数或浮点)、布尔值、字符串、原始字节,甚至(在上面的示例中)其他协议缓冲消息类型,允许你结构分层次的数据。你可以指定可选字段、必需字段和重复字段。你可以在协议缓冲区语言指南中找到关于编写.proto文件的更多信息。一旦定义了消息,就可以在.proto文件上运行应用程序语言的协议缓冲区编译器来生成数据访问类。这些提供简单的访问器为每个字段(如名称()和set_name())以及序列化/解析整个结构的方法从原始字节——所以,例如,如果您选择的语言C++,上面的例子运行编译器将生成一个类称为人。然后可以在应用程序中使用该类填充、序列化和检索Person协议缓冲区消息。你可以这样写一些代码:

Person person;
person.set_name("John Doe");
person.set_id(1234);
person.set_email("jdoe@example.com");
fstream output("myfile", ios::out | ios::binary);
person.SerializeToOstream(&output);

然后,稍后,你可以把你的信息读回去:

fstream input("myfile", ios::in | ios::binary);
Person person;
person.ParseFromIstream(&input);
cout << "Name: " << person.name() << endl;
cout << "E-mail: " << person.email() << endl;

你可以在不破坏向后兼容性的情况下向消息格式添加新字段;旧的二进制文件在解析时简单地忽略新字段。因此,如果你有一个使用协议缓冲区作为数据格式的通信协议,你可以扩展你的协议,而不必担心破坏现有的代码。你将在API参考部分中找到使用生成的协议缓冲区代码的完整参考,你还可以了解更多关于协议缓冲区消息如何在协议缓冲区编码中编码的信息。

Why not just use XML?

协议缓冲区在序列化结构化数据方面比XML有很多优势。协议缓冲区:

  • 更简单
  • 3到10倍小
  • 速度是20到100倍
  • 不太模糊
  • 生成易于编程使用的数据访问类

例如,假设您想为一个拥有姓名和电子邮件的人建模。在XML中,您需要:

  <person>
    <name>John Doe</name>
    <email>jdoe@example.com</email>
  </person>

而对应的协议缓冲区消息(协议缓冲区文本格式)为:

# Textual representation of a protocol buffer.
# This is *not* the binary format used on the wire.
person {
  name: "John Doe"
  email: "jdoe@example.com"
}

当将此消息编码为协议缓冲区二进制格式(上面的文本格式只是便于调试和编辑的人类可读的表示形式)时,它可能有28字节长,需要大约100-200纳秒来解析。如果去掉空格,XML版本至少是69字节,解析大约需要5000 - 10000纳秒。此外,操作协议缓冲区要容易得多:

cout << "Name: " << person.name() << endl;
cout << "E-mail: " << person.email() << endl;

而使用XML时,你必须执行以下操作:

cout << "Name: "
     << person.getElementsByTagName("name")->item(0)->innerText()
     << endl;
cout << "E-mail: "
     << person.getElementsByTagName("email")->item(0)->innerText()
     << endl;

然而,协议缓冲区并不总是比XML更好的解决方案——例如,协议缓冲区不是用标记(例如HTML)对基于文本的文档建模的好方法,因为你不能轻易地将结构与文本交织在一起。此外,XML是人类可读和可编辑的;协议缓冲区(至少以其本机格式)则不是。在某种程度上,XML也是自描述的。协议缓冲区只有在具有消息定义(.proto文件)时才有意义。

Sounds like the solution for me! How do I get started?

下载这个包——它包含Java、Python和C++协议缓冲区编译器的完整源代码,以及I/O和测试所需的类。要构建和安装编译器,请遵循自述文件中的说明。设置好之后,请按照教程中的步骤来学习你选择的语言——这将帮助你创建一个使用协议缓冲区的简单应用程序。

Introducing proto3

我们最新的版本3引入了一个新的语言版本——协议缓冲区语言版本3 (aka proto3),以及我们现有的语言版本(aka proto2)中的一些新特性。Proto3简化了协议缓冲区语言,既便于使用,又使其在更广泛的编程语言中可用:我们当前的版本允许您用Java、c++、Python、Java Lite、Ruby、JavaScript、Objective-C和c#生成协议缓冲区代码。此外,你还可以使用最新的Go protoc插件生成用于Go的proto3代码,该插件可以从golang/protobuf Github库中获得。更多的语言正在开发中。注意,这两个语言版本的API并不完全兼容。为了避免给现有用户带来不便,我们将继续在新的协议缓冲区版本中支持以前的语言版本。你可以在发布说明中看到与当前默认版本的主要区别,并在proto3语言指南中了解了proto3语法。proto3的完整文档即将发布!(如果名称proto2和proto3看起来有点混乱,那是因为当我们最初使用开源协议缓冲区时,它实际上是谷歌语言的第二个版本——也称为proto2。这也是我们的开源版本号从v2.0.0开始的原因。

A bit of history

协议缓冲区最初是在谷歌开发的,用于处理索引服务器请求/响应协议。在协议缓冲区之前,有一种用于请求和响应的格式,它使用请求和响应的手工编组/反编组,并且支持协议的多个版本。这导致了一些非常丑陋的代码,比如:

 if (version == 3) {
   ...
 } else if (version > 4) {
   if (version == 5) {
     ...
   }
   ...
 }

显式格式化的协议也使新协议版本的推出变得复杂,因为开发人员必须确保请求发起者和处理请求的实际服务器之间的所有服务器都理解新协议,然后才能切换开关开始使用新协议。

协议缓冲区的设计是为了解决这些问题:

  • 可以很容易地引入新的字段,不需要检查数据的中间服务器可以简单地解析数据并传递数据,而不需要了解所有字段。
  • 格式更具自描述性,可以用多种语言(c++、Java等)处理。

但是,用户仍然需要手工编写自己的解析代码。随着系统的发展,它获得了许多其他特点和用途:

  • 自动生成的序列化和反序列化代码避免了手工解析的需要。
  • 除了用于短时间RPC(远程过程调用)请求之外,人们开始使用协议缓冲区作为一种方便的自描述格式来持久存储数据(例如,在Bigtable中)。
  • 服务器RPC接口开始声明为协议文件的一部分,协议编译器生成存根类,用户可以用服务器接口的实际实现覆盖这些存根类。

协议缓冲区现在是谷歌的数据通用语言——在编写本文时,谷歌代码树中定义了横跨348,952 .proto文件的306,747种不同的消息类型。它们既用于RPC系统,也用于在各种存储系统中持久存储数据。

Protocol Buffer Basics: Python

本教程提供了使用协议缓冲区的基本Python程序员介绍。通过创建一个简单的示例应用程序,它向你展示了如何创建

  • 在.proto文件中定义消息格式。
  • 使用协议缓冲区编译器。
  • 使用Python协议缓冲区API编写和读取消息。

这不是在Python中使用协议缓冲区的全面指南。有关更详细的参考信息,请参阅协议缓冲区语言指南、Python API参考、Python生成的代码指南和编码参考。

Why Use Protocol Buffers?

我们将要使用的示例是一个非常简单的“地址簿”应用程序,它可以在文件中读写人们的联系信息。地址簿中的每个人都有姓名、ID、电子邮件地址和联系电话号码。如何像这样序列化和检索结构化数据?有几种方法可以解决这个问题:

  • 使用Python酸洗。这是默认的方法,因为它内置在语言中,但是它不能很好地处理模式演化,而且如果需要与用c++或Java编写的应用程序共享数据,它也不能很好地工作。
  • 你可以发明一种特殊的方法来将数据项编码为单个字符串—例如将4个int编码为“12:3:-23:67”。这是一种简单而灵活的方法,尽管它确实需要编写一次性编码和解析代码,而且解析会增加少量运行时成本。这对于编码非常简单的数据最有效。
  • 将数据序列化为XML。这种方法非常有吸引力,因为XML(某种程度上)是人类可读的,而且有许多语言的绑定库。如果你想与其他应用程序/项目共享数据,这是一个很好的选择。然而,XML是出了名的空间密集型,对它进行编码/解码会给应用程序带来巨大的性能损失。此外,导航XML DOM树要比导航类中的简单字段复杂得多。

协议缓冲区是一种灵活、高效、自动化的解决方案,可以准确地解决这个问题。使用协议缓冲区,可以编写要存储的数据结构的.proto描述。然后,协议缓冲区编译器创建一个类,该类使用有效的二进制格式实现协议缓冲区数据的自动编码和解析。生成的类为组成协议缓冲区的字段提供getter和setter方法,并负责作为一个单元读写协议缓冲区的细节。重要的是,协议缓冲区格式支持随着时间的推移扩展格式,以便代码仍然可以读取用旧格式编码的数据。

Where to Find the Example Code

示例代码包含在源代码包的“examples”目录下。下载在这里。

Defining Your Protocol Format

要创建地址簿应用程序,需要从.proto文件开始。proto文件中的定义很简单:为要序列化的每个数据结构添加一条消息,然后为消息中的每个字段指定一个名称和类型。这是定义消息的.proto文件addressbook.proto。

syntax = "proto2";

package tutorial;

message Person {
  required string name = 1;
  required int32 id = 2;
  optional string email = 3;

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

  message PhoneNumber {
    required string number = 1;
    optional PhoneType type = 2 [default = HOME];
  }

  repeated PhoneNumber phones = 4;
}

message AddressBook {
  repeated Person people = 1;
}

正如你所看到的,语法类似于C++或Java。让我们检查一下文件的每一部分,看看它是做什么的。proto文件以一个包声明开始,这有助于防止不同项目之间的命名冲突。在Python中,包通常由目录结构决定,所以在.proto文件中定义的包对生成的代码没有影响。但是,您仍然应该声明一个,以避免协议缓冲区名称空间和非python语言中的名称冲突。

接下来,你有了消息定义。消息只是包含一组类型化字段的聚合。许多标准的简单数据类型都可以作为字段类型使用,包括bool、int32、float、double和string。你还可以通过使用其他消息类型作为字段类型来为消息添加进一步的结构——在上面的示例中,Person消息包含PhoneNumber消息,而AddressBook消息包含Person消息。你甚至可以定义嵌套在其他消息中的消息类型——如你所见,PhoneNumber类型是在Person中定义的。如果希望其中一个字段具有预定义的值列表中的一个值,还可以定义enum类型——这里你希望指定电话号码可以是移动电话、家庭电话或工作电话之一。

每个元素上的“= 1”、“= 2”标记标识字段在二进制编码中使用的惟一“标记”。标记号1-15需要的编码字节比编号高的少一个,因此,作为一种优化,你可以决定对常用或重复的元素使用这些标记,而对不常用的可选元素使用标记16或更高。重复字段中的每个元素都需要重新编码标记号,因此重复字段特别适合进行这种优化。

每个字段必须用下列修饰符之一进行注释:

  • required:必须提供字段的值,否则消息将被视为“未初始化”。序列化未初始化的消息将引发异常。解析未初始化的消息将失败。除此之外,必填字段的行为与可选字段完全相同。
  • optional:字段可以设置,也可以不设置。如果未设置可选字段值,则使用默认值。对于简单类型,你可以指定自己的默认值,就像我们在示例中为电话号码类型所做的那样。否则,将使用系统默认值:数值类型为0,字符串为空字符串,bools为false。对于嵌入式消息,默认值始终是消息的“默认实例”或“原型”,它没有设置任何字段。调用访问器获取未显式设置的可选(或必需)字段的值,总是返回该字段的默认值。
  • repeated:字段可以重复任意次数(包括0次)。重复值的顺序将保留在协议缓冲区中。将重复字段看作动态大小的数组。

必填永远是必须的:你应该非常小心地按照要求标记字段。如果你希望在某个时候停止编写或发送必填项,将该字段更改为可选字段将会有问题——老读者会认为没有该字段的消息是不完整的,可能会无意中拒绝或删除它们。你应该考虑为缓冲区编写特定于应用程序的自定义验证例程。谷歌的一些工程师已经得出了这样的结论:使用必要的设备弊大于利;他们喜欢只使用可选的和重复的。然而,这种观点并不普遍。

你将在协议缓冲区语言指南中找到编写.proto文件的完整指南(包括所有可能的字段类型)。不过,不要去寻找类似于类继承的工具——协议缓冲区不会这样做。

Compiling Your Protocol Buffers

现在你已经有了.proto,接下来需要做的是生成读和写AddressBook(以及Person和PhoneNumber)消息所需的类。为此,需要在.proto上运行协议缓冲区编译器原型:

  • 如果还没有安装编译器,请下载包并按照自述文件中的说明进行操作。
  • 现在运行编译器,指定源目录(应用程序源代码所在的位置——如果不提供值,则使用当前目录)、目标目录(希望生成的代码所在的位置;通常与$SRC_DIR相同),以及.proto的路径。

proc -I=$SRC_DIR——python_out=$DST_DIR $SRC_DIR/addressbook。因为需要Python类,所以使用——python_out选项——其他受支持的语言也提供了类似的选项。

The Protocol Buffer API

与生成Java和C++协议缓冲区代码不同,Python协议缓冲区编译器不会直接为您生成数据访问代码。相反(如果你查看addressbook_pb2.py,就会看到),它会为你的所有消息、枚举和字段生成特殊的描述符,以及一些神秘的空类,每个类对应一个消息类型:

class Person(message.Message):
  __metaclass__ = reflection.GeneratedProtocolMessageType

  class PhoneNumber(message.Message):
    __metaclass__ = reflection.GeneratedProtocolMessageType
    DESCRIPTOR = _PERSON_PHONENUMBER
  DESCRIPTOR = _PERSON

class AddressBook(message.Message):
  __metaclass__ = reflection.GeneratedProtocolMessageType
  DESCRIPTOR = _ADDRESSBOOK

每个类中的重要行是_metaclass__ = reflection.GeneratedProtocolMessageType。虽然Python元类如何工作的细节超出了本教程的范围,但是你可以将它们看作是创建类的模板。在加载时,GeneratedProtocolMessageType元类使用指定的描述符创建处理每种消息类型所需的所有Python方法,并将它们添加到相关类中。然后可以在代码中使用完全填充的类。所有这些的最终效果是,您可以使用Person类,就好像它将消息基类的每个字段定义为常规字段一样。例如,你可以这样写:

import addressbook_pb2
person = addressbook_pb2.Person()
person.id = 1234
person.name = "John Doe"
person.email = "jdoe@example.com"
phone = person.phones.add()
phone.number = "555-4321"
phone.type = addressbook_pb2.Person.HOME

注意,这些赋值并不只是向泛型Python对象添加任意的新字段。如果试图分配.proto文件中没有定义的字段,将会引发AttributeError。如果将字段分配给错误类型的值,将引发类型错误。此外,在字段被设置之前读取它的值将返回默认值。

person.no_such_field = 1  # raises AttributeError
person.id = "1234"        # raises TypeError

有关协议编译器为任何特定字段定义生成哪些成员的详细信息,请参阅Python生成的代码引用。

Enums

枚举由元类扩展为一组具有整数值的符号常量。例如,常量addressbook_pb2.Person。功的值是2。

Standard Message Methods

每个消息类还包含许多其他方法,让您检查或操作整个消息,包括:

  • IsInitialized():检查是否设置了所有必需的字段。
  • __str__()返回消息的可读表示形式,对调试特别有用。(通常作为str(消息)或打印消息调用。)
  • CopyFrom(other_msg):用给定消息的值覆盖消息。
  • Clear():将所有元素清除回空状态。

这些方法实现了消息接口。有关更多信息,请参阅消息的完整API文档。

Parsing and Serialization

最后,每个协议缓冲区类都有使用协议缓冲区二进制格式编写和读取所选类型消息的方法。这些包括:

  • SerializeToString():序列化消息并将其作为字符串返回。注意,字节是二进制的,而不是文本;我们只使用str类型作为一个方便的容器。
  • ParseFromString(data):解析来自给定字符串的消息。

这只是为解析和序列化提供的几个选项。同样,要获得完整的列表,请参阅Message API引用。

协议缓冲区和O-O设计:协议缓冲区类基本上是哑数据持有者(如C语言中的结构);它们不能成为对象模型中的良好的第一类公民。如果希望向生成的类添加更丰富的行为,最好的方法是将生成的协议缓冲区类包装在特定于应用程序的类中。如果你无法控制.proto文件的设计(例如,如果你正在重用另一个项目中的一个),包装协议缓冲区也是一个好主意。在这种情况下,你可以使用包装器类来创建更适合您的应用程序的独特环境的接口:隐藏一些数据和方法,公开方便的函数,等等。永远不要通过继承生成的类来向它们添加行为。这将破坏内部机制,而且无论如何都不是良好的面向对象实践。

Writing A Message

现在让我们尝试使用协议缓冲区类。你希望地址簿应用程序能够做的第一件事是将个人详细信息写入地址簿文件。为此,你需要创建并填充协议缓冲区类的实例,然后将它们写入输出流。这是一个程序,它从一个文件中读取地址簿,根据用户输入向其中添加一个新用户,然后将新的地址簿重新写入文件。将突出显示直接调用或引用协议编译器生成的代码的部分。

#! /usr/bin/python

import addressbook_pb2
import sys

# This function fills in a Person message based on user input.
def PromptForAddress(person):
  person.id = int(raw_input("Enter person ID number: "))
  person.name = raw_input("Enter name: ")

  email = raw_input("Enter email address (blank for none): ")
  if email != "":
    person.email = email

  while True:
    number = raw_input("Enter a phone number (or leave blank to finish): ")
    if number == "":
      break

    phone_number = person.phones.add()
    phone_number.number = number

    type = raw_input("Is this a mobile, home, or work phone? ")
    if type == "mobile":
      phone_number.type = addressbook_pb2.Person.MOBILE
    elif type == "home":
      phone_number.type = addressbook_pb2.Person.HOME
    elif type == "work":
      phone_number.type = addressbook_pb2.Person.WORK
    else:
      print "Unknown phone type; leaving as default value."

# Main procedure:  Reads the entire address book from a file,
#   adds one person based on user input, then writes it back out to the same
#   file.
if len(sys.argv) != 2:
  print "Usage:", sys.argv[0], "ADDRESS_BOOK_FILE"
  sys.exit(-1)

address_book = addressbook_pb2.AddressBook()

# Read the existing address book.
try:
  f = open(sys.argv[1], "rb")
  address_book.ParseFromString(f.read())
  f.close()
except IOError:
  print sys.argv[1] + ": Could not open file.  Creating a new one."

# Add an address.
PromptForAddress(address_book.people.add())

# Write the new address book back to disk.
f = open(sys.argv[1], "wb")
f.write(address_book.SerializeToString())
f.close()

Reading A Message

当然,如果你不能从地址簿中得到任何信息,地址簿就没有多大用处了!这个例子读取上面例子创建的文件并打印其中的所有信息。

#! /usr/bin/python

import addressbook_pb2
import sys

# Iterates though all people in the AddressBook and prints info about them.
def ListPeople(address_book):
  for person in address_book.people:
    print "Person ID:", person.id
    print "  Name:", person.name
    if person.HasField('email'):
      print "  E-mail address:", person.email

    for phone_number in person.phones:
      if phone_number.type == addressbook_pb2.Person.MOBILE:
        print "  Mobile phone #: ",
      elif phone_number.type == addressbook_pb2.Person.HOME:
        print "  Home phone #: ",
      elif phone_number.type == addressbook_pb2.Person.WORK:
        print "  Work phone #: ",
      print phone_number.number

# Main procedure:  Reads the entire address book from a file and prints all
#   the information inside.
if len(sys.argv) != 2:
  print "Usage:", sys.argv[0], "ADDRESS_BOOK_FILE"
  sys.exit(-1)

address_book = addressbook_pb2.AddressBook()

# Read the existing address book.
f = open(sys.argv[1], "rb")
address_book.ParseFromString(f.read())
f.close()

ListPeople(address_book)

Extending a Protocol Buffer

在发布使用协议缓冲区的代码之后,你迟早会希望“改进”协议缓冲区的定义。如果你希望新的缓冲区向后兼容,而旧的缓冲区向前兼容(你几乎肯定希望这样),那么你需要遵循一些规则。在新版本的协议缓冲区:

  • 你不能更改任何现有字段的标记号。
  • 你不能添加或删除任何必需的字段。
  • 你可以删除可选的或重复的字段。
  • 你可以添加新的可选字段或重复字段,但必须使用新的标记号(即从未在此协议缓冲区中使用过的标记号,甚至未被删除的字段使用过)。

(这些规则有一些例外,但很少使用。)如果你遵循这些规则,旧代码将很高兴地阅读新消息,并简单地忽略任何新字段。对于旧代码,已删除的可选字段将只有默认值,已删除的重复字段将为空。新代码还将透明地读取旧消息。但是,请记住,新的可选字段不会出现在旧消息中,因此你需要显式地检查它们是否用has_设置,或者在.proto文件中提供一个合理的默认值,在标记号后面加上[default = value]。如果未为可选元素指定默认值,则使用特定于类型的默认值:对于字符串,默认值是空字符串。对于布尔值,默认值为false。对于数值类型,默认值为零。还请注意,如果你添加了一个新的重复字段,你的新代码将无法判断它是空的(根据新代码)还是从未设置过(根据旧代码),因为它没有has_标志。

Advanced Usage

协议缓冲区的用途不仅限于简单的访问器和序列化。一定要研究Python API引用,看看还可以用它们做什么。协议消息类提供的一个关键特性是反射。你可以遍历消息的字段并操作它们的值,而无需针对任何特定的消息类型编写代码。使用反射的一种非常有用的方法是将协议消息转换为其他编码(如XML或JSON)并将其转换为其他编码。反射更高级的用途可能是查找同一类型的两个消息之间的差异,或者开发一种“协议消息正则表达式”,你可以在其中编写匹配特定消息内容的表达式。如果你发挥您的想象力,就有可能将协议缓冲区应用到比您最初预期的范围更广的问题上!反射作为消息接口的一部分提供。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Wanderer001

ROIAlign原理

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值