二、protocol buffer二进制协议

一、是什么

  • 是一种灵活,高效,自动化的机制,用于序列化结构化数据 - 想想XML,但更小,更快,更简单。您可以定义数据的结构化时间,然后可以使用特殊生成的源代码轻松地在各种数据流中使用各种语言编写和读取结构化数据。您甚至可以更新数据结构,而不会破坏根据“旧”格式编译的已部署程序
  • 特点:
  • 在谷歌内部长期使用,产品成熟度高;
  • 跨语言、支持多种语言,包括 C++、Java 和 Python
  • 编码后的消息更小,更加有利于存储和传输
  • 编解码的性能非常高
  • 支持不同协议版本的前向兼容
  • 支持定义可选和必选字段

二、如何工作

通过在.proto文件中定义protocol buffer消息类型来指定您希望如何构建序列化信息。每个protocol buffer消息都是一个小的逻辑信息记录,包含一系列名称 - 值对。以下.proto是定义包含有关人员信息的消息的文件的一个非常基本的示例:

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;
}
  • 消息格式很简单 - 每种消息类型都有一个或多个唯一编号的字段,每个字段都有一个名称和一个值类型,其中值类型可以是数字(整数或浮点数),布尔值,字符串,原始字节,甚至(如上例所示)其他protocol buffer消息类型,允许分层次地构建数据。同时可以指定可选字段,必填字段和重复字段。
  • 一旦定义了消息,就可以在.proto文件上运行应用程序语言的protocol buffer编译器来生成数据访问类。并为每个字段(如name()和set_name())提供了简单的访问器,以及将整个结构序列化/解析为原始字节的方法,同时提供了向后兼容性,在消息格式中添加新字段,而不会破坏向后兼容性; 旧的二进制文件在解析时只是忽略新字段。

三、proto3

  • Protocol Buffers语言版本是3(proto3),Proto3简化了协议缓冲区语言,既易于使用,又可以在更广泛的编程语言中使用。但是Proto3和Proto2不完全兼容。因此如果现在使用最好使用Proto3

1、定义消息类型

syntax = "proto3";//明确使用版本proto3,不指定的话默认是proto2
//SearchRequest 定义了三个属性,每一个属性都有名称和类型
message SearchRequest {
  string query = 1;
  int32 page_number = 2;
  int32 result_per_page = 3;
}

2、指定字段类型

  • 在上面的示例中,所有字段都是标量类型:两个整数(page_numberresult_per_page)和一个字符串(query)。但是,您还可以为字段指定复合类型,包括枚举和其他消息类型
    (1)标量值类型(基本数据类型)
    image.png

    image.png

(2)复杂数据结构

  • repeated 关键字,标识该字段可以重复任意次数,等价与数组和列表
  • enum表示枚举
  • map类型:map<key_type,value_type> map_field=N;

3、分配字段编号

  • 消息定义中的每个字段都有唯一的编号。这些字段编号用于以消息二进制格式标识字段,并且在使用消息类型后不应更改。请注意,1到15范围内的字段编号需要一个字节进行编码,包括字段编号和字段类型,16到2047范围内的字段编号占用两个字节。因此,您应该为非常频繁出现的消息元素保留数字1到15。请记住为将来可能添加的常用元素留出一些空间。
  • 可以指定的最小字段数为1,最大字段数为2 29 - 1或536,870,911。但是不能使用不能使用数字19000到19999,因为这些数字是被Protocol Buffers保留的

4、指定字段规则

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

单数:格式良好的消息可以包含该字段中的零个或一个(但不超过一个)。
复数:此字段可以在格式良好的消息中重复任意次数(包括零)。将保留重复值的顺序。

5、添加更多消息类型

  • 可以在单个.proto文件中定义多种消息类型。如果要定义多个相关消息,这很有用 - 例如,如果要定义与SearchResponse消息类型对应的回复消息格式,可以将其添加到相同的消息.proto:
message SearchRequest { 
  string query = 1; 
  int32 page_number = 2; 
  int32 result_per_page = 3; 
} 

message SearchResponse { 
 ... 
}

