Google Protocol Buffer 简单介绍 - zhanjindong

标签: google protocol buffer | 发表时间:2015-03-18 19:49 | 作者:zhanjindong
出处:

以下内容主要整理自 官方文档

为什么使用 Protocol Buffers

通常序列化和解析结构化数据的几种方式?

  • 使用Java默认的序列化机制。这种方式缺点很明显:性能差、跨语言性差。
  • 将数据编码成自己定义的字符串格式。简单高效,但是仅适合比较简单的数据格式。
  • 使用XML序列化。比较普遍的做法,优点很明显,人类可读,扩展性强,自描述。但是相对来说XML结构比较冗余,解析起来比较复杂性能不高。

Protocol Buffers是一个更灵活、高效、自动化的解决方案。它通过一个.proto文件描述你想要的数据结构,它能够自动生成解析 这个数据结构的Java类,这个类提供高效的读写二进制格式数据的API。最重要的是 Protocol Buffers的扩展性和兼容性很强,只要遵很少的规则 就可以保证向前和向后兼容。

.proto文件

package tutorial;

option java_package = "com.example.tutorial";
option java_outer_classname = "AddressBookProtos";

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;
}

message AddressBook {
repeated Person person = 1;
}

Protocol Buffers 语法

.proto文件的语法跟Java的很相似,message相当于class,enum即枚举类型, 基本的数据类型有 boolint32floatdouble, 和  string,类型前的修饰符有:

  • required 必需的字段
  • optional 可选的字段
  • repeated 重复的字段

NOTE 1: 由于历史原因,数值型的repeated字段后面最好加上[packed=true],这样能达到更好的编码效果。 repeated int32 samples = 4 [packed=true];

NOTE 2: Protocol Buffers不支持map,如果需要的话只能用两个repeated代替:keys和values。

字段后面的1,2,3…是它的字段编号(tag number),注意这个编号在后期协议扩展的时候不能改动。 [default = HOME]即默认值。 为了避免命名冲突,每个.proto文件最好都定义一个 package,package用法和Java的基本类似,也支持 import

import "myproject/other_protos.proto";

扩展

PB语法虽然跟Java类似,但是它并没有继承机制,它有所谓的 Extensions,这很不同于我们原来基于面向对象的 JavaBeans式的协议设计。

Extensions就是我们定义 message的时候保留一些 field number 让第三方去扩展。

message Foo {
required int32 a = 1;
extensions 100 to 199;
}
message Bar {

optional string name =1;
optional Foo foo = 2;
}

extend Foo {
optional int32 bar = 102;
}

也可以嵌套:

message Bar {

extend Foo {
optional int32 bar = 102;
}

optional string name =1;
optional Foo foo = 2;
}

Java中设置扩展的字段:

BarProto.Bar.Builder bar = BarProto.Bar.newBuilder();
bar.setName("zjd");

FooProto.Foo.Builder foo = FooProto.Foo.newBuilder();
foo.setA(1);
foo.setExtension(BarProto.Bar.bar,12);

bar.setFoo(foo.build());
System.out.println(bar.getFoo().getExtension(BarProto.Bar.bar));

个人觉得使用起来非常不方便。

有关PB的语法的详细说明,建议看 官方文档。PB的语法相对比较简单,一旦能嵌套就能定义出非常复杂的数据结构,基本可以满足我们所有的需求。

编译.proto文件

可以用Google提供的一个proto程序来编译,Windows版本下载 protoc.exe。基本使用如下:

protoc.exe -I=$SRC_DIR --java_out=$DST_DIR $SRC_DIR/addressbook.proto

.proto文件中的 java_packagejava_outer_classname定义了生成的Java类的包名和类名。

Protocol Buffers API

AddressBookProtos.java中对应.proto文件中的每个message都会生成一个内部类: AddressBookPerson。 每个类都有自己的一个内部类 Builder用来创建实例。messages只有 getter只读方法,builders既有 getter方法也有 setter方法。

Person

// required string name = 1;
public boolean hasName();
public String getName();

// required int32 id = 2;
public boolean hasId();
public int getId();

