Probotbuf简介

在网络通信和通用数据交换等应用场景中经常使用的技术是 JSON 或 XML,这两种技术常被用于数据的结构化呈现和序列化。我们可以从两个方面来看JSON 和 XML与protobuf的异同:一个是数据结构化,一个是数据序列化。这里的数据结构化主要面向开发或业务层面,数据序列化面向通信或存储层面,当然数据序列化也需要“结构”和“格式”,所以这两者之间的区别主要在于面向领域和场景不同,一般要求和侧重点也会有所不同。数据结构化侧重人类可读性甚至有时会强调语义表达能力,而数据序列化侧重效率和压缩。

JSON、XML 同样也可以直接被用来数据序列化,实际上很多时候它们也是这么被使用的,例如直接采用 JSON、XML 进行网络通信传输,此时 JSON、XML 就成了一种序列化格式,它发挥了数据序列化的能力。但是经常这么被使用,不代表这么做就是合理。实际将 JSON、XML 直接作用数据序列化通常并不是最优选择,因为它们在速度、效率、空间上并不是最优。换句话说它们更适合数据结构化而非数据序列化。

扯完 XML 和 JSON,我们来看看 ProtoBuf,同样的 ProtoBuf 也具有数据结构化的能力,其实也就是上面介绍的 message 定义。我们能够在 .proto 文件中,通过 message、import、内嵌 message 等语法来实现数据结构化,但是很容易能够看出,ProtoBuf 在数据结构化方面和 XML、JSON 相差较大,人类可读性较差,不适合上面提到的 XML、JSON 的一些应用场景。

但是如果从数据序列化的角度你会发现 ProtoBuf 有着明显的优势,效率、速度、空间几乎全面占优,看完后面的 ProtoBuf 编码的文章,你更会了解 ProtoBuf 是如何极尽所能的压榨每一寸空间和性能,而其中的编码原理正是 ProtoBuf 的关键所在,message 的表达能力并不是 ProtoBuf 最关键的重点。所以可以看出ProtoBuf重点侧重于数据序列化而非数据结构化。

最终对这些个人思考做一些小小的总结:

XML、JSON、ProtoBuf 都具有数据结构化和数据序列化的能力

XML、JSON 更注重数据结构化,关注人类可读性和语义表达能力。ProtoBuf 更注重数据序列化,关注效率、空间、速度,人类可读性差,语义表达能力不足(为保证极致的效率,会舍弃一部分元信息)

ProtoBuf 的应用场景更为明确,XML、JSON 的应用场景更为丰富。

我们先来看看官方文档给出的定义和描述:

protocol buffers 是一种语言无关、平台无关、可扩展的序列化结构数据的方法,它可用于(数据)通信协议、数据存储等。

Protocol Buffers 是一种灵活,高效,自动化机制的结构数据序列化方法-可类比 XML,但是比 XML 更小(3 ~ 10倍)、更快(20 ~ 100倍)、更为简单。

你可以定义数据的结构,然后使用特殊生成的源代码轻松的在各种数据流中使用各种语言进行编写和读取结构数据。你甚至可以更新数据结构,而不破坏由旧数据结构编译的已部署程序。

简单来讲, ProtoBuf 是结构数据序列化[1] 方法,可简单类比于 XML[2],其具有以下特点:

语言无关、平台无关。即 ProtoBuf 支持 Java、C++、Python 等多种语言,支持多个平台

高效。即比 XML 更小(3 ~ 10倍)、更快(20 ~ 100倍)、更为简单

扩展性、兼容性好。你可以更新数据结构,而不影响和破坏原有的旧程序

protobuf3的变化

默认值

protobuf3 删除了 protobuf2 中用来设置默认值的 default 关键字,取而代之的是protobuf3为各类型定义的默认值,也就是约定的默认值,如下表所示:

类型

默认值

bool

false

整形

0

string

空字符串 ""

enum

第一个枚举元素的值,因为Protobuf3强制要求第一个枚举元素的值必须是0,所以枚举的默认值就是0

message

不是null,而是DEFAULT_INSTANCE

可以看出来,protobuf3定义的默认值跟Java中类的属性的默认值规则并不一样:Java中,如果类的属性类型是类,则该属性默认值是null,而protobuf3中,string、message的默认值都不是null。

枚举类型

不支持一个proto文件中,多个枚举中定义相同的枚举常量名

如下的两个枚举,定义在同一个proto文件中:

