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

为什么使用 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。关于二进制格式,看这里(可能需要翻墙)。

  • 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

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

(图片转自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置为空:

还原str值,将val置为1:

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

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

序列化性能

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

解析性能

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

Google Protocol Buffer 简单介绍的更多相关文章

  1. 【神经网络与深度学习】Google Protocol Buffer介绍

    简介 什么是 Google Protocol Buffer? 假如您在网上搜索,应该会得到类似这样的文字介绍: Google Protocol Buffer( 简称 Protobuf) 是 Googl ...

  2. Google Protocol Buffer 的使用和原理[转]

    本文转自: http://www.ibm.com/developerworks/cn/linux/l-cn-gpb/ Protocol Buffers 是一种轻便高效的结构化数据存储格式,可以用于结构 ...

  3. Google Protocol Buffer 的使用和原理

    Google Protocol Buffer 的使用和原理 Protocol Buffers 是一种轻便高效的结构化数据存储格式,可以用于结构化数据串行化,很适合做数据存储或 RPC 数据交换格式.它 ...

  4. Google Protocol Buffer 协议

    1. Protocol Buffers 简介 Protocol Buffers (ProtocolBuffer/ protobuf )是Google公司开发的一种数据描述语言,类似于XML能够将结构化 ...

  5. 【Google Protocol Buffer】Google Protocol Buffer

    http://www.ibm.com/developerworks/cn/linux/l-cn-gpb/ Google Protocol Buffer 的使用和原理 Protocol Buffers ...

  6. 前端后台以及游戏中使用Google Protocol Buffer详解

    前端后台以及游戏中使用Google Protocol Buffer详解 0.什么是protoBuf protoBuf是一种灵活高效的独立于语言平台的结构化数据表示方法,与XML相比,protoBuf更 ...

  7. 转Google Protocol Buffer 的使用和原理

    Google Protocol Buffer 的使用和原理 Protocol Buffers 是一种轻便高效的结构化数据存储格式,可以用于结构化数据串行化,很适合做数据存储或 RPC 数据交换格式.它 ...

  8. Google Protocol Buffer 的使用和原理(无论对存储还是数据交换,都是个挺有用的东西,有9张图做说明,十分清楚)

    感觉Google Protocol Buffer无论对存储还是数据交换,都是个挺有用的东西,这里记录下,以后应该用得着.下文转自: http://www.ibm.com/developerworks/ ...

  9. (转)Google Protocol Buffer 的使用和原理

    转自:https://www.ibm.com/developerworks/cn/linux/l-cn-gpb/index.html   简介 什么是 Google Protocol Buffer? ...

随机推荐

  1. codevs 3143 二叉树的序遍历

    传送门 Description 求一棵二叉树的前序遍历,中序遍历和后序遍历 Input 第一行一个整数n,表示这棵树的节点个数. 接下来n行每行2个整数L和R.第i行的两个整数Li和Ri代表编号为i的 ...

  2. 掘金chrome插件

    掘金chrome插件 点击下载 掘金是一个高质量的互联网技术社区,而其提供的一个chrome插件个人觉得非常不错.最终效果如下所示: 每天都会有优秀的内容更新.

  3. VGA 视频输出

    VGA Video Output by Nathan Ickes Introduction VGA is a high-resolution video standard used mostly fo ...

  4. 【原】使用webpack打包的后,公共请求路径的配置问题

    在我们公司,和后台接接口时,公共的请求路径我们是单独抽出来的,放在一个独立的public.js中,在public.js中存入那个公共变量 公共变量是这样 在其他地方使用ajax时,我们就这样使用 这种 ...

  5. js020-JSON

    js020-JSON 20.1 语法 JSON的语法可以表示为一下三种类型的值. 简单值 使用与JS相同的语法,可以在JSON中表示字符串.数值.布尔值和null,但是JSON不支持JS中的特殊性Un ...

  6. Github for Windows使用介绍

    Git已经变得非常流行,连Codeplex现在也已经主推Git.Github上更是充斥着各种高质量的开源项目,比如ruby on rails,cocos2d等等.对于习惯Windows图形界面的程序员 ...

  7. .NET Reflector Visual Studio Extension

    https://visualstudiogallery.msdn.microsoft.com/95789cdb-08f9-4dae-9b2f-fc45a452ad77/

  8. js正则,电话,邮箱

    1. <script type="text/javascript"> var str="Is this all th05777-89856825ere is5 ...

  9. DNS域传送漏洞利用

    DNS区域传送(DNS zone transfer)指的是一台备用服务器使用来自主服务器的数据刷新自己的域(zone)数据库.这为运行中的DNS服务提供了一定的冗余度,其目的是为了防止主的域名服务器因 ...

  10. cookie, localStorage, sessionStorage区别

    cookie 有过期时间,默认是关闭浏览器后失效,4K,兼容ie6,不可跨域,子域名会继承父域名的cookielocalStorage 永不过期,除非手动删除,5M,兼容IE8,不可跨域,子域名不能继 ...