Protocol Buffers学习小记

1. 写在前面

这篇文章是有关Protocol Buffers的学习笔记,工作中这个东西会用的非常多,这是因为它提供了一种跨平台,跨语言,可延展且非常灵活的(前后向兼容)的序列化和反序列化结构化数据的方式,并且轻量悠然,在团队协作中,是一个必不可少的数据传输工具,不同团队做数据交互的时候,一般都会问到"你这个数据的proto定义是什么?", 而这个proto定义,其实就是protocol buffer的相关知识 了,如果这个东西不会用,在工作中可能会影响与其他团队的沟通,所以借着国庆,抓紧补一下这块知识。

学习这样的知识,其实最好的工具就是官方文档, 这篇文章也是参考的官方文档,然后加上我自己学习过程中的一些理解,首先,我会先介绍学习新知识的三板斧(what, why, how),然后就是message的基本格式,最后是如何基于python语言使用protocol buffer,因为这是我目前主流语言,通过实例,直接打通,把这块的知识用起来,这样以后再工作中,看到相关的proto文件或者相关的类声明,就不会陌生了。

主要内容:

  • protocol buffers初识
  • 定义message
  • python使用protocol buffer实践

Ok, lets go!

2. protocol buffers初识

学完一个新知识的时候, 怎么去知道是否已经掌握呢? 其实只需要问自己三个问题:是什么? 为什么? 怎么用?这是我目前学习的一个基本逻辑,关于protocol buffers, 我也先从这三个问题上,用我自己理解的话介绍下,文档上说的还是有些官方,看不太懂。

首先,protocol buffers是什么呢? 其实这个东西可以理解成序列化结构化数据的一种方式,序列结构化数据应该不陌生,比如之前我们常用的可能是json,也就是json.dumps一个字典字符串来序列化数据,然后json.loads反序列化读出来。还有python中常见的pickle序列化操作等。所以有了这些基础,protocol buffers应该就好理解,无非也是把结构化数据通过某种方式存起来,方便别人读取使用。

那么,既然我有pickle,有json等序列化的方式,为啥要再有个protocol buffers呢?这是因为,protocol buffers是可以跨平台,跨语言,且灵活性非常强的一种序列化方式,怎么理解? 像pickle序列数据的化,你读取的时候,得基于python,像c++,java是读不出来的,而json,虽然可以跨平台,但读取出来,操作的时候不容易操作,并且,可扩展性不好,比如结构化数据里面我想加一个字段,删除字段等,这个是没法扩展的,另外传输起来,不会快。 So,这就是,使用protocol buffers的原因, 看个例子,就可以知道它的强大之处。

首先, 有两个组Java和C++团队,Java团队先用proto文件,定义一个message:

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

其实就是一个Person类,然后它有name, id, email三个属性。有了这样一个简单的文件,通过Java语言的编译方式,就可以生成Java语言的一个类。然后,我们就可以用Java序列化一个结构化数据:

Person john = Person.newBuilder()
    .setId(1234)
    .setName("John Doe")
    .setEmail("[email protected]")
    .build();
output = new FileOutputStream(args[0]);
john.writeTo(output);

通过Java我们结构化好了数据, 这时候,就可以给到下游的C++组,C++组这时候,只需要问下这个结构化数据的proto定义是啥?就是上面那个proto文件,有了这个文件,C++组就可以基于C++语言的编译器同样生成一个C++的Person类,也同样有这几个属性,于是乎, C++组就可以直接通过代码去反序列化上游Java组给到的Person对象数据。

Person john;
fstream input(argv[1], ios::in | ios::binary);
john.ParseFromIstream(&input);
int id = john.id();
std::string name = john.name();
std::string email = john.email();

这样C++组就直接拿到了Java组的结构化数据,做自己的开发操作了。

扫描二维码关注公众号,回复: 14786417 查看本文章

这其实就是一个微型版的团队数据交互的小案例,公司里跨平台,跨语言做结构化数据的传输,基本上都是使用上面这种方式,所以我们再总结下这东西究竟是咋用的?

在这里插入图片描述

  1. 首先,我们会先事先定义好一个.proto文件,在这里面我们会定义message的格式,对应着我们后面生成的类以及属性。
  2. 有了这样一个文件,就构成了数据在团队之间传输的桥梁,接下来,根据这个文件,基于不同语言的protoc编译器,就能生成不同语言下的类
  3. 有了类,我们就可以创建对象,然后给属性赋值,然后序列化结构化数据
  4. 同样,有了类,我们创建对象,也可以拿到序列化好的数据,反序列化解析到变量里面,给我们做开发使用

