文章目录

  • 01protobuf基础
  • protobuf概述
  • message
  • 定义message结构
  • 保留Filed和保留Filed number
  • 枚举类型
  • 引用其它message类
  • message扩展
  • 数据类型对应关系
  • 编码规则
  • 可变长整数编码
  • 有符号整数编码
  • 定长编码
  • 代码生成
  • 下载安装protobuf
  • 生成代码
  • 方法1:使用cmd
  • 方法2:使用java调用cmd
  • 使用pom生成java类
  • 编译生成Java类
  • 使用
  • 引入protobuf
  • 使用builder
  • idea使用protobuf
  • 添加protobuf支持


01protobuf基础


protobuf概述

protobuf是google团队开发的用于高效存储和读取结构化数据的工具。什么是结构化数据呢,正如字面上表达的,就是带有一定结构的数据。比如电话簿上有很多记录数据,每条记录包含姓名、ID、邮件、电话等,这种结构重复出现。

xml、json也可以用来存储此类结构化数据,但是使用protobuf表示的数据能更加高效,并且将数据压缩得更小,大约是json格式的1/10,xml格式的1/20。

它是一种轻便高效的数据格式,平台无关、语言无关、可扩展,可用于通讯协议和数据存储等领域。

  • 优点
  • 平台无关,语言无关,可扩展;
  • 提供了友好的动态库,使用简单;
  • 解析速度快,比对应的XML快约20-100倍;
  • 序列化数据非常简洁、紧凑,与XML相比,其序列化之后的数据量约为1/3到1/10。
  • 缺点
  • 不适合用于对基于文本的标记文档(如HTML)建模,因为文本不适合描述数据结构
  • 通用性较差:Json, XML已经成为多种行业标准的编写工具,而Protobuf只是Google公司内部使用的工具
  • 自解释性差:以二进制数据流方式存储(不可读) ,需要通过.proto文件才能了解到数据结构

message

定义message结构

protobuf使用message,类似class文件,

例如:

message Person {
    // ID(必需)
    required int32 id = 1;[default = 0]
    // 姓名(必需)
    required string name = 2;
    // email(可选)
    optional string email = 3;[default = ""]
    // 朋友(集合)
    repeated string friends = 4 [packed=true];
}

其中Person是message这种结构的名称,name、id、email是其中的Field,每个Field保存着一种数据类型,后面的1、2、3是Filed对应的数字id。id在115之间编码只需要占一个字节,包括Filed数据类型和Filed对应数字id,在162047之间编码需要占两个字节,所以最常用的数据对应id要尽量小一些。

optional后面可以加default默认值,如果不加,数据类型的默认为0[default = 0],字符串类型的默认为空串[default = “”]。

repeated后面加[packed=true]会使用新的更高效的编码方式。

注意:使用required规则的时候要谨慎,因为以后结构若发生更改,这个Filed若被删除的话将可能导致兼容性的问题。

syntax = "proto3";
 
package net.cc.luffy.entity.proto;//指定java的包名,生成java之后的包路径
//option java_package = "net.cc.luffy.entity.proto"; \\指定java的报名
option java_outer_classname = "UpDownProto";//指定java的编译前类名,生成java之后,java文件交
 
// 起降记录
message UpDown {
 
    // 起降记录ID
    fixed64 id = 1;
    // 设备ID
    string deviceId = 2;
    // 用户ID
    fixed64 usrId = 3;
    // 厂商ID
    string mid = 4;
    // 起飞时间
    fixed64 upTime = 5;
    // 降落时间
    fixed64 downTime = 6;
    // 飞行状态
    int32 flyStatus = 7;
    // 是否删除
    bool isDelete = 8;
    // 日志跟踪ID
    string traceId = 9;
    // 创建时间
    fixed64 createdate = 10;
    // 平均速度
    double avgSpeed = 11;
    // 平均高度
    double avgHeight = 12;
    // 最大速度
    double maxSpeed = 13;
    // 最大高度
    double maxHeight = 14;
    // 最小速度
    double minSpeed = 15;
    // 最小高度
    double minHeight = 16;
    // 开关机记录ID
    fixed64 onOffId = 17;
}
保留Filed和保留Filed number

每个Filed对应唯一的数字id,但是如果该结构在之后的版本中某个Filed删除了,为了保持向前兼容性,需要将一些id或名称设置为保留的,即不能被用来定义新的Field。
实质:已经定义的数字id和字段为了兼容性,之后不能重复使用,若是对应的字段删除,就记录下来,以后不再使用

message Person {
  reserved 2, 15, 9 to 11;//排除的id
  reserved "samples", "email";//排除的字段
}
枚举类型

比如电话号码,只有移动电话、家庭电话、工作电话三种,因此枚举作为选项,如果没设置的话枚举类型的默认值为第一项。
  在上面的例子中在个人message中加入电话号码这个Filed。如果枚举类型中有不同的名字对应相同的数字id,需要加入option allow_alias = true这一项,否则会报错。枚举类型中也有reserverd Filed和number,定义和message中一样。