enum Enum1 {

IDLE = 0;

RUNNING = 1;

}

enum Enum2 {

IDLE = 5;

RUNNING = 6;

}

编译时,会报出错误:IDLE is already defined in "xxx",出现这一错误的原因就是:Protobuf3中不允许同一proto中,多个枚举中使用相同的枚举值。

枚举第一个常量的值必须是0

message类型

Java中,message类型的默认值是DEFAULT_INSTANCE,其值相当于空的message,即XXX.newBuilder().build(),这样对message类型的判空操作就应该是这样:

// protobuf message

message User {

int32 id = 1;

string name = 2;

string email = 3;

Address address = 4;

}

message Address {

string street = 1;

string building = 2;

}

// Java

if (user.getAddress() != null && user.getAddress() != UserProto.Address.getDefaultInstance()) {

...

} else {

...

}

Protobuf数据类型

基础类型

proto type

描述

java type

double

双精度

double

float

单精度

float

int32

32位整数,可变长度,编码负数效率低,编码负数推荐使用sint32

int

int64

64位整数,可变长度,编码负数效率低,编码负数推荐使用sint64

long

uint32

32位无符号整数,存储正数时与int32一致,用的较少,一般用int32,长度可变

int

uint64

64位无符号整数,存储正数与int64一致,用的较少,一般用int64,长度可变

long

sint32

有符号32位,长度可变,编码负数效率高

int

sint64

有符号64位,长度可变,编码负数效率高,存储正数时不推荐使用

long

fixed32

固定4个字节的长度,比uint32更有效率,存储正数时不推荐使用

int

fixed64

固定8个字节的长度,比uint64更有效率

long

sfixed32

有符号整数,固定4个字节

int

sfixed64

有符号整数,固定8个字节

long

bool

布尔值

boolean

string

字符串

String

bytes

字节数组

ByteString

枚举类型

枚举类型中必须包含至少一个元素,并且元素的编号必须从0开始。因为如果没有设置值的话,可以使用0作为默认值。

定义消息体

syntax = "proto3";

option java_package = "com.ray.protobufdemo";

option java_outer_classname = "StudentProto3";

message Student {

string username = 1;

string password = 2;

string email = 3;

sint32 age = 4;

int64 timeSpane = 5;

double value = 6;

Address address = 7;

enum Gender {

MALE = 0;

FEMALE = 1;

}

Gender gender = 8;

}

message Address {

string province = 1;

string city = 2;

// 相当于java中的List

repeated string area = 3;

}

测试demo

StudentProto3.Student studentProto = StudentProto3.Student.newBuilder()

.setUsername("admin")

.setPassword("123456")

.setEmail("3306@qq.com")

.setValue(Double.MAX_VALUE)

// .setAge(Integer.MAX_VALUE)

.setAge(-2)

.setTimeSpane(System.currentTimeMillis())

.setAddress(address)

.setGender(StudentProto3.Student.Gender.MALE)

.build();

嵌套消息类型

Student.proto

syntax = "proto3";

option java_package = "com.ray.protobufdemo";

option java_outer_classname = "StudentProto3";

message Student {

string username = 1;

string password = 2;

string email = 3;

sint32 age = 4;

int64 timeSpane = 5;

double value = 6;

Address address = 7;

}

message Address {

string province = 1;

string city = 2;

// 相当于java中的List

repeated string area = 3;

}

StudentPtoto3Demo.java

// 使用protobuf3序列化

StudentProto3.Address address = StudentProto3.Address.newBuilder()

.setProvince("北京")

.setCity("北京")

.addArea("chaoyang")

.addArea("miyun")

.build();

StudentProto3.Student studentProto = StudentProto3.Student.newBuilder()

.setUsername("admin")

.setPassword("123456")

.setEmail("3306@qq.com")

.setValue(Double.MAX_VALUE)

// .setAge(Integer.MAX_VALUE)

.setAge(-2)

.setTimeSpane(System.currentTimeMillis())

.setAddress(address)

.build();

System.out.println(studentProto);

System.out.println(studentProto.toByteArray().length);

repeated类型

repeated相当于java中的List类型,在其内部定义的类型可以是任意的。

reserved类型

当定义文件中的一些字段需要移除,最好不要直接删除,而是使用reserved标记要删除的字段,如果有人使用了被标记删除的字段,编译器会报错。有两种标记删除方式:

根据字段顺序标记