// optional string email = 3;
public boolean hasEmail();
public String getEmail();

// repeated .tutorial.Person.PhoneNumber phone = 4;
public List<PhoneNumber> getPhoneList();
public int getPhoneCount();
public PhoneNumber getPhone(int index);

Person.Builder

// required string name = 1;
public boolean hasName();
public java.lang.String getName();
public Builder setName(String value);
public Builder clearName();

// required int32 id = 2;
public boolean hasId();
public int getId();
public Builder setId(int value);
public Builder clearId();

// optional string email = 3;
public boolean hasEmail();
public String getEmail();
public Builder setEmail(String value);
public Builder clearEmail();

// repeated .tutorial.Person.PhoneNumber phone = 4;
public List<PhoneNumber> getPhoneList();
public int getPhoneCount();
public PhoneNumber getPhone(int index);
public Builder setPhone(int index, PhoneNumber value);
public Builder addPhone(PhoneNumber value);
public Builder addAllPhone(Iterable<PhoneNumber> value);
public Builder clearPhone();

除了JavaBeans风格的getter-setter方法之外,还会生成一些其他getter-setter方法:

  • has_ 非repeated的字段都有一个这样的方法来判断字段值是否设置了还是取的默认值。
  • clear_ 每个字段都有1个clear方法用来清理字段的值为空。
  • _Count 返回repeated字段的个数。
  • addAll_ 给repeated字段赋值集合。
  • repeated字段还有根据index设置和读取的方法。

枚举和嵌套类

message嵌套message会生成嵌套类,enum会生成未Java 5的枚举类型。

public static enum PhoneType {
MOBILE(0, 0),
HOME(1, 1),
WORK(2, 2),
;
...
}

Builders vs. Messages

所有的messages生成的类像Java的string一样都是不可变的。要实例化一个message必须先创建一个builder, 修改message类只能通过builder类的setter方法修改。每个setter方法会返回builder自身,这样就能在一行代码内完成所有字段的设置:

Person john =
Person.newBuilder()
.setId(1234)
.setName("John Doe")
.setEmail("jdoe@example.com")
.addPhone(
Person.PhoneNumber.newBuilder()
.setNumber("555-4321")
.setType(Person.PhoneType.HOME))
.build();

每个message和builder提供了以下几个方法:

  • isInitialized(): 检查是否所有的required字段都已经设置;
  • toString(): 返回一个人类可读的字符串,这在debug的时候很有用;
  • mergeFrom(Message other): 只有builder有该方法,合并另外一个message对象,非repeated字段会覆盖,repeated字段则合并两个集合。
  • clear(): 只有builder有该方法,清除所有字段回到空值状态。

解析和序列化

每个message都有以下几个方法用来读写二进制格式的protocol buffer。关于二进制格式,看 这里(可能需要FQ)。

  • byte[] toByteArray(); 将message序列化为byte[]。
  • static Person parseFrom(byte[] data); 从byte[]解析出message。
  • void writeTo(OutputStream output); 序列化message并写到OutputStream。
  • static Person parseFrom(InputStream input); 从InputStream读取并解析出message。

每个 Protocol buffer类提供了对于二进制数据的一些基本操作,在面向对象上面做的并不是很好,如果需要更丰富操作或者无法修改.proto文件 的情况下,建议在生成的类的基础上封装一层。

Writing A Message

import com.example.tutorial.AddressBookProtos.AddressBook;
import com.example.tutorial.AddressBookProtos.Person;
import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.InputStreamReader;
import java.io.IOException;
import java.io.PrintStream;

