我们经常在网络通信和通用数据交换等应用场景中经常使用的技术是 JSON 或 XML,比如一般情况下,我们都是选择JSON来作为API接口的返回结果的数据序列化方法。因为JSON可读性和跨平台性很好,处理起来也方便,当然,2019年在做搜索数据重构项目时,为了达到更高的性能,也接触了另外一个数据序列化方案——MessagePack(下一节详细解读)。今天咱们一起来学习一个2020年在做RPC项目时接触到的一个高性能序列化协议——Protocol Buffer!
一、Protocol Buffer是何方神圣
先来看下官方定义:
Protocol buffers are Google's language-neutral, platform-neutral, extensible mechanism for serializing structured data.
protocol buffers 是一种Google开发的语言无关、平台无关、可扩展的序列化结构数据的方法,它可用于(数据)通信协议、数据存储等。
Protocol Buffers 是一种灵活,高效,自动化机制的结构数据序列化方法-可类比 XML,但是比 XML 更小(3 ~ 10倍)、更快(20 ~ 100倍)、更为简单。你可以定义数据的结构,然后使用特殊生成的源代码轻松的在各种数据流中使用各种语言进行编写和读取结构数据。你甚至可以更新数据结构,而不破坏由旧数据结构编译的已部署程序。总结一下,具有以下特点:
- 语言无关、平台无关:即 ProtoBuf 支持 Java、C++、Python 等多种语言,支持多个平台
- 高效:即比 XML 更小(3 ~ 10倍)、更快(20 ~ 100倍)、更为简单
- 扩展性、兼容性好:你可以更新数据结构,而不影响和破坏原有的旧程序
和JSON或者XML相比,Protocol buffers的优势在于数据序列化方面。ProtoBuf有着明显的优势,效率、速度、空间几乎全面占优。而充数据结构化(message的表达能力)角度讲,它并不占优,人类可读性较差,但数据结构化并不是 ProtoBuf 最关键的重点。所以可以看出 ProtoBuf 重点侧重于数据序列化 而非 数据结构化。
关于ProtoBuf的内部如何得到如此高效的序列化特性的编码实现,大家可以参考阅读下面2篇文章:
二、Protocol Buffer快速上手
下面我们以Go编程语言为例,介绍一下如何使用Protocol Buffer。通过创建一个简单的示例程序,来向你展示如何:
- 在一个.proto 文件中定义消息(message)的格式
- 使用Protocol buffer编译器
- 使用Go Protocol buffer API来写和读取消息数据
定义你的Protocol消息格式
消息被定义到一个后缀名为 .proto 的文件中。这个文件里列出了你的完整数据结构,包括一系列字段的集合,每一个字段有它的数据类型(很多基本的简单数据类型都可以作为这些字段的类型,包括 bool,int32,float,double,和 string)。当然也可以类似于嵌套,一个消息可以作为另一个消息中的一个字段的类型。可以参考下面的示例代码中, PhoneNumber 类型被定义到了Person消息中。
// go-pb/blob/master/addressbook.proto
syntax = "proto3”;
package tutorial;
import "google/protobuf/timestamp.proto";
//go_package这一行定义了包的引入路径
option go_package = "tutorialpb/addressbookpb";
message Person {
string name = 1;
int32 id = 2; // Unique ID number for this person.
string email = 3;
enum PhoneType {
MOBILE = 0;
HOME = 1;
WORK = 2;
}
message PhoneNumber {
string number = 1;
PhoneType type = 2;
}
repeated PhoneNumber phones = 4;
google.protobuf.Timestamp last_updated = 5;
}
// Our address book file is just one of these.message
AddressBook {
repeated Person people = 1;
}
编译你的Protocol buffers
有了一个 .proto 文件,接下来你需要做的事情就是用编译器生成类文件,以便于你未来要读或写 Addressbook(也包括 Person 和 PhoneNumber)消息时候用到。为了达到这个目的,你需要在你写好的 .proto文件上运行 Protocol buffer 的编译器程序。
所以,此刻你需要确保你的机器上已经成功安装了Protocol buffer的编译器。以Linux为例,可以通过下面的命令直接安装或者编译安装,根据你自己的习惯选择即可。
//命令直接安装最新稳定版
apt-get install libprotobuf-dev
apt-get install protobuf-compiler
//源码方式构建安装编译器(记得替换url为最新地址)
apt-get install build-essential
wget https://protobuf.googlecode.com/svn/rc/protobuf-2.6.0.tar.gz
tar xvfz protobuf-2.6.0.tar.gz
cd protobuf-2.6.0 $ ./configure && make install
Protocol buffer编译器安装完成之后,安装Go Protocol buffers插件:go install
go get -u -v github.com/golang/protobuf/proto
go get -u -v github.com/golang/protobuf/protoc-gen-go
到此刻,终于可以编译你的消息文件了:
//参数1:指定资源的目录位置(你的应用程序源代码放置的地方——如果你不提供一个目录的话,将会被默认使用当前目录)
//参数2:目标目录位置(你想要将编译之后生成好的代码放置的位置;通常情况下和 $SRC_DIR 相同)
//参数3:你的 .proto文件的位置
protoc -I=. --go_out=. ./addressbook.proto
执行之后,tutorialpb/addressbookpb/addressbook.pb.go这个文件就会在你指定的目标目录下生成了。
关于出现Unknown flag --go_opt错误的解决方法
今天(2021年2月22日)有同事遇到了执行编译命令是的报错信息,提示Unknown flag --go_opt。 最后查明是因为本地编译器版本过低造成的,他用的是3.0版本。升级之后解决问题。另外如果给--go_opt=paths=source_relative标记 protoc,则将输出文件放置在与输入文件相同的相对目录中。例如,该文件protos/foo.proto 生成名为的文件protos/foo.pb.go。执行效果如下图所示:
序列化和反序列化一个消息
前两步操作完之后,你就可以进行序列化和反序列化了!在Go中,你使用proto库中的Marshl方法来序列化你的pb数据。调用 proto.Marshal 会返回一个被编码为特定格式的Protocol buffer数据。
相对应的,为了解析出一个经过编码的消息(的原始信息),你可以使用proto库的 Unmarshal方法。调用这个方法,会在buf中作为Protocol buffer进行解析,然后把结果放到pb中。
// go-pb/master/main.go
package main
import (
"fmt”
"log”
"github.com/golang/protobuf/proto" pb
"github.com/gufranmirza/go-pb/tutorialpb/addressbookpb”
)
func main() {
// Create Addressbook Object with few persons
book := &pb.AddressBook{
People: []*pb.Person{
{
Id: 1234,
Name: "John Doe”,
Email: "jdoe@example.com”,
Phones: []*pb.Person_PhoneNumber{
{
Number: "555-4321”,
Type: pb.Person_HOME
},
},
},
{
Id: 1235,
Name: "Alex”,
Email: "alex@example.com”,
Phones: []*pb.Person_PhoneNumber{
{
Number: "1234-5678”,
Type: pb.Person_HOME
},
},
},
},
}
// let's unmarshal our Address object into byte array so
// that we can use it transfer the data across services
data, err := proto.Marshal(book)
if err != nil {
log.Fatal("marshaling error: ", err)
}
// printing out our raw protobuf object
fmt.Println("Raw data", data)
// let's go the other way and unmarshal
// our byte array into an object we can modify
// and use
addresssBook := pb.AddressBook{}
err = proto.Unmarshal(data, &addresssBook)
if err != nil {
log.Fatal("unmarshaling error: ", err)
}
for _, person := range addresssBook.People {
// print out our `person` object
// for good measure
fmt.Println("===========================")
fmt.Println("ID: ", person.GetId())
fmt.Println("Name: ", person.GetName())
fmt.Println("Email: ", person.GetEmail())
fmt.Println("Phones: ", person.GetPhones())
}
}
当我们执行这段代码之后,输出结果如下:
go run main.go
Raw data [10 45 10 8 74 111 104 110 32 68 111 101 16 210 9 26 16 106 100 111 101 64 101 120 97
109 112 108 101 46 99 111 109 34 12 10 8 53 53 53 45 52 51 50 49 16 1 10 42 10 4 65 108 101 1
20 16 211 9 26 16 97 108 101 120 64 101 120 97 109 112 108 101 46 99 111 109 34 13 10 9 49 50
51 52 45 53 54 55 56 16 1]
===========================
ID: 1234
Name: John Doe
Email: jdoe@example.com
Phones: [number:"555-4321" type:HOME]
===========================
ID: 1235
Name: Alex
Email: alex@example.com
Phones: [number:"1234-5678" type:HOME]
祝贺你!你现在已经可以在Go中使用Protocol buffers了。
三、Protocol Buffer优缺点
优点:
- 序列化后码流小,性能高
- 结构化数据存储格式(XML JSON等)
- 通过标识字段的顺序,可以实现协议的前向兼容
- 结构化的文档更容易管理和维护
缺点:
- 需要依赖于工具生成代码
- 支持的语言相对较少,官方只支持Java 、C++ 、Python
四、Protocol Buffer的适用场景
- 对性能要求高的RPC调用
- 具有良好的跨防火墙的访问属性
- 适合应用层对象的持久化
有关Protocol Buffer的相关内容就总结到这里,你了解了吗?
* 本页内容更新时间线:
- 2021年02月23日 18:22:01 : 知识复习 & 内容更新--go_opt参数信息
- 2020年11月27日 21:12:21 : 创建文档
* 本页内容参考以下数据源:
- https://developers.google.cn/protocol-buffers/
- https://ask.zkbhj.com/?/article/292
- https://blog.csdn.net/sanyaoxu_2/article/details/79722431
- https://mp.weixin.qq.com/s/8lgDKKM3OskvuGxCVfoIYA