其实这个过程还是比较简单的,那么它如何解决了上面json的问题呢? 首先跨平台跨语言就不用解释了,另外就是它灵活性非常强,比如我想在Person类里面加一个字段,这时候,其实不需要该任何代码,只需要修改proto文件,在这里面加减字段,就会自动加到对应语言的类里面去,这是非常方便的。 当然,也要知道不适合的一些场景:

  1. 太大的结构化数据不适合,因为protocol buffers是整个message都读到内存里面去的
  2. 非结构化数据,图片等不适合
  3. 当protocol buffers被压缩的时候, 相同的数据也会有不同的二进制序列结果,所以在反序列化数据之前,无法比较是否相等
  4. 不是面向对象的语言不支持

所以,使用场景也是很重要的。

Protocol buffers are ideal for any situation in which you need to serialize structured, record-like, typed data in a language-neutral, platform-neutral, extensible manner. They are most often used for defining communications protocols (together with gRPC) and for data storage.

接下来是一些使用指导.

3. 定义Message

要想使用protocol buffer, 第一步是定义.proto文件,也就是在这里面定义mesage, 这个目前有两个不同的语法协议proto2和proto3, 也就是定义message要遵循的一些格式, 这两个有些区别,文档建议新手的话直接学习proto3, 但这里我不具体细分这两个,我们直接针对两种类型看个例子解释下即可,实际使用中我看都有用的.

syntax = "proto2";

enum Corpus {
    
    
  CORPUS_UNSPECIFIED = 0;
  CORPUS_UNIVERSAL = 1;
  CORPUS_WEB = 2;
  CORPUS_IMAGES = 3;
  CORPUS_LOCAL = 4;
  CORPUS_NEWS = 5;
  CORPUS_PRODUCTS = 6;
  CORPUS_VIDEO = 7;
}

message SearchRequest {
    
    
  required string query = 1;
  optional int32 page_number = 2;
  optional int32 result_per_page = 3 [default = 10];
  optional Corpus corpus = 4 [default = CORPUS_UNIVERSAL];
}

这样,就在proto文件里面定义了一个message,名字叫做SearchRequest, 它有三个字段,每个字段会有描述, 数据类型, 名字以及编号.

  • 描述, proto2这里描述一般会有下面3种选择:
    • required: 这个字段恰好只有一个,且必须有这个字段,这个使用的时候要小心
    • optional: 这个字段有0个或者1个,不能超过1个
    • repeated: 0个或者多个,一般用来描述像列表这样的数据
  • 数据类型,这个其实是和生成类之后,属性的数据类型对应的, 当然这里有很多, 具体可以看文档
  • 名字,这个就不用解释了,就是字段名,生成类对应属性名
  • 编号, 每个字段有一个唯一的数字,用来在二进制消息里面标识字段, 不要改. 1-15的字段用1个字节编码,16-2047用2个字节编码,这个有啥用? 启发是比较常用的字段,放到前面,用1-15的序号编码,这样传输的时候可以节省字节数,更快. 另外,要记住,1-15不要轻易就填满了,为后面可能出现的常用字段空出点位置来

当然,字段也可以是消息类型的, 字段也能有默认值.

如果某个message在别的proto里面,我们还可以导入,然后再去基于这个message定义新的字段

import "myproject/other_protos.proto";
import public "new.proto";
import "other.proto";

//这时候,就可以用这些proto里面的message去定义新的message的某些字段了

接下来一个类型是Nested,也就是message里面还可以嵌套

message SearchResponse {
    
    
  message Result {
    
    
    required string url = 1;
    optional string title = 2;
    repeated string snippets = 3;
  }
  repeated Result result = 1;
}

//这时候,如果想用这个message里面的Result定义某个字段,需要先写他爹
message SomeOtherMessage {
    
    
  optional SearchResponse.Result result = 1;
}

ok,定义message先整理这几个简单的,等后面用到复杂的使用之后,再回来整理. 最后就是Proto3语法里面, 和2差不多,但有一些不一样的点.

syntax = "proto3";

message SearchRequest {
    
    
  string query = 1;
  int32 page_number = 2;
  int32 result_per_page = 3;
}

3里面字段是可以不加前面的描述词的,此时会默认singular(0或者1个), 并且3里面去掉了required.

4. python实践