class AddPerson {
// This function fills in a Person message based on user input.
static Person PromptForAddress(BufferedReader stdin,
PrintStream stdout) throws IOException {
Person.Builder person = Person.newBuilder();

stdout.print("Enter person ID: ");
person.setId(Integer.valueOf(stdin.readLine()));

stdout.print("Enter name: ");
person.setName(stdin.readLine());

stdout.print("Enter email address (blank for none): ");
String email = stdin.readLine();
if (email.length() > 0) {
person.setEmail(email);
}

while (true) {
stdout.print("Enter a phone number (or leave blank to finish): ");
String number = stdin.readLine();
if (number.length() == 0) {
break;
}

Person.PhoneNumber.Builder phoneNumber =
Person.PhoneNumber.newBuilder().setNumber(number);

stdout.print("Is this a mobile, home, or work phone? ");
String type = stdin.readLine();
if (type.equals("mobile")) {
phoneNumber.setType(Person.PhoneType.MOBILE);
} else if (type.equals("home")) {
phoneNumber.setType(Person.PhoneType.HOME);
} else if (type.equals("work")) {
phoneNumber.setType(Person.PhoneType.WORK);
} else {
stdout.println("Unknown phone type. Using default.");
}

person.addPhone(phoneNumber);
}

return person.build();
}

// Main function: Reads the entire address book from a file,
// adds one person based on user input, then writes it back out to the same
// file.
public static void main(String[] args) throws Exception {
if (args.length != 1) {
System.err.println("Usage: AddPerson ADDRESS_BOOK_FILE");
System.exit(-1);
}

AddressBook.Builder addressBook = AddressBook.newBuilder();

// Read the existing address book.
try {
addressBook.mergeFrom(new FileInputStream(args[0]));
} catch (FileNotFoundException e) {
System.out.println(args[0] + ": File not found. Creating a new file.");
}

// Add an address.
addressBook.addPerson(
PromptForAddress(new BufferedReader(new InputStreamReader(System.in)),
System.out));

// Write the new address book back to disk.
FileOutputStream output = new FileOutputStream(args[0]);
addressBook.build().writeTo(output);
output.close();
}
}

Reading A Message

import com.example.tutorial.AddressBookProtos.AddressBook;
import com.example.tutorial.AddressBookProtos.Person;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.PrintStream;

class ListPeople {
// Iterates though all people in the AddressBook and prints info about them.
static void Print(AddressBook addressBook) {
for (Person person: addressBook.getPersonList()) {
System.out.println("Person ID: " + person.getId());
System.out.println(" Name: " + person.getName());
if (person.hasEmail()) {
System.out.println(" E-mail address: " + person.getEmail());
}

for (Person.PhoneNumber phoneNumber : person.getPhoneList()) {
switch (phoneNumber.getType()) {
case MOBILE:
System.out.print(" Mobile phone #: ");
break;
case HOME:
System.out.print(" Home phone #: ");
break;
case WORK:
System.out.print(" Work phone #: ");
break;
}
System.out.println(phoneNumber.getNumber());
}
}
}

// Main function: Reads the entire address book from a file and prints all
// the information inside.
public static void main(String[] args) throws Exception {
if (args.length != 1) {
System.err.println("Usage: ListPeople ADDRESS_BOOK_FILE");
System.exit(-1);
}

// Read the existing address book.
AddressBook addressBook =
AddressBook.parseFrom(new FileInputStream(args[0]));

Print(addressBook);
}
}

扩展协议

实际使用过程中, .proto文件可能经常需要进行扩展,协议扩展就需要考虑兼容性的问题,  Protocol Buffers有良好的扩展性,只要遵守一些规则:

  • 不能修改现有字段的 tag number
  • 不能添加和删除 required字段;
  • 可以删除 optionalrepeated字段;
  • 可以添加 optionalrepeated字段,但是必须使用新的 tag number

向前兼容(老代码处理新消息):老的代码会忽视新的字段,删除的option字段会取默认值,repeated字段会是空集合。

向后兼容(新代码处理老消息):对新的代码来说可以透明的处理老的消息,但是需要谨记新增的字段在老消息中是没有的, 所以需要显示的通过has_方法判断是否设置,或者在新的.proto中给新增的字段设置合理的默认值, 对于可选字段来说如果.proto中没有设置默认值那么会使用类型的默认值,字符串为空字符串,数值型为0,布尔型为false。

注意对于新增的repeated字段来说因为没有 has_方法,所以如果为空的话是无法判断到底是新代码设置的还是老代码生成的原因。

建议字段都设置为optional,这样扩展性是最强的。

编码

英文好的可以直接看 官方文档,但我觉得博客园上 这篇文章说的更清楚点。