message Person {
    //普通参数
  required string name = 1;
  required int32 id = 2;
  optional string email = 3;
//枚举--手机号类型(移动电话、家庭电话、工作电话)
  enum PhoneType {
    //allow_alias = true;
    MOBILE = 0;
    HOME = 1;
    WORK = 2;
  }
  //定义手机号信息
  message PhoneNumber {
    required string number = 1;
    optional PhoneType type = 2 [default = HOME];//默认是家庭电话
  }
  //手机号字段
  repeated PhoneNumber phones = 4;
}
引用其它message类
  • 在同一个文件中,可以直接引用定义过的message类型。
  • 在一个message类型中嵌套定义其它的message类型
  • 在同一个项目中,可以用import来导入其它message类型。
import "myproject/other_protos.proto";
message扩展
message Person {
  // ...
  extensions 100 to 199;//扩展允许的id范围
}

在另一个文件中,import 这个proto之后,可以对Person这个message进行扩展。

extend Person {
  optional int32 bar = 126;
}
数据类型对应关系

在使用规则创建proto类型的数据结构文件之后,会将其转化成对应编程语言中的头文件或者类定义。

proto中的数据类型和c++,Python中的数据类型对应规则如下:

.proto

C++

Python

java

介绍

double

double

float

double

float

float

float

float

int32

int32

int

int

使用可变长编码方式。编码负数时不够高效——如果你的字段可能含有负数,那么请使用sint32。

int64

int64

int/long

long

使用可变长编码方式。编码负数时不够高效——如果你的字段可能含有负数,那么请使用sint64。

uint32

uint32

int/long

int

Uses variable-length encoding.

uint64

uint64

int/long

long

Uses variable-length encoding.

sint32

int32

int

int

使用可变长编码方式。有符号的整型值。编码时比通常的int32高效。

sint64

int64

int/long

long

使用可变长编码方式。有符号的整型值。编码时比通常的int64高效。

fixed32

uint32

int/long

int

总是4个字节。如果数值总是比总是比228大的话,这个类型会比uint32高效。

fixed64

uint64

int/long

long

总是8个字节。如果数值总是比总是比256大的话,这个类型会比uint64高效。.

sfixed32

int32

int

int

总是4个字节。

sfixed64

int64

int/long

long

总是8个字节。

bool

bool

bool

boolean

string

string

str/unicode

String

一个字符串必须是UTF-8编码或者7-bit ASCII编码的文本。

bytes

string

str

ByteString

可能包含任意顺序的字节数据。

编码规则

protobuf有一套高效的数据编码规则。

可变长整数编码

每个字节有8bits,其中第一个bit是most significant bit(msb),0表示结束,1表示还要读接下来的字节。

对message中每个Filed来说,需要编码它的数据类型、对应id以及具体数据。

数据类型有以下6种,可以用3个bits表示。每个整数编码用最后3个bits表示数据类型。所以,对应id在1~15之间的Filed,可以用1个字节编码数据类型、对应id。

Type

Meaning

Used For

0

Varint

int32, int64, uint32, uint64, sint32, sint64, bool, enum

1

64-bit

fixed64, sfixed64, double

2

Length-delimited

string, bytes, embedded messages, packed repeated

fields

3

Start group

groups (deprecated)

4

End group

groups (deprecated)

5

32-bit

fixed32, sfixed32, float

比如对于下面这个例子来说,如果给a赋值150,那么最终得到的编码是什么呢?

message Test {
   optional int32 a = 1;
}

首先数据类型编码是000,因此和id联合起来的编码是00001000. 然后值150的编码是1 0010110,采用小端序交换位置,即0010110 0000001,前面补1后面补0,即10010110 00000001,即96 01,加上最前面的数据类型编码字节,总的编码为08 96 01。

有符号整数编码

如果用int32来保存一个负数,结果总是有10个字节长度,被看做是一个非常大的无符号整数。使用有符号类型会更高效。它使用一种ZigZag的方式进行编码。即-1编码成1,1编码成2,-2编码成3这种形式。
  也就是说,对于sint32来说,n编码成 (n << 1) ^ (n >> 31),注意到第二个移位是算法移位。

定长编码

定长编码是比较简单的情况。

代码生成

下载安装protobuf
  • 在https://github.com/protocolbuffers/protobuf/releases 下载合适的版本
    例如,我下载了protoc-3.9.0-win32.zip
  • 将解压出来的protoc.exe放在一全英文路径下,并把其路径名放在windows环境变量下的path下,同时添加proto_path,值为protoc.exe的路径
生成代码
方法1:使用cmd

在所使用的proto文件路径下打开cmd窗口执行以下命令

protoc -I=源地址 --java_out=目标地址  源地址/xxx.proto

注:此处生成时会以proto里面注明的java_package为路径完整生成,所以目标地址不必包含java_package及之后的路径,比如: option java_package = "com.test.protocol";那么就会生成com/test/protocol/XXX.java

  • -I选项,主要用于指定待编译的.proto消息定义文件所在的目录,即可能出现的包含文件的路径,该选项可以被同时指定多个。此处指定的路径不能为空,如果是当前目录,直接使用.,如果是子目录,直接使用子目录相对径,如:foo/bar/baz,如果要编译的文件import指定的文件路径为baz/test.proto,那么应这么写-I=foo/bar,而不要一直写到baz。
    示例命令如下:
