本文作为Protobuf入门学习的第一篇文章,将简单介绍Protobuf协议以及如何使用Protobuf来实现序列化与反序列化。
原文地址:https://mp.weixin.qq.com/s/QbLq5gVKjaHyoaY2Vv5MRQ
protobuf
Protobuf即Protocol Buffers,是Google公司开发的一种跨语言和平台的序列化数据结构的方式,是一个灵活的、高效的用于序列化数据的协议。
与XML和JSON格式相比,Protobuf更小、更快、更便捷。Protobuf是跨语言的,并且自带一个编译器(protoc),只需要用protoc进行编译,就可以编译成Java、Python、C++、C#、Go等多种语言代码,然后可以直接使用,不需要再写其它代码,自带有解析的代码。
只需要将要被序列化的结构化数据定义一次(在.proto文件定义),便可以使用特别生成的源代码(使用Protobuf提供的生成工具)轻松的使用不同的数据流完成对结构数据的读写操作。甚至可以更新.proto文件中对数据结构的定义而不会破坏依赖旧格式编译出来的程序。
Protobuf的优点如下:
性能号,效率高。序列化后字节占用空间比XML少3-10倍,序列化的时间效率比XML快20-100倍。
有代码生成机制。将对结构化数据的操作封装成一个类,便于使用。
支持向后和向前兼容。当客户端和服务器同时使用一块协议的时候, 当客户端在协议中增加一个字节,并不会影响客户端的使用。
支持多种编程语言。Protobuf目前已经支持Java,C++,Python、Go、Ruby等多种语言。
Protobuf的缺点如下:
二进制格式导致可读性差
缺乏自描述
应用不是很广泛
现阶段Protobuf的应用场景主要有Google的gRPC, Tensorflow/Serving等。
这里介绍在Linux环境中如何安装Protobuf编译器,以及对应的Python模块。
首先,下载Protobuf的源代码:
wget https://github.com/google/protobuf/releases/download/v3.6.0/protobuf-python-3.6.0.tar.gz
解压文件,编译并安装:
tar zxvf protobuf-python-3.6.0.tar.gz
cd protobuf-3.6.0
./configure
make
make check
make install
验证Protobuf是否安装成功:
protoc --version
libprotoc 3.6.0(笔者电脑上的输出结果)
Protobuf对应的Python第三方模块为protobuf,直接使用pip安装即可:pip3 install protobuf
,验证安装成功的办法如下:
$ python
Python 3.6.10 |Anaconda, Inc.| (default, May 8 2020, 02:54:21)
[GCC 7.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import google.protobuf
>>>
如果导入第三方模块没有报错,则表明该模块安装成功。
在前面的简介部分已经说过,Protobuf在使用时定义序列化结构的文件为后缀是.proto的文件。
.proto文件有专门的语法结构,ProtoBuf有两个语法版本:v2与v3。message 用来定义一个数据结构。
我们先来看一个简单的.proto文件的例子:
syntax = "proto3";
message Person {
int64 id = 1;
string name = 2;
repeated string skills = 3; // 这里表示skills可以接受多个string类型的值
}
文件的首行生命该语法使用Protobuf3语法,同时在文件后面定义了Person消息,该消息有三个字段:id, name, skill。
每个字段的定义格式为 指定字段规则 数据类型 变量名称=数字标识符
。
指定字段规则
在Protobuf3语法中只有repeated、singular两种类型,其中singular类型(默认类型,不需要声明)表示有0个或者1个这种字段(但是不能超过1个);repeated类型表示该字段可以重复任意多次(包括0次),重复值的顺序会被保留。
数据类型
常见的有double、float、int32、string、bytes、bool等,也可以是枚举、嵌套消息类型、Any、oneof等。
在消息定义中,每个字段都有唯一的一个数字标识符
。这些标识符是用来在消息的二进制格式中识别各个字段的,一旦开始使用就不能够再改变。最小的标识号可以从1开始,最大到2^29-1(536,870,911)。不可以使用其中的[19000-19999]( (从FieldDescriptor::kFirstReservedNumber 到 FieldDescriptor::kLastReservedNumber))的标识号, Protobuf协议实现中对这些进行了预留。如果非要在.proto文件中使用这些预留标识号,编译时就会报警。同样你也不能使用早期保留的标识号。
注:[1,15]之内的标识号在编码的时候会占用一个字节。
[16,2047]之内的标识号则占用2个字节。
所以应该为那些频繁出现的消息元素保留[1,15]之内的标识号。
切记:要为将来有可能添加的、频繁出现的标识号预留一些标识号。
下面将通过一个简单的里来介绍如何使用Protobuf来实现序列化与反序列化。
定义数据结构文件(person_and_book.proto
)如下:
syntax = "proto3";
message Book { // 书籍信息
string name = 1;
float price = 2;
string press = 3;
repeated Person people = 4;
}
message Person { // 人物信息
int32 id = 1;
string name = 2;
int32 age = 3;
string email = 4;
string job = 5;
bool work_status = 6;
string city = 7;
MyAddress maps = 8;
}
message MyAddress { // 地址信息,字段类型为map
map tell_address = 1;
}
使用protoc编译person_and_book.proto文件, 命令行如下:
protoc ./person_and_book.proto --python_out=./
编译完毕,会自动生成person_and_book_pb2.py文件。在命令行中,./person_and_book.proto为需要编译的.proto文件所在路径,python_out为输出python脚本路径,./表示为当前路径。
接着我们使用一个新的脚本(add_person.py)针对该数据结构进行序列化与反序列化。完整代码如下:
# -*- coding: utf-8 -*-
import person_and_book_pb2
# 书籍信息
book = person_and_book_pb2.Book()
book.name = "Introduction to Protobuf"
book.price = 8.5
book.press = "NY Press"
# 添加人物信息
person = book.people.add()
person.id = 1
person.name = "protobuf"
person.age = 25
person.email = "[email protected]"
person.job = "college professor"
person.work_status = True
person.city = "Shanghai"
# 添加人物的地址信息
address_maps = person.maps
address_maps.tell_address["born place"] = "SX"
address_maps.tell_address["living place"] = "SH"
address_maps.tell_address["visited place"] = "BJ, GZ, SY"
# 序列化
serializeToString = book.SerializeToString()
print(type(serializeToString), serializeToString)
# 反序列化
parsed_book = person_and_book_pb2.Book()
parsed_book.ParseFromString(serializeToString)
print(type(parsed_book))
# 输出书籍信息
print("book_name: %s, book_price: %s, book_press: %s" % (parsed_book.name, parsed_book.price, parsed_book.press))
# 输出人物信息
for person in parsed_book.people:
print("p_id: %s, p_name: %s, p_age: %s, p_email: %s, p_job: %s, p_work_status: %s, p_city: %s"
% (person.id, person.name, person.age, person.email, person.job, person.work_status, person.city))
for key in person.maps.tell_address:
print(key, person.maps.tell_address[key])
输出结果如下:
b'\n\x18Introduction to Protobuf\x15\x00\x00\x08A\x1a\x08NY Press"\x84\x01\x08\x01\x12\x08protobuf\x18\x19"\[email protected]*\x11college professor0\x01:\x08ShanghaiBC\n\x1b\n\rvisited place\x12\nBJ, GZ, SY\n\x12\n\x0cliving place\x12\x02SH\n\x10\n\nborn place\x12\x02SX'
book_name: Introduction to Protobuf, book_price: 8.5, book_press: NY Press
p_id: 1, p_name: protobuf, p_age: 25, p_email: [email protected], p_job: college professor, p_work_status: True, p_city: Shanghai
born place SX
living place SH
visited place BJ, GZ, SY
google.protobuf模块中的text_format脚本允许我们直接对文本格式的消息进行解析。
我们有如下文本格式的消息(book_example.ini):
name: "Learning Protobuf",
price: 10,
press: "SH Edu Press",
people {
id: 2
name: "JC",
age: 28,
email: "[email protected]",
job: "IT",
work_status: True,
city: "SH",
maps {
tell_address: {key: "born place", value: "SX"},
tell_address: {key: "living place", value: "HZ"},
tell_address: {key: "visited place", value: "LZ, HZ, SZ, YZ, DH, JYG"},
tell_address: {key: "educated place", value: "SX, HZ, SH"},
}
}
解析该文本格式消息的代码(read_message_from_text)如下:
# -*- coding: utf-8 -*-
from google.protobuf import text_format
import person_and_book_pb2
# message: Book
book = person_and_book_pb2.Book()
model_config_file_path = "book_example.ini"
with open(model_config_file_path, 'r+') as f:
book_ini = f.read()
parsed_book = text_format.Parse(text=book_ini, message=book)
# 输出解析信息
# 输出书籍信息
print("book_name: %s, book_price: %s, book_press: %s" % (parsed_book.name, parsed_book.price, parsed_book.press))
# 输出人物信息
for person in parsed_book.people:
print("p_id: %s, p_name: %s, p_age: %s, p_email: %s, p_job: %s, p_work_status: %s, p_city: %s"
% (person.id, person.name, person.age, person.email, person.job, person.work_status, person.city))
for key in person.maps.tell_address:
print("{}: {}".format(key, person.maps.tell_address[key]))
输出结果如下:
book_name: Learning Protobuf, book_price: 10.0, book_press: SH Edu Press
p_id: 2, p_name: JC, p_age: 28, p_email: [email protected], p_job: IT, p_work_status: True, p_city: SH
visited place: LZ, HZ, SZ, YZ, DH, JYG
born place: SX
living place: HZ
educated place: SX, HZ, SH
oneof类型用来代表在实现的时候,该组属性中有且只能有一个被定义,不能出现多个。
我们有如下的数据结构(oneof_test.proto):
syntax = "proto3";
message Test {
oneof message {
MessageA a = 1;
MessageB b = 2;
}
}
message MessageA {
string content = 1;
}
message MessageB {
int32 content = 1;
}
编译生成oneof_test_pb2.py。现在需要将一个Test消息的a字段设置为MessageA,另一个Test消息的b字段设置为MessageB,代码(set_onof_field)如下:
# -*- coding: utf-8 -*-
from oneof_test_pb2 import Test, MessageA, MessageB
message_a = MessageA()
message_a.content = "Hello from MessagesA!"
message_b = MessageB()
message_b.content = 1
# 设置test.a为MessageA
test1 = Test()
test1.a.CopyFrom(message_a)
# 设置test.a为MessageB
test2 = Test()
test2.b.CopyFrom(message_b)
print(test1)
print(test2)
输出结果如下:
a {
content: "Hello from MessagesA!"
}
b {
content: 1
}
本文演示的例子都已放至Github,访问地址为:https://github.com/percent4/protobuf_learning 。
本文到此结束,感谢大家的阅读~
python基础--protobuf的使用(一):https://blog.csdn.net/u013210620/article/details/81317731
Github/protobuf: https://github.com/protocolbuffers/protobuf
gRPC快速入门(一)——Protobuf简介: https://blog.51cto.com/9291927/2331980?source=drh
Protobuf3语法详解: https://blog.csdn.net/qq_36373500/article/details/86551886
protobuf文本格式解析地图: http://www.voidcn.com/article/p-yrhvscxz-bwp.html
Question: How to set oneof fields in python?: https://github.com/protocolbuffers/protobuf/issues/5012