总的来说 Protocol Buffers的编码的优点是非常紧凑、高效,占用空间很小,解析很快,非常适合移动端。 缺点是不含有类型信息,不能自描述( 使用一些技巧也可以实现),解析必须依赖 .proto文件。

Google把PB的这种编码格式叫做 wire-format

message-buffer.jpg

PB的紧凑得益于 Varint这种可变长度的整型编码设计。

message-buffer-varint.jpg

(图片转自 http://www.cnblogs.com/shitouer/archive/2013/04/12/google-protocol-buffers-encoding.html

对比XML 和 JSON

数据大小

我们来简单对比下 Protocol BufferXMLJSON

.proto

message Request {
repeated string str = 1;
repeated int32 a = 2;
}

JavaBean

public class Request {
public List<String> strList;
public List<Integer> iList;
}

 

首先我们来对比生成数据大小。测试代码很简单,如下:

public static void main(String[] args) throws Exception {
int n = 5;
String str = "testtesttesttesttesttesttesttest";
int val = 100;
for (int i = 1; i <=n; i++) {
for (int j = 0; j < i; j++) {
str += str;
}
protobuf(i, (int) Math.pow(val, i), str);
serialize(i, (int) Math.pow(val, i), str);
System.out.println();
}
}

public static void protobuf(int n, int in, String str) {
RequestProto.Request.Builder req = RequestProto.Request.newBuilder();

List<Integer> alist = new ArrayList<Integer>();
for (int i = 0; i < n; i++) {
alist.add(in);
}
req.addAllA(alist);

List<String> strList = new ArrayList<String>();
for (int i = 0; i < n; i++) {
strList.add(str);
}
req.addAllStr(strList);

// System.out.println(req.build());
byte[] data = req.build().toByteArray();
System.out.println("protobuf size:" + data.length);
}

public static void serialize(int n, int in, String str) throws Exception {
Request req = new Request();

List<String> strList = new ArrayList<String>();
for (int i = 0; i < n; i++) {
strList.add(str);
}
req.strList = strList;

List<Integer> iList = new ArrayList<Integer>();

for (int i = 0; i < n; i++) {
iList.add(in);
}
req.iList = iList;

String xml = SerializationInstance.sharedInstance().simpleToXml(req);
// System.out.println(xml);
System.out.println("xml size:" + xml.getBytes().length);

String json = SerializationInstance.sharedInstance().fastToJson(req);
// System.out.println(json);
System.out.println("json size:" + json.getBytes().length);
}

 

随着n的增大, int类型数值越大, string类型的值也越大。我们先将 str置为空:

protobuf-int-size.png

还原str值,将 val置为1:

protobuf-string-size.png

可以看到对于int型的字段 protobufxmljson的都要小不少,尤其是xml,这得益于它的 Varint编码。对于string类型的话,随着字符串内容越多, 三者之间基本就没有差距了。

针对序列话和解析(反序列化)的性能,选了几个我们项目中比较常用的方案和 Protocol Buffer做了下对比, 只是简单的基准测试(用的是 bb.jar)结果如下:

序列化性能

protobuf-vs-xml-json-serialize.png

protobuf-vs-xml-json-serialize-tu.png

可以看到数据量较小的情况下,protobuf要比一般的xml,json序列化 快1-2个数量级fastjson已经很快了,但是protobuf比它还是要快不少。

解析性能

protobuf-parsing.png

protobuf-parsing-tu.png

protobuf解析的性能比一般的xml,json反序列化要 快2-3个数量级,比fastjson也要快1个数量级左右。

 


本文链接: Google Protocol Buffer 简单介绍,转载请注明。

相关 [google protocol buffer] 推荐:

Google Protocol Buffer 简单介绍 - zhanjindong

- - 博客园_首页
以下内容主要整理自 官方文档. 为什么使用 Protocol Buffers. Protocol Buffers 语法. 为什么使用 Protocol Buffers. 通常序列化和解析结构化数据的几种方式. 使用Java默认的序列化机制. 这种方式缺点很明显:性能差、跨语言性差. 将数据编码成自己定义的字符串格式.

Google Protocol Buffers 概述

- - 博客园_首页
Protocol Buffers 是一种轻便高效的结构化数据存储格式,可以用于结构化数据串行化,或者说序列化. 它很适合做数据存储或 RPC 数据交换格式. 可用于通讯协议、数据存储等领域的语言无关、平台无关、可扩展的序列化结构数据格式. 目前提供了 C++、Java、Python 三种语言的 API.

Oracle 如何强制刷新Buffer Cache

- - 数据库 - ITeye博客
  在Oracle9i里,Oracle提供了一个内部事件,用以强制刷新Buffer Cache,其语法为:.   类似的也可以使用alter system系统级设置:.   在Oracle 10g中,Oracle提供一个新的特性,可以通过如下命令刷新Buffer Cache:.   我们通过试验来看一下刷新Cache的作用:.

数据读取之逻辑读简单解析--关于BUFFER CACHE

- - CSDN博客数据库推荐文章
数据读取之逻辑读简单解析--BUFFER CACHE. 一、实验数据准备--查出一条数据的ROWID,及FILE_ID,BLOCK_ID等信息. 使用下面语句查出相应行的FILE_ID,BLOCK_ID,关于ROWID,详见:http://blog.csdn.net/q947817003/article/details/11490051.

Linux 内存 buffer 和 cache 的区别(转载)

- - 操作系统 - ITeye博客
Free 命令相对于top 提供了更简洁的查看系统内存使用情况:.        在linux的内存分配机制中,优先使用物理内存,当物理内存还有空闲时(还够用),不会释放其占用内存,就算占用内存的程序已经被关闭了,该程序所占用的内存用来做缓存使用,对于开启过的程序、或是读取刚存取过得数据会比较快.        Linux 内存机制.

三段序列化代码的测试:比较protocol buffers的CodedOutputStream和java自带的DataOutputStream

- ndv - snnn的blog
最近一段时间在写一个小东西,一个很简单k-v数据库. 我并没有像MyISAM那样把每个表放在一个单独的文件中,而是用一个总的大文件来放所有的表. (类似于InnoDB默认的方式). 我需要在这个硕大无比的文件的开头放一个map,key是表名,value是这个表的第一个页在此文件中的偏移地址. 即这样一个结构:Map < String, Long > headers.

谷奥: Google = Google+

- 吞佛 - 谷奥聚合——谷奥主站+谷安 aggregator
在上周举办的Google Zeitgeist 2011大会上,John Battelle问Larry Page:在Google大部分的历史里,人们会想到搜索,那么Google品牌=搜索. 但在随后Google的发展史里,Google品牌会等于什么. Larry Page并未直面回答这个问题,至少没有从市场角度来回答.

Google宣布Google CDN

- way - Solidot
Google宣布了最新的帮助加快互联网速度的工具Page Speed Service,加快静态网页的载入速度,不支持动态网页. 在开发者注册该服务之后,可将网站的DNS入口记录指向Google,然后Page Speed Service从服务器上抓取内容,采用最佳的Web性能方案重写网页,通过Google在全球部署的服务器将内容展示给终端用户,加快网页载入速度.

Google将关闭Google Labs

- yifan - Solidot
Google宣布将关闭Google实验室,搜索巨人表示此举将帮助他们将精力集中在优先的产品项目上. Google称,关闭Google实验室意味着大部分试验项目将会被放弃,但不是每一个项目都会被抛弃. Google会将部分试验项目整合到其它产品中. Android应用程序如Google Goggles和Google Listen,则将会继续留在Android Market中.

當Google Docs遇上Google Finances

- 沒有暱稱 - 海芋小站
Google Finances是由Google所推出的一個財經服務,裡面記錄了全球的財經資訊,而如果我們要在Google文件中插入這些財經資訊,如某支股票的收盤價,開盤價等資訊,那要怎麼辦到呢. Google其實提供了非常簡單的函式,怎麼用就往下看啦. 其實在Google文件的試算表中,以插入股票為例,只要輸入「=GoogleFinance("股票代碼.tw"; "參數")」就可以了,以鴻海為例,代碼就是「2317」,記得一定要加變成「2317.tw」才可以.