message Demo {

reserved 2, 5, 9 to 11 // 字段顺序为2、5,以及9到11的标记为删除

}

根据字段名称标记

message Demo {

reserved "name", "age" // 字段名称为name和age的被标记为删除

}

Map类型

在ProtoBuf中可以定义Map类型,语法如下:

map map_field = N;

key_type可以是其他的Message类型,string类型,PB类型定义表中(scalar value type)除了浮点类型和byte字节类型以外的其他类型。

需要注意以下几点:

枚举类型不能够作为key_type,value_type可以是除了Map以外的其他任何类型

map不能定义为repeated类型

map不保证顺序

定义proto文件

syntax = "proto3";

option java_package = "com.ray.protobufdemo";

option java_outer_classname = "MapProto3";

message MapPerson {

map projects = 1;

}

message Project {

string name = 1;

int32 age = 2;

}

测试demo

public class MapProtoDemo {

public static void main(String[] args) {

MapProto3.Project p1 = MapProto3.Project.newBuilder()

.setName("neo")

.setAge(22)

.build();

MapProto3.Project p2 = MapProto3.Project.newBuilder()

.setName("mary")

.setAge(33)

.build();

MapProto3.MapPerson student = MapProto3.MapPerson.newBuilder()

.putProjects("student", p1)

.putProjects("teacher", p2)

.build();

System.out.println(student);

}

}

oneof类型

oneof关键字内部可以定义多个field,在使用的时候只能设置一个值。

定义消息体

syntax = "proto3";

option java_package = "com.ray.protobufdemo";

option java_outer_classname = "OneOfProto3";

message Test1 {

string name = 1;

int32 age = 2;

oneof test_oneof {

Request req = 3;

Response rep = 4;

}

}

message Request {

string req = 1;

}

message Response {

string rep = 1;

}

测试demo

public class OneOfProtoDemo {

public static void main(String[] args) throws InvalidProtocolBufferException, Descriptors.DescriptorValidationException {

OneOfProto3.Request request = OneOfProto3.Request.newBuilder()

.setReq("request")

.build();

OneOfProto3.Response response = OneOfProto3.Response.newBuilder()

.setRsp("response")

.build();

OneOfProto3.Test1 neo = OneOfProto3.Test1.newBuilder()

.setName("neo")

.setAge(22)

// rep和rsp只能设置其中的一个

// .setReq(request)

.setRsp(response)

.build();

System.out.println(neo);

OneOfProto3.Test1 test1 = OneOfProto3.Test1.parseFrom(neo.toByteArray());

}

}

package包定义

Package包定义,可以防止Message重名问题,类似于java中的包。在java中使用Protobuf的包,有以下两种方式:

使用package关键字定义protobuf的模板消息包名,这种方式可以在多个语言中使用

syntax = "proto3";

package bar.foo;

option java_outer_classname = "OneOfProto3";

message PackageProto {

string name = 1;

int32 age = 2;

}

我们在使用的时候需要如下做:

package com.ray.protobufdemo.entity;

// 导入package处声明的包

import bar.foo.OneOfProto3;

public class PackageProtoDemo {

public static void main(String[] args) {

OneOfProto3.PackageProto.Builder builder = OneOfProto3.PackageProto.newBuilder();

}

}

使用option java_package语句声明java的包名,该用法是java独有的,如果不定义则使用package中的包路径

option java_package= "com.ray.protobufdemo";

import语法

在Protobuf中,不同的消息可以分别写在不同的proto文件中,在使用的时候可以使用关键字import引用其他消息模板。

protobuf的使用

定义消息的格式

// 声明使用proto3协议,如果不指定则默认使用proto2协议

syntax = "proto3";

option java_package = "com.ray.protobufdemo";

option java_outer_classname = "AddressBook";

message Person {

string name = 1;

int32 id = 2;

string email = 3;

enum PhoneType {

// 在proto3中,第一个枚举值的序号必须为0

MOBILE = 0;

HOME = 1;

WORK = 2;

}

message PhoneNumber {

string number = 1;

PhoneType type = 2;

}

repeated PhoneNumber phone = 4;

}

message AddressBook {

// 在AddressBook message中引用另一个message Person

repeated Person person = 1;

}

protobuf消息格式说明:Person消息定义指定了三个字段(名称/值对),每一个字段对应于要包含在这种类型的消息中的数据。每个字段都有一个名称和一个类型,以及一个序号。

指定字段类型