6、添加注释

  • 采用// 和 /* ... */ 符号进行添加注释,例如
/* SearchRequest represents a search query, with pagination options to
 * indicate which results to include in the response. */

message SearchRequest {
  string query = 1;
  int32 page_number = 2;  // Which page number do we want?
  int32 result_per_page = 3;  // Number of results to return per page.
}

7、导入其他的proto文件

  • 对于一些公共的数据结构,可以单独放在一个proto文件里面,然后通过导入的方式导入到其他的文件中,同时导入也支持级联。
import "/other.proto"

8、编写proto3文件规范

(1)消息(使用驼峰命名)和字段名称(使用下划线)

message SongServerRequest {
  required string song_name = 1;
}

(2)枚举(名称使用驼峰,字段使用大写的下划线)

enum Foo {
  FIRST_VALUE = 0;
  SECOND_VALUE = 1;
}

(3)RPC 服务(服务名称和任何RPC方法名称全部使用驼峰命名)

service FooService {
  rpc GetSomething(FooRequest) returns (FooResponse);
}

四、java语言实战Proto3

  • 需求:创建一个非常简单的“地址簿”应用程序,可以在文件中读取和写入人员的联系人详细信息。地址簿中的每个人都有姓名,ID,电子邮件地址和联系电话号码。(这里使用Proto3序列化和检索这样的结构化数据)

1、定义协议格式(addressbook.proto)

syntax = "proto3";
//1、proto3包名,这有助于防止不同项目之间的命名冲突
package tutorial;
//2、申明java包名
option java_package = "com.example.tutorial";
//3、申明产生的外部java类名,如果不申明将使用驼峰形式产生(AddressBook)
option java_outer_classname = "AddressBookProtos";

message Person {
//4、required 必须提供该字段的值,否则该消息将被视为“未初始化”。
//尝试构建一个未初始化的消息将抛出一个RuntimeException。
//解析未初始化的消息将抛出一个IOException。除此之外,必填字段的行为与可选字段完全相同。
  required string name = 1;
  required int32 id = 2;
//5、optional:该字段可能已设置,也可能未设置。如果未设置可选字段值,则使用默认值。
//对于简单类型,您可以指定自己的默认值,就像我们type在示例中为电话号码所做的那样。
//否则,使用系统默认值:数字类型为0,字符串为空字符串,bools为false。对于嵌入式消息,
//默认值始终是消息的“默认实例”或“原型”,其中没有设置任何字段。调用访问器以获取尚未显
//式设置的可选(或必需)字段的值始终返回该字段的默认值。
  optional string email = 3;

  enum PhoneType {
    MOBILE = 0;
    HOME = 1;
    WORK = 2;
  }

  message PhoneNumber {
    required string number = 1;
    optional PhoneType type = 2 [default = HOME];
  }
//6、repeated:该字段可以重复任意次数(包括零)。重复值的顺序将保
//留在协议缓冲区中。将重复字段视为动态大小的数组。
  repeated PhoneNumber phones = 4;
}

message AddressBook {
  repeated Person people = 1;
}
  • 注意点:

(1)应该非常小心地将字段标记为required。谷歌的一些工程师得出的结论是,使用required弊大于利; 他们更喜欢只使用optional和repeated。但是,这种观点并不普遍。
(2)可以在Protocol Buffer Language Guide中中找到需要的所有格式形式

2、编译Proto3文件

参考网址进行:https://my.oschina.net/u/573325/blog/1617416
(1)proto文件(由于插件的原因上面所讲的optional 和required 属性描述全部会报错误,因此全部删除)

syntax = "proto3";
option java_package = "com.qiu.proto";
option java_outer_classname = "AddressBookProtos";

message Person {
    string name = 1;
    int32 id = 2;
    string email = 3;

    enum PhoneType {
        MOBILE = 0;
        HOME = 1;
        WORK = 2;
    }

    message PhoneNumber {
        string number = 1;
        PhoneType type = 2;
    }
    repeated PhoneNumber phones = 4;
}

message AddressBook {
    repeated Person people = 1;
}

