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

为什么使用 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. C/C++ 程序的build过程

    (This article is under constant construction) DISCLAIMER: 本文的主要内容来自https://gcc.gnu.org/onlinedocs/gc ...

  2. CF 208A Dubstep(简单字符串处理)

    题目链接: 传送门 Dubstep Time Limit: 1000MS     Memory Limit: 32768 KB Description Vasya works as a DJ in t ...

  3. 感受身边app

    第一款:高考小秘书.http://www.liqucn.com/rj/519571.shtml.下载链接.我认为产品最大的优势在于对高考生非常有利,实用价值大,对于每年的高考生来说,高考资讯和大学资讯 ...

  4. ubuntu下的ssh

    ubuntu默认是没有安装openssh-server的,今天简单的写一下ubuntu上ssh的安装与使用. 一.服务器端 安装 apt-get install openssh-server #安装 ...

  5. POJ2635The Embarrassed Cryptographer(大数取余+素数筛选+好题)

    题目链接 题意:K是由两个素数乘积,如果最小的素数小于L,输出BAD最小的素数,否则输出GOOD 分析 素数打表将 L 大点的素数打出来,一定要比L大,然后就开始枚举,只需K对 素数 取余 看看是否为 ...

  6. centos 搭建git服务器

    centos 6搭建git服务器 安装 rpm -ivh http://mirrors.aliyun.com/epel/epel-release-latest-6.noarch.rpm yum ins ...

  7. java编程思想-java中的并发(三)

    三.终结任务 1. 在阻塞时终结 线程状态 一个线程可以处于以下四种状态之一: 1)新建(new):当线程被创建时,他只会短暂的处于这种状态.此时,他已经分配了必须的系统资源,并执行了初始化.此刻线程 ...

  8. JQuery遍历方法$.each输出函数

    each()方法能使DOM循环结构简洁,不容易出错.each()函数封装了十分强大的遍历功能,使用也很方便,它可以遍历一维数组.多维数组.DOM, JSON 等等在javaScript开发过程中使用$ ...

  9. htons

    在Windows和Linux网络编程时需要用到的,用来将主机字节顺序转化为网络字节顺序,以Windows下的代码为例: 1 2 #include<winsock2.h> u_shortht ...

  10. easyUI 树的上下文菜单

    一.属性:onContextMenu onContextMenu: function(e,node){ e.preventDefault(); $(this).tree('select',node.t ...