在上例中,所有字段都是标量类型:两个整数(page_number和result_per_page)和一个字符串(query)。但是,您也可以为字段指定复合类型,包括枚举和其他消息类型。

分配字段编号

如您所见,消息定义中的每个字段都有一个唯一的编号。这些字段编号用于以二进制格式标识您的字段,一旦您的消息类型被使用,就不应该被更改。请注意,1到15范围内的字段编号需要一个字节来编码,包括字段编号和字段类型(您可以在协议缓冲区编码中找到更多信息)。16到2047范围内的字段编号需要两个字节。因此,您应该为经常出现的消息元素保留数字1到15。记住为将来可能添加的频繁出现的元素留出一些空间。

那protobuf是怎么做到向前及向后兼容的呢?靠的就是这个字段的编号,在反序列化的时候,protobuf会从输入流中读取出字段编号,然后再设置message中对应的值。如果读出来的字段编号是message中没有的,就直接忽略,如果message中有字段编号是输入流中没有的,则该字段不会被设置。所以即使通信的两端存在一方比另一方多出编号,也不会影响反序列化。但是如果两端同一编号的字段规则或者字段类型不一样,那就肯定会影响反序列化了。所以一般调整proto文件的时候,尽量选择加字段或者删字段,而不是修改字段编号或者字段类型。

您可以指定的最小字段编号为1,最大字段编号为229 - 1,即536,870,911。但是不能使用数字19000到19999 ( FieldDescriptor::kFirstReservedNumber 到FieldDescriptor::kLastReservedNumber),因为它们是为协议缓冲区实现而保留的-如果您在 .proto文件中使用这些保留的数字之一,协议缓冲区编译器就会报错。同样,您也不能使用任何保留字段。

指定字段规则

消息字段可以是以下字段之一:

singular: 可以有零个或其中一个字段(但不超过一个)。

repeated: 该字段可以重复任意次数(包括零次)。重复值的顺序将保留在Protocol Buffer中,将重复字段视为动态大小的数组。protobuf处理这个字段的时候,另外加了一个count计数变量,用于标明这个字段有多少个,这样发送方发送的时候,同时发送了count计数变量和这个字段的起始地址,接收方在接受到数据之后,按照count来解析对应的数据即可。

在java中使用protobuf3

安装idea插件

添加pom依赖

com.google.protobuf

protobuf-java

3.4.0

kr.motd.maven

os-maven-plugin

1.6.2

org.xolstice.maven.plugins

protobuf-maven-plugin

0.5.0

${project.basedir}/src/main/protobuf

com.google.protobuf:protoc:3.1.0:exe:${os.detected.classifier}

compile

定义message

// user.proto

// 定义protobuf

syntax = "proto3";

option java_package = "com.ray.bigdata.protobuf";

// 指定生成的java类名

option java_outer_classname = "DemoModel";

message User {

int32 id = 1;

string name = 2;

string sex = 3;

}

测试demo

package com.ray.bigdata.canal;

import com.google.protobuf.InvalidProtocolBufferException;

import com.ray.bigdata.protobuf.DemoModel;

/**

* 使用protobuf进行数据的序列化和反序列化

*/

public class ProtobufDemo {

public static void main(String[] args) throws InvalidProtocolBufferException {

// 实例化protobuf对象

DemoModel.User.Builder builder = DemoModel.User.newBuilder();

// 给user对象进行赋值

builder.setId(1);

builder.setName("张三");

builder.setSex("男");

// 获取user对象的属性值

DemoModel.User userBuilder = builder.build();

System.out.println(userBuilder.getId());

System.out.println(userBuilder.getName());

System.out.println(userBuilder.getSex());

/**

* 数据的序列化和反序列化

* 序列化:可以将对象转换成字节码数据存储到kafka中

* 反序列化:可以将kafka中的数据消费出来,转换为java对象使用

*/

// 将一个对象序列化成二进制的字节码数据存储到kafka中

byte[] bytes = builder.build().toByteArray();

for (byte b: bytes) {

System.out.println(b);

}

// 将kafka中消费的数据反序列化

DemoModel.User user = DemoModel.User.parseFrom(bytes);

System.out.println(user);

System.out.println(user.getName());

System.out.println(user.getSex());

}

}

