1. 概述
互联网的产生带来了机器间通讯的需求,而互联通讯的双方需要采用约定的协议,序列化和反序列化属于通讯协议的一部分。在OSI七层协议模型中展现层(Presentation Layer)的主要功能是把应用层的对象转换成一段连续的二进制串,或者反过来,把二进制串转换成应用层的对象–这两个功能就是序列化和反序列化。
- 序列化: 将数据结构或对象转换成二进制串的过程
- 反序列化:将在序列化过程中所生成的二进制串转换成数据结构或者对象的过程
2. Protobuf
Protobuf是一种轻量级的数据交换格式,具有高效、可扩展、跨平台等特点,用于通信协议、数据存储等。
Protobuf只需定义一次数据的结构化方式,然后可以使用特殊生成的源代码轻松地使用多种语言在各种数据流中写入和读取结构化数据。
Protobuf甚至可以在不破坏针对“旧”格式编译的已部署程序的情况下更新数据结构。
其主要优点有:
- 标准的IDL和IDL编译器,这使得其对工程师非常友好。
- 序列化数据非常简洁,紧凑,与XML相比,其序列化之后的数据量约为1/3到1/10。
- 解析速度非常快,比对应的XML快约20-100倍。
- 提供了非常友好的动态库,使用非常简洁,反序列化只需要一行代码。
Protobuf的语法规则定义了如何描述消息类型、字段、枚举等元素,以及如何将这些元素序列化为二进制数据。下面是Protobuf的语法规则的简要介绍:
-
消息类型的定义:使用message关键字定义一个消息类型,可以包含多个字段,每个字段有一个名称、类型和标识符。字段可以是必需的、可选的或重复的,可以设置默认值和注释。
-
字段类型的定义:Protobuf支持多种基本类型,包括整数、浮点数、布尔值、字符串、字节流等,还支持嵌套类型和枚举类型。
-
可以使用required、optional和repeated关键字定义字段的必需性和重复性。
①required: 必须赋值,不能为空,否则该条message会被认为是“uninitialized”。除此之外,“required”字段跟“optional”字段并无差别。
②optional:字段可以赋值,也可以不赋值。假如没有赋值的话,会被赋上默认值。
③repeated: 该字段可以重复任意次数,包括0次。重复数据的顺序将会保存在protocol buffer中,将这个字段想象成一个可以自动设置size的数组就可以了。 -
字段的值为字段编号,用来标记该字段在序列化后的二进制数据中所在的field,每个字段的Number在message内部都是独一无二的。也不能进行改变,否则数据就不能正确的解包,可以用于快速定位和解析字段。
-
-
枚举类型的定义:使用enum关键字定义一个枚举类型,可以包含多个枚举值,每个枚举值有一个名称和一个整数值。枚举类型的整数值可以是任意整数,也可以使用[deprecated]标记表示该枚举值已经过时。
-
服务类型的定义:使用service关键字定义一个服务类型,可以包含多个RPC方法,每个RPC方法有一个名称、输入参数和输出参数。RPC方法可以是单向的、请求-响应的或流式的。
-
文件描述符的定义:使用syntax、package和import关键字定义文件的元数据,包括语法版本、包名和导入的其他文件。文件描述符可以使用proto文件的二进制格式或JSON格式进行序列化和反序列化。
Protobuf还支持扩展、注释、默认值、嵌套类型等高级特性,可以根据实际需求进行灵活使用。
3. 简单的实例分析
使用Protobuf语法编写.proto
文件
syntax = "proto2";
package test;
message TestMsg {
required string name = 1;
required int32 id = 2;
}
enum PhoneType {
MOBILE = 0;
HOME = 1;
WORK = 2;
}
syntax = "proto2"
:指定了protobuf文件的语法版本,即proto2。proto2是protobuf的第二个版本,是目前广泛使用的版本。package test
:指定了protobuf文件的包名,即test。包名用于标识该文件所属的命名空间,可以避免不同文件中的消息类型名称冲突。message TestMsg
部分定义了一个protobuf消息类型,用于描述测试信息。具体来说,包含了两个必须赋值的字段,分别是string类型的name和32位整数类型的id。它们的字段编号分别为1和2,这标识了它们的数据的位置。enum PhoneType
部分定义了一个枚举类型,有着三个值。
4. 编译文件
安装参考:https://github.com/protocolbuffers/protobuf
安装完成后,使用如下命令编译.proto
文件。
python环境会将.proto文件编译为*_pb2.py (比如test.proto编译为test_pb2.py),python内部调用就需要import *_pb2
。
protoc ./test.proto --python_out=./
编译完成后的python文件test_pb2.py 如下:
# Generated by the protocol buffer compiler. DO NOT EDIT!
# source: test.proto
import sys
_b=sys.version_info[0]<3 and (lambda x:x) or (lambda x:x.encode('latin1'))
from google.protobuf.internal import enum_type_wrapper
from google.protobuf import descriptor as _descriptor
from google.protobuf import message as _message
from google.protobuf import reflection as _reflection
from google.protobuf import symbol_database as _symbol_database
from google.protobuf import descriptor_pb2
# @@protoc_insertion_point(imports)
_sym_db = _symbol_database.Default()
DESCRIPTOR = _descriptor.FileDescriptor(
name='test.proto',
package='try',
syntax='proto2',
serialized_pb=_b('\n\ntest.proto\x12\x03try\"#\n\x07TestMsg\x12\x0c\n\x04name\x18\x01 \x02(\t\x12\n\n\x02id\x18\x02 \x02(\x05*+\n\tPhoneType\x12\n\n\x06MOBILE\x10\x00\x12\x08\n\x04HOME\x10\x01\x12\x08\n\x04WORK\x10\x02')
)
_PHONETYPE = _descriptor.EnumDescriptor(
name='PhoneType',
full_name='try.PhoneType',
filename=None,
file=DESCRIPTOR,
values=[
_descriptor.EnumValueDescriptor(
name='MOBILE', index=0, number=0,
options=None,
type=None),
_descriptor.EnumValueDescriptor(
name='HOME', index=1, number=1,
options=None,
type=None),
_descriptor.EnumValueDescriptor(
name='WORK', index=2, number=2,
options=None,
type=None),
],
containing_type=None,
options=None,
serialized_start=56,
serialized_end=99,
)
_sym_db.RegisterEnumDescriptor(_PHONETYPE)
PhoneType = enum_type_wrapper.EnumTypeWrapper(_PHONETYPE)
MOBILE = 0
HOME = 1
WORK = 2
_TESTMSG = _descriptor.Descriptor(
name='TestMsg',
full_name='try.TestMsg',
filename=None,
file=DESCRIPTOR,
containing_type=None,
fields=[
_descriptor.FieldDescriptor(
name='name', full_name='try.TestMsg.name', index=0,
number=1, type=9, cpp_type=9, label=2,
has_default_value=False, default_value=_b("").decode('utf-8'),
message_type=None, enum_type=None, containing_type=None,
is_extension=False, extension_scope=None,
options=None, file=DESCRIPTOR),
_descriptor.FieldDescriptor(
name='id', full_name='try.TestMsg.id', index=1,
number=2, type=5, cpp_type=1, label=2,
has_default_value=False, default_value=0,
message_type=None, enum_type=None, containing_type=None,
is_extension=False, extension_scope=None,
options=None, file=DESCRIPTOR),
],
extensions=[
],
nested_types=[],
enum_types=[
],
options=None,
is_extendable=False,
syntax='proto2',
extension_ranges=[],
oneofs=[
],
serialized_start=19,
serialized_end=54,
)
DESCRIPTOR.message_types_by_name['TestMsg'] = _TESTMSG
DESCRIPTOR.enum_types_by_name['PhoneType'] = _PHONETYPE
_sym_db.RegisterFileDescriptor(DESCRIPTOR)
TestMsg = _reflection.GeneratedProtocolMessageType('TestMsg', (_message.Message,), dict(
DESCRIPTOR = _TESTMSG,
__module__ = 'test_pb2'
# @@protoc_insertion_point(class_scope:try.TestMsg)
))
_sym_db.RegisterMessage(TestMsg)
# @@protoc_insertion_point(module_scope)
5. 使用Protobuf
编译完成以后,就可以直接导入刚才编译得到的python文件用于序列化和反序列化了
import test_pb2
# 序列化
obj = test_pb2.TestMsg()
obj.id = 1
obj.name = "zhangsan"
p = obj.SerializeToString()
print("序列化结果:", p)
# 反序列化
new_obj = test_pb2.TestMsg()
new_obj.ParseFromString(p)
print("反序列化结果:\n", new_obj)
这里打印出序列化结果,并将构建新的对象进行反序列化,再打印出反序列化结果
序列化结果: b'\n\x08zhangsan\x10\x01'
反序列化结果:
name: "zhangsan"
id: 1
6. 扩展阅读
Protobuf 有没有比 JSON 快 5 倍?
该文章针对不同类型的数据,详细测试了Protobuf与JSON之间的性能对比。
他的比较结果显示,JSON 最差的情况是下面几种:
- 跳过非常长的字符串:和字符串长度线性相关。
- 解码 double 字段:Protobuf 优势明显,是 Jsoniter 的 3.27 倍,是 Jackson 的 13.75 倍。
- 编码 double 字段:如果不能接受只保留 6 位小数,Protobuf 是 Jackson 的 12.71 倍。如果接受精度损失,Protobuf 是 Jsoniter 的 1.96 倍。
- 解码整数:Protobuf 是 Jsoniter 的 2.64 倍,是 Jackson 的 8.51 倍。
Protobuf在处理字符串和对象列表时,性能甚至不如某些高性能的JSON,如DSL-JSON
因此如果生产环境中的 JSON 没有那么多的 double 字段,都是字符串占大头,那么基本上来说替换成 Protobuf 并不会特别明显的提高速度。如果不幸的话,没准 Protobuf 还要更慢一点。