Protocol buffers其实不复杂,关键是要知道如何使用,由于我常用的语言是python,所以这里我主要是整理下python如何使用proto buffer, 走一下官方的那个小demo.

这里写的是一个"地址簿"的应用程序, 地址簿里面是一些人,每个人会有姓名,身份证,邮箱以及电话等信息, 是一个非常典型的结构化数据格式了,这里主要是看看如何用python对结构化数据进行序列和反序列.

首先, 先编写一个proto文件,在这里面,要定义好相关的message, 如下:

syntax = "proto2";

// 类所在的包的名字
package learning_protocol;

// 定义person message name, id, email, phones
message Person {
    
    
	optional string name = 1;
	optional int32 id = 2;
	optional string email = 3;

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

	message PhoneNumber{
    
    
		optional string number = 1;
		optional PhoneType type = 2 [default = HOME];
	}
	repeated PhoneNumber phones = 4;  # 可能有多个电话
}

// 定义地址簿message, 只有一个字段人,但人可能有多个,所以是repeated
message AddressBook{
    
    
	repeated Person people = 1;    
}

这样就定义完了一个proto文件,我们命名为addressbook.proto。

接下来,安装protoc编译器,这个ubuntu上安装会比较简单, 首先去谷歌的官网, 下载压缩包,我这里下载的是protobuf-python-4.21.6.tar.gz包, 下载下来之后,解压进入protobuf-4.21.6, 执行命令:

// 配置
./configure

// 完事之后编译, 这个过程稍微长一些
make

// 完事之后,安装
sudo make install

// 成功之后, 输入
protoc
//看看是否安装成功, 我这里报了一个错误
protoc:error while loading shared libraries: libprotoc.so.3: cannot open shared objects file: No such...

//解决办法
export LD_LIBRARY_PATH=/usr/local/lib

这样就安装完了protoc。

接下来,建立一个新的目录test, 在里面建立一个proto目录,把上面的adressbook.proto放到里面。来到test目录,输入下面命令:

protoc -I=$SRC_DIR --python_out=$DST_DIR $SRC_DIR/addressbook.proto

这时候,就会在proto_gen下面生成了一个addressbook_pb2.py的文件。

在这里插入图片描述
这里面直接打开看这个py文件,是看不懂里面这些东西的。但我们可以使用了。

接下来,在test目录下面建立一个serialize.py文件,在里面写入代码:

from proto_gen import addressbook_pb2
import sys

def PromptForAddress(person):
  person.id = int(input("Enter person ID number: "))
  person.name = input("Enter name: ")

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

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

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

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


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()

这个操作没有啥技术含量, 就是先创建一个AddressBook类,这个类是直接可以从addressbook_pb2里面导入的,这就是一个电话簿信息。 这里面有一个person字段,调用address_book.people.add()方法,就可以添加人员,我们输入,就能把这个信息写入到argv[1]里面去,用的是SerializeToString()方法

  • SerializeToString(): 把一个message序列化成一个字符串,序列化后的文件是二进制文件.
  • ParseFromString(data): 把一个message从一个序列后的字符串里面解析出来

这里注意运行的时候,要加一个额外的参数,作为序列化数据保存的文件地址,比如python serialize.py a,此时就会把这个数据序列化到a里面去。

那么我们如何解开呢? 新建一个deserialize.py文件, 写入下面代码:

from proto_gen import addressbook_pb2
import sys

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.PhoneType.MOBILE:
        print("Mobile phone #: ")
      elif phone_number.type == addressbook_pb2.Person.PhoneType.HOME:
        print("Home phone #: ")
      elif phone_number.type == addressbook_pb2.Person.PhoneType.WORK:
        print("Work phone #: ")
      print(phone_number.number)

if len(sys.argv) != 2:
  print("Usage:", sys.argv[0], "ADDRESS_BOOK_FILE")
  sys.exit(-1)

address_book = addressbook_pb2.AddressBook()

f = open(sys.argv[1], "rb")
address_book.ParseFromString(f.read())
f.close()

ListPeople(address_book)

这里面就用到了ParsefromString方法,当然这里的方法有很多啊,比如ParsefromJson,就是从json串里面解析数据等。然后调用ListPeople就可以显示之前写入的地址信息了。下面就是整个实验结果了:
在这里插入图片描述
此时,如果想加字段怎么办呢? 直接在proto文件里面加,重新执行下protoc命令,py文件里面就可以直接访问了。还是非常方便的。

猜你喜欢

转载自blog.csdn.net/wuzhongqiang/article/details/127170092