probuffer java_Protocol Buffer的使用相关推荐

  1. protocol buffer java_Protocol Buffer Java实例

    大家要先下载protobuffer,在这里: 注意,需要下载两个,一个是complier,另外一个是source code (我下载的是2.5的版本); 讲complier对应的 protoc.exe ...

  2. [WARNING] unable to add QUERY_STRING=XXXX to uwsgi packet, consider increasing buffer size

    1. 问题现象 在用 flask uwsgi api 报文发送 GET 请求时,请求参数的 value 值 "XXX" 太长时,会报下面的错误. [WARNING] unable ...

  3. Java NIO中的Buffer

    简介 Buffer缓冲区,首先要弄明白的是,缓冲区是怎样一个概念.它其实是缓存的一种,我们常说的缓存,包括保存在硬盘上的浏览器缓存,保存在内存中的缓存(比如Redis.memcached).Buffe ...

  4. Buffer的工作方式

    1.Buffer的工作方式 前面<java NIO的工作方式>介绍了Selector检测到通信信道I/O有数据传输时,通过select()方法取得SocketChannel,将数据读取或写 ...

  5. TCP性能和发送接收Buffer的关系

    本文希望解析清楚,当我们在代码中写下 socket.setSendBufferSize 和 sysctl 看到的rmem/wmem系统参数以及最终我们在TCP常常谈到的接收发送窗口的关系,以及他们怎样 ...

  6. Linux操作系统中内存buffer和cache的区别

    我们一开始,先从Free命令说起. free 命令相对于top 提供了更简洁的查看系统内存使用情况: $ free                      total  used   free  s ...

  7. 【C++】Google Protocol Buffer(protobuf)详解(一)

    1.简介 Google Protocol Buffer( 简称 Protobuf) 是 Google 公司内部的混合语言数据标准, Protocol Buffers 是一种轻便高效的结构化数据存储格式 ...

  8. Golang中Buffer高效拼接字符串以及自定义线程安全Buffer

    本文原创文章,转载注明出处,博客地址 https://segmentfault.com/u/to... 第一时间看后续精彩文章.觉得好的话,顺手分享到朋友圈吧,感谢支持. Go中可以使用"+ ...

  9. Sep 26 09:22:41 ck01 kernel: Buffer I/O error on device sda2, logical block 2

    错误 kernel: sd 0:2:0:0: SCSI error: return code kernel: end_request: I/O error, dev sda, sector 23085 ...

最新文章

  1. R语言应用calibrate包的textxy函数向R原生绘图结果中添加文本标签:添加多个文本标签、改变文本标签的字体、改变文本标签的字体颜色
  2. SAP MM盘点流程里如何处理事务代码MI11 Recount过的盘点凭证?
  3. odbc里面没有Microsoft Access Driver(*.mdb)问题解决
  4. java基础面试题:java中实现多态的机制是什么?
  5. 基于Docker + Consul + Nginx + Consul-template的服务负载均衡实现
  6. 前仓后仓是什么意思_高支纱到底是什么?镰仓衬衫面料全解析
  7. POSIX信号量API函数
  8. 服务器按ctrl alt delete没有用_详细教程——用PS制作直邮广告
  9. InfoPath读取数据库
  10. Java中的Thread.sleep()– Java线程睡眠
  11. 梅花易数C语言实现(六十四卦卦辞用的是python)用了python
  12. 数学建模优化和仿真模拟的区别001
  13. 滑铁卢大学开发了一套AI工具,教泥瓦匠初学者搬砖诀窍
  14. docker搭建企业级habor仓库
  15. 【宇麦科技】腾xun云登场,群晖NAS自定义域名教程来啦~
  16. DSG-01-3C4-A110-51T油研液压直动式电磁阀
  17. ug10.0许可证服务器失败,ug10.0许可错误
  18. 【进制转换】负进制转换 多进制转换
  19. 如何在行中统计满足条件的数据个数占比
  20. 指纹辨识传感器解决方案

热门文章

  1. RequireJS使用注意地方
  2. 浏览器如何生成URL
  3. bootstrap-select控件全选,全不选,查询功能实现
  4. WAP自助建站 我编程之路的启蒙
  5. HDU - 1024 Max Sum Plus Plus 最大m段子段和+滚动数组优化
  6. welcome to my blog
  7. 关于编写流程的一些经验
  8. springboot干什么的_Spring Boot 项目的这些文件都是干啥用的?
  9. java中u怎么用_Java中interrupt的使用
  10. python3连接数据库失败_python3使用pymysql连接mysql数据库报Keyerror