(2)生成java文件解析

  • 每一个meeage都会生成一个Builder类用于生成message类实例,例如message Person会生成一个Person类和Person.builder类。
  • message中的每一个属性都有自己的set和get方法。
  • enum关键字会直接生成对应的一个java枚举类

3、测试

maven添加如下的依赖,用于序列化操作

<dependency>
            <groupId>com.google.protobuf</groupId>
            <artifactId>protobuf-java-util</artifactId>
            <version>3.5.1</version>
        </dependency>

测试类

public class ProtoFileTest {
    public static void main(String[] args) throws InvalidProtocolBufferException {
        //1、构建person类的build
        AddressBookProtos.Person.Builder person= AddressBookProtos.Person.newBuilder();
        person.setEmail("2222@a");
        person.setId(1);
        person.setName("test");
        //2、构建phonenumer内部类的build
        AddressBookProtos.Person.PhoneNumber.Builder phoneNumer= AddressBookProtos.Person.PhoneNumber.newBuilder();
        phoneNumer.setNumber("123456789");
        phoneNumer.setType(AddressBookProtos.Person.PhoneType.MOBILE);
        AddressBookProtos.Person.PhoneNumber.Builder phoneNumer2= AddressBookProtos.Person.PhoneNumber.newBuilder();
        phoneNumer2.setNumber("987654321");
        phoneNumer2.setType(AddressBookProtos.Person.PhoneType.MOBILE);
        //person类添加phonenumbers列表
        person.addPhones(phoneNumer);
        person.addPhones(phoneNumer2);
    //创建person类
        AddressBookProtos.Person build = person.build();
        byte[] bytes = build.toByteArray();
        AddressBookProtos.Person addressBook1 = AddressBookProtos.Person.parseFrom(bytes);

        List<AddressBookProtos.Person.PhoneNumber> phonesList = person.getPhonesList();
        phonesList.forEach(p->{
            System.out.println("type:"+p.getType()+" number:"+p.getNumber());
        });
        //进行person json序列化
        String personJsonString = JsonFormat.printer().print(person);
        System.out.println("person序列化:"+personJsonString);
        //3、构建AddressBook通讯录
        AddressBookProtos.AddressBook.Builder addressBook= AddressBookProtos.AddressBook.newBuilder();
        addressBook.addPeople(person);
        //进行地址序列化
        String addressJsonStr = JsonFormat.printer().print(addressBook);
        System.out.println("通讯录序列化:"+addressJsonStr);

    }
}

测试结果:

type:MOBILE number:123456789
type:MOBILE number:987654321
person序列化:{
  "name": "test",
  "id": 1,
  "email": "2222@a",
  "phones": [{
    "number": "123456789"
  }, {
    "number": "987654321"
  }]
}
通讯录序列化:{
  "people": [{
    "name": "test",
    "id": 1,
    "email": "2222@a",
    "phones": [{
      "number": "123456789"
    }, {
      "number": "987654321"
    }]
  }]
}

4、总结

(1)使用步骤总结

  • IDL 文件定义(*.proto), 包含数据结构定义
  • 各种语言的代码生成(含数据结构定义、以及序列化和反序列化接口)
  • 使用 Protocol Buffers 的 API 进行序列化和反序列化

(2)使用protoBuffer原生api总结

  • 构建对象(构造器模式):
AddressBookProtos.Person person1= AddressBookProtos.Person.newBuilder()
                                                .setId(1).setName("test")
                                                .build();
  • 序列化,转为字节数组或者输出流
toByteArray()
toByteString()
writeTo()
....
  • 反序列化,转为原始对象或者类(使用parseFrom方法)
AddressBookProtos.Person build = person.build();
byte[] bytes = build.toByteArray();
AddressBookProtos.Person addressBook1 = AddressBookProtos.Person.parseFrom(bytes);

(3)使用第三方工具进行序列化和反序列化(比如netty)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 200,612评论 5 471
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,345评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 147,625评论 0 332
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,022评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,974评论 5 360
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,227评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,688评论 3 392
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,358评论 0 255
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,490评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,402评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,446评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,126评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,721评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,802评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,013评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,504评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,080评论 2 341

推荐阅读更多精彩内容