protoc -I=. --java_out=../../../../ beans/*.proto apis/*.proto *.proto
方法2:使用java调用cmd
/**
* protoc.exe
* @author ganhaibin
*
*/
public class GenerateClass {
    public static void main(String[] args) {
        String protoFile = "person-entity.proto";// 
        String strCmd = "d:/dev/protobuf-master/src/protoc.exe -I=./proto --java_out=./src/main/java ./proto/"+ protoFile; 
        try {
            Runtime.getRuntime().exec(strCmd);
        } catch (IOException e) {
            e.printStackTrace();
        }//通过执行cmd命令调用protoc.exe程序 
    }
}
使用pom生成java类
<!--版本-->
<properties>
    <grpc.version>1.6.1</grpc.version>
    <protobuf.version>3.3.0</protobuf.version>
</properties>
 
<!--依赖-->
  <dependencies>
         <dependency>
            <groupId>io.grpc</groupId>
            <artifactId>grpc-netty</artifactId>
            <version>${grpc.version}</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>io.grpc</groupId>
            <artifactId>grpc-protobuf</artifactId>
            <version>${grpc.version}</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>io.grpc</groupId>
            <artifactId>grpc-stub</artifactId>
            <version>${grpc.version}</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>com.google.protobuf</groupId>
            <artifactId>protobuf-java</artifactId>
            <version>${protobuf.version}</version>
        </dependency>
</dependencies>
<!--build配置-->
<build>
        <extensions>
            <extension>
                <groupId>kr.motd.maven</groupId>
                <artifactId>os-maven-plugin</artifactId>
                <version>1.5.0.Final</version>
            </extension>
        </extensions>
        <plugins>
            <plugin>
                <groupId>org.xolstice.maven.plugins</groupId>
                <artifactId>protobuf-maven-plugin</artifactId>
                <version>0.5.0</version>
                <configuration>
                    <protocArtifact>com.google.protobuf:protoc:${protobuf.version}:exe:${os.detected.classifier}</protocArtifact>
                    <pluginId>grpc-java</pluginId>
                    <pluginArtifact>io.grpc:protoc-gen-grpc-java:${grpc.version}:exe:${os.detected.classifier}</pluginArtifact>
                </configuration>
                <executions>
                    <execution>
                        <goals>
                            <goal>compile</goal>
                            <goal>compile-custom</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>           
        </plugins>
    </build>
编译生成Java类
  • src/main/proto文件夹中添加protobuf的文件;
  • 右键proto,将文件夹设置为源码目录
  • 点击maven projectsPluginsprotobufprotobuf:compile
  • 即可在target/generated-sources/protobuf中看到根据.proto文件生成的Java类 ;

使用

引入protobuf
  • maven项目,引入pom依赖
<dependency>
    <groupId>com.google.protobuf</groupId>
    <artifactId>protobuf-java</artifactId>
    <version>2.5.0</version>
</dependency>
  • 普通项目需要引入protobuf-java-2.5.0.jar文件
使用builder
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.List;
 
public class Main {
    public static void main(String[] args) throws IOException {
        // 按照定义的数据结构,创建一个Person
        PersonMsg.Person.Builder personBuilder = PersonMsg.Person.newBuilder();
        personBuilder.setId(1);
        personBuilder.setName("叉叉哥");
        personBuilder.setEmail("xxg@163.com");
        personBuilder.addFriends("Friend A");
        personBuilder.addFriends("Friend B");
        PersonMsg.Person xxg = personBuilder.build();
 
        // 将数据写到输出流,如网络输出流,这里就用ByteArrayOutputStream来代替
        ByteArrayOutputStream output = new ByteArrayOutputStream();
        xxg.writeTo(output);
 
        // -------------- 分割线:上面是发送方,将数据序列化后发送 ---------------
 
        byte[] byteArray = output.toByteArray();
 
        // -------------- 分割线:下面是接收方,将数据接收后反序列化 ---------------
 
        // 接收到流并读取,如网络输入流,这里用ByteArrayInputStream来代替
        ByteArrayInputStream input = new ByteArrayInputStream(byteArray);
 
        // 反序列化
        PersonMsg.Person xxg2 = PersonMsg.Person.parseFrom(input);
        System.out.println("ID:" + xxg2.getId());
        System.out.println("name:" + xxg2.getName());
        System.out.println("email:" + xxg2.getEmail());
        System.out.println("friend:");
        List<String> friends = xxg2.getFriendsList();
        for(String friend : friends) {
            System.out.println(friend);
        }
    }
}
也可以:
序列化:byte[] bytes=personBuilder.build().toByteArray();
反序列化:PersonMsg.Person person=PersonMsg.Person.parseFrom(bytes);
    可以直接  person.getId();等操作

注意:protobuf不是json,若是使用json解析会抛出异常

idea使用protobuf

添加protobuf支持

安装插件Protobuf Support