当前位置: 首页 > news >正文

ProtoBuf3语法详解

目录:

  1. 需求:
  2. 字段规则
  3. 消息类型的定义与使用
  4. 通讯录2.0的写⼊实现
  5. TestRead.java(通讯录2.0)
  6. TestRead.java(通讯录2.0) 另⼀种验证⽅法--toString()
  7. enum类型
  8. 升级通讯录⾄2.1版本
  9. Any类型
  10. oneof类型
  11. map类型
  12. 默认值
  13. 更新消息
  14. 保留字段reserved
  15. 未知字段
  16. 选项option 
  17. 通讯录4.0实现---⽹络版
  18. 序列化能⼒对⽐验证
  19. 总结:

1.需求:

  • 不再打印联系⼈的序列化结果,⽽是将通讯录序列化后并写⼊⽂件中。
  • 从⽂件中将通讯录解析出来,并进⾏打印。
  • 新增联系⼈属性,共包括:姓名、年龄、电话信息、地址、其他联系⽅式、备注。
     

2.字段规则

消息的字段可以⽤下⾯⼏种规则来修饰:

  • singular:消息中可以包含该字段零次或⼀次(不超过⼀次)。proto3语法中,字段默认使⽤该规则。
  • repeated:消息中可以包含该字段任意多次(包括零次),其中重复值的顺序会被保留。可以理解为定义了⼀个数组。

 我们在 src/main/proto/proto3 ⽬录下新建 contacts.proto ⽂件,内容如下:

syntax = "proto3";
package start;option java_multiple_files = true; // 编译后⽣成的⽂件是否分为多个⽂件
option java_package = "com.example.start"; // 编译后⽣成⽂件所在的包路径
option java_outer_classname = "ContactsProtos"; // 编译后⽣成的proto包装类的类名message PeopleInfo{string name = 1;int32 age = 2;repeated string phone_numbers = 3;
}
  • PeopleInfo 消息中新增phone_numbers 字段,表⽰⼀个联系⼈有多个号码,所以将其设置为repeated。

3.消息类型的定义与使⽤

定义:

  • 在单个.proto⽂件中可以定义多个消息体,且⽀持定义嵌套类型的消息(任意多层)。每个消息体中的字段编号可以重复。
  • 更新contacts.proto,我们可以将phone_number提取出来,单独成为⼀个消息:

使⽤

  • 消息类型可作为字段类型使⽤

contacts.proto


 

  • 可导⼊其他.proto⽂件的消息并使⽤

例如Phone消息定义在phone.proto⽂件中:

syntax = "proto3";
package phone;option java_multiple_files = true; // 编译后⽣成的⽂件是否分为多个⽂件
option java_package = "com.example.start"; // 编译后⽣成⽂件所在的包路径
option java_outer_classname = "PhoneProtos"; // 编译后⽣成的proto包装类的类名message Phone{string number = 1;
}

contacts.proto

syntax = "proto3";
package start;option java_multiple_files = true; // 编译后⽣成的⽂件是否分为多个⽂件
option java_package = "com.example.start"; // 编译后⽣成⽂件所在的包路径
option java_outer_classname = "ContactsProtos"; // 编译后⽣成的proto包装类的类名import "start/phone.proto";message PeopleInfo{string name = 1;int32 age = 2;repeated  phone.Phone phone = 3;
}

运行结果:

3.创建通讯录2.0版本

通讯录2.x的需求是向⽂件中写⼊通讯录列表,以上我们只是定义了⼀个联系⼈的消息,并不能存放通讯录列表,所以还需要在完善⼀下contacts.proto(终版通讯录2.0):

syntax = "proto3";
package start;option java_multiple_files = true; // 编译后⽣成的⽂件是否分为多个⽂件
option java_package = "com.example.start"; // 编译后⽣成⽂件所在的包路径
option java_outer_classname = "ContactsProtos"; // 编译后⽣成的proto包装类的类名message PeopleInfo{string name = 1;int32 age = 2;message Phone{string number = 1;}repeated Phone phone = 3;}message Contacts{repeated PeopleInfo contacts = 1;
}

接着使⽤maven插件进⾏⼀次编译,这次编译会多⽣成五个⽂件: Contacts.java 
ContactsOrBuilder.java  ContactsProtos.java  PeopleInfo.java PeopleInfoOrBuilder.java 。
可以看出由于我们设置了option java_multiple_files = true; ,会给⽣成的每个⾃定义
message 类都⽣成两个对应的⽂件:。

在message 类中,主要包含:

  • 获取字段值的get⽅法,⽽没有set⽅法。
  • 序列化(在MessageLite中定义)和反序列化⽅法。
  • newBuilder()静态⽅法:⽤来创建Builder。

在 Builder 类中,主要包含:

  • 包含⼀个build()⽅法:主要是⽤来构造出⼀个⾃定义类对象。
  • 编译器为每个字段提供了获取和设置⽅法,以及能够操作字段的⼀些⽅法。

且在上述的例⼦中:

  • 对于builder,每个字段都有⼀个clear_⽅法,可以将字段重新设置回empty状态。
  • mergeFrom(Message other):合并other的内容到这个message中,如果是单数域则覆盖,如果是重复值则追加连接。
  • 对于使⽤repeated修饰的字段,也就是数组类型,pb为我们提供了⼀系列add⽅法来新增⼀个值或⼀个builder,并且提供了getXXXCount()⽅法来获取数组存放元素的个数。

4.通讯录2.0的写⼊实现

TestWrite.java(通讯录2.0)

package testcode;import com.example.start.Contacts;
import com.example.start.PeopleInfo;import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Scanner;public class TestWrite {public static void main(String[] args) throws IOException {Contacts.Builder contactsBuilder = Contacts.newBuilder();// 读取已存在的contactstry {contactsBuilder.mergeFrom(newFileInputStream("src/main/java/com/example/start/contacts.bin"));} catch (FileNotFoundException e) {System.out.println("contacts.bin not found. Creating a new file.");}// 新增⼀个联系⼈contactsBuilder.addContacts(addPeopleInfo());// 将新的contacts写回磁盘FileOutputStream output = new FileOutputStream("src/main/java/com/example/start/contacts.bin");contactsBuilder.build().writeTo(output);output.close();}private static PeopleInfo addPeopleInfo() {Scanner scan = new Scanner(System.in);PeopleInfo.Builder peopleBuilder = PeopleInfo.newBuilder();System.out.println("-------------新增联系⼈-------------");System.out.print("请输⼊联系⼈姓名: ");String name = scan.nextLine();peopleBuilder.setName(name);System.out.print("请输⼊联系⼈年龄: ");int age = scan.nextInt();peopleBuilder.setAge(age);scan.nextLine();for (int i = 0; ; i++) {System.out.print("请输⼊联系⼈电话" + (i + 1) + "(只输⼊回⻋完成电话新 增): ");String number = scan.nextLine();if (number.isEmpty()) {break;}PeopleInfo.Phone.Builder phoneBuilder = PeopleInfo.Phone.newBuilder();phoneBuilder.setNumber(number);peopleBuilder.addPhone(phoneBuilder);}System.out.println("-------------添加联系⼈成功-------------");return peopleBuilder.build();}
}

运行结果:

5.TestRead.java(通讯录2.0)

package testcode;import com.example.start.Contacts;
import com.example.start.PeopleInfo;import java.io.FileInputStream;
import java.io.IOException;public class TestRead {public static void main(String[] args) throws IOException {// 从磁盘⽂件⾥读取,并反序列化为 Message 实例Contacts contacts = Contacts.parseFrom(new FileInputStream("src/main/java/com/example/start/contacts.bin"));// 打印printContacts(contacts);}private static void printContacts(Contacts contacts) {for (int i = 0; i < contacts.getContactsCount(); i++) {System.out.println("--------------联系⼈" + (i + 1) + "-----------");PeopleInfo peopleInfo = contacts.getContacts(i);System.out.println("姓名: " + peopleInfo.getName());System.out.println("年龄: " + peopleInfo.getAge());int j = 1;for (PeopleInfo.Phone phone : peopleInfo.getPhoneList()) {System.out.println("电话" + (j++) + ": " + phone.getNumber());}}}
}

运行结果:

 6.TestRead.java(通讯录2.0) 另⼀种验证⽅法--toString()

在⾃定义消息类的⽗抽象类AbstractMessage中,重写了toString()⽅法。该⽅法返回的内容是⼈类可读的,对于调试特别有⽤。例如在TestRead类的main函数中调⽤⼀下:

package testcode;import com.example.start.Contacts;
import com.example.start.PeopleInfo;import java.io.FileInputStream;
import java.io.IOException;public class TestRead {public static void main(String[] args) throws IOException {// 从磁盘⽂件⾥读取,并反序列化为 Message 实例Contacts contacts = Contacts.parseFrom(new FileInputStream("src/main/java/com/example/start/contacts.bin"));// 打印
//        printContacts(contacts);System.out.println(contacts.toString());}
//
//    private static void printContacts(Contacts contacts) {
//        for (int i = 0; i < contacts.getContactsCount(); i++) {
//            System.out.println("--------------联系⼈" + (i + 1) + "-----------");
//            PeopleInfo peopleInfo = contacts.getContacts(i);
//            System.out.println("姓名: " + peopleInfo.getName());
//            System.out.println("年龄: " + peopleInfo.getAge());
//            int j = 1;
//            for (PeopleInfo.Phone phone : peopleInfo.getPhoneList()) {
//                System.out.println("电话" + (j++) + ": " + phone.getNumber());
//            }
//        }
//    }
}

运行结果:在这⾥是将utf-8汉字转为⼋进制格式输出了

7. enum类型

定义规则

语法⽀持我们定义枚举类型并使⽤。在.proto⽂件中枚举类型的书写规范为:

枚举类型名称:
使⽤驼峰命名法,⾸字⺟⼤写。例如: MyEnum 
常量值名称:
全⼤写字⺟,多个字⺟之间⽤ _ 连接。例如: ENUM_CONST = 0; 

我们可以定义⼀个名为PhoneType的枚举类型,定义如下:

enum PhoneType {

        MP = 0;          //移动电话

        TEL = l;          //固定电话

}

要注意枚举类型的定义有以下⼏种规则:

  1. 0值常量必须存在,且要作为第⼀个元素。这是为了与proto2的语义兼容:第⼀个元素作为默认值,且值为0。
  2. 枚举类型可以在消息外定义,也可以在消息体内定义(嵌套)。
  3. 枚举的常量值在32位整数的范围内。但因负值⽆效因⽽不建议使⽤(与编码规则有关)。

定义时注意

将两个具有相同枚举值名称的枚举类型放在单个.proto⽂件下测试时,编译后会报错:某某某常

量已经被定义!所以这⾥要注意:

  • 同级(同层)的枚举类型,各个枚举类型中的常量不能重名。
  • 单个.proto⽂件下,最外层枚举类型和嵌套枚举类型,不算同级。
  • 多个.proto⽂件下,若⼀个⽂件引⼊了其他⽂件,且每个⽂件都未声明package,每个proto⽂
  • 件中的枚举类型都在最外层,算同级。
  • 多个.proto⽂件下,若⼀个⽂件引⼊了其他⽂件,且每个⽂件都声明了package,不算同级。
     

 

8.升级通讯录⾄2.1版本

更新contacts.proto(通讯录2.1),新增枚举字段并使⽤,更新内容如下:

syntax = "proto3";
package start;option java_multiple_files = true; // 编译后⽣成的⽂件是否分为多个⽂件
option java_package = "com.example.start"; // 编译后⽣成⽂件所在的包路径
option java_outer_classname = "ContactsProtos"; // 编译后⽣成的proto包装类的类名message PeopleInfo{string name = 1;int32 age = 2;message Phone {string number = 1; // 电话号码enum PhoneType {MP = 0; // 移动电话TEL = 1; // 固定电话}PhoneType type = 2; // 类型}repeated Phone phone = 3; // 电话}message Contacts{repeated PeopleInfo contacts = 1;
}

接着使⽤maven插件进⾏⼀次编译。


更新TestWrite.java(通讯录2.1)

package testcode;import com.example.start.Contacts;
import com.example.start.PeopleInfo;import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Scanner;public class TestWrite {public static void main(String[] args) throws IOException {Contacts.Builder contactsBuilder = Contacts.newBuilder();// 读取已存在的contactstry {contactsBuilder.mergeFrom(newFileInputStream("src/main/java/com/example/start/contacts.bin"));} catch (FileNotFoundException e) {System.out.println("contacts.bin not found. Creating a new file.");}// 新增⼀个联系⼈contactsBuilder.addContacts(addPeopleInfo());// 将新的contacts写回磁盘FileOutputStream output = new FileOutputStream("src/main/java/com/example/start/contacts.bin");contactsBuilder.build().writeTo(output);output.close();}private static PeopleInfo addPeopleInfo() {Scanner scan = new Scanner(System.in);PeopleInfo.Builder peopleBuilder = PeopleInfo.newBuilder();System.out.println("-------------新增联系⼈-------------");System.out.print("请输⼊联系⼈姓名: ");String name = scan.nextLine();peopleBuilder.setName(name);System.out.print("请输⼊联系⼈年龄: ");int age = scan.nextInt();peopleBuilder.setAge(age);scan.nextLine();for (int i = 0; ; i++) {System.out.print("请输⼊联系⼈电话" + (i + 1) + "(只输⼊回⻋完成电话新 增): ");String number = scan.nextLine();if (number.isEmpty()) {break;}PeopleInfo.Phone.Builder phoneBuilder = PeopleInfo.Phone.newBuilder();phoneBuilder.setNumber(number);System.out.print("选择此电话类型 (1、移动电话 2、固定电话) : ");int type = scan.nextInt();scan.nextLine();switch (type) {case 1:phoneBuilder.setType(PeopleInfo.Phone.PhoneType.MP);break;case 2:phoneBuilder.setType(PeopleInfo.Phone.PhoneType.TEL);break;default:System.out.println("⾮法选择,使⽤默认值!");break;}peopleBuilder.addPhone(phoneBuilder);}System.out.println("-------------添加联系⼈成功-------------");return peopleBuilder.build();}
}

运行结果:

更新TestRead.java(通讯录2.1)

package testcode;import com.example.start.Contacts;
import com.example.start.PeopleInfo;import java.io.FileInputStream;
import java.io.IOException;public class TestRead {public static void main(String[] args) throws IOException {// 从磁盘⽂件⾥读取,并反序列化为 Message 实例Contacts contacts = Contacts.parseFrom(new FileInputStream("src/main/java/com/example/start/contacts.bin"));// 打印printContacts(contacts);//System.out.println(contacts.toString());}private static void printContacts(Contacts contacts) {for (int i = 0; i < contacts.getContactsCount(); i++) {System.out.println("--------------联系⼈" + (i + 1) + "-----------");PeopleInfo peopleInfo = contacts.getContacts(i);System.out.println("姓名: " + peopleInfo.getName());System.out.println("年龄: " + peopleInfo.getAge());int j = 1;for (PeopleInfo.Phone phone : peopleInfo.getPhoneList()) {System.out.println("电话" + (j++) + ": " + phone.getNumber() + " (" + phone.getType().name() + ")");}}}
}

运行结果:


 

9.Any类型

字段还可以声明为Any类型,可以理解为泛型类型。使⽤时可以在Any中存储任意消息类型。Any类型的字段也⽤repeated来修饰。Any类型是google已经帮我们定义好的类型,在装ProtoBu时,其中的include⽬录下查找所有google已经定义好的.proto⽂件。
 

升级通讯录⾄2.2版本

通讯录2.2版本会新增联系⼈的地址信息,我们可以使⽤any类型的字段来存储地址信息。
更新contacts.proto(通讯录2.2),更新内容如下:

contacts.proto

syntax = "proto3";
package start;option java_multiple_files = true; // 编译后⽣成的⽂件是否分为多个⽂件
option java_package = "com.example.start"; // 编译后⽣成⽂件所在的包路径
option java_outer_classname = "ContactsProtos"; // 编译后⽣成的proto包装类的类名import "google/protobuf/any.proto";message PeopleInfo{string name = 1;int32 age = 2;message Phone {string number = 1; // 电话号码enum PhoneType {MP = 0; // 移动电话TEL = 1; // 固定电话}PhoneType type = 2; // 类型}repeated Phone phone = 3; // 电话google.protobuf.Any data = 4;
}message Contacts{repeated PeopleInfo contacts = 1;
}message Address{string home_address = 1;string unit_address = 2;
}

TestWrite.java

package testcode;import com.example.start.Address;
import com.example.start.Contacts;
import com.example.start.PeopleInfo;
import com.google.protobuf.Any;import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Scanner;public class TestWrite {public static void main(String[] args) throws IOException {Contacts.Builder contactsBuilder = Contacts.newBuilder();// 读取已存在的contactstry {contactsBuilder.mergeFrom(newFileInputStream("src/main/java/com/example/start/contacts.bin"));} catch (FileNotFoundException e) {System.out.println("contacts.bin not found. Creating a new file.");}// 新增⼀个联系⼈contactsBuilder.addContacts(addPeopleInfo());// 将新的contacts写回磁盘FileOutputStream output = new FileOutputStream("src/main/java/com/example/start/contacts.bin");contactsBuilder.build().writeTo(output);output.close();}private static PeopleInfo addPeopleInfo() {Scanner scan = new Scanner(System.in);PeopleInfo.Builder peopleBuilder = PeopleInfo.newBuilder();System.out.println("-------------新增联系⼈-------------");System.out.print("请输⼊联系⼈姓名: ");String name = scan.nextLine();peopleBuilder.setName(name);System.out.print("请输⼊联系⼈年龄: ");int age = scan.nextInt();peopleBuilder.setAge(age);scan.nextLine();for (int i = 0; ; i++) {System.out.print("请输⼊联系⼈电话" + (i + 1) + "(只输⼊回⻋完成电话新 增): ");String number = scan.nextLine();if (number.isEmpty()) {break;}PeopleInfo.Phone.Builder phoneBuilder = PeopleInfo.Phone.newBuilder();phoneBuilder.setNumber(number);System.out.print("选择此电话类型 (1、移动电话 2、固定电话) : ");int type = scan.nextInt();scan.nextLine();switch (type) {case 1:phoneBuilder.setType(PeopleInfo.Phone.PhoneType.MP);break;case 2:phoneBuilder.setType(PeopleInfo.Phone.PhoneType.TEL);break;default:System.out.println("⾮法选择,使⽤默认值!");break;}peopleBuilder.addPhone(phoneBuilder);}Address.Builder addressBulider = Address.newBuilder();System.out.println("请输入联系人家庭地址:");String homeAddress = scan.nextLine();addressBulider.setHomeAddress(homeAddress);System.out.println("请输入联系人单位地址");String unitAddress = scan.nextLine();addressBulider.setUnitAddress(unitAddress);peopleBuilder.setData(Any.pack(addressBulider.build()));System.out.println("-------------添加联系⼈成功-------------");return peopleBuilder.build();}
}

 TestRead.java

package testcode;import com.example.start.Address;
import com.example.start.Contacts;
import com.example.start.PeopleInfo;
import com.google.protobuf.InvalidProtocolBufferException;import java.io.FileInputStream;
import java.io.IOException;public class TestRead {public static void main(String[] args) throws IOException {// 从磁盘⽂件⾥读取,并反序列化为 Message 实例Contacts contacts = Contacts.parseFrom(new FileInputStream("src/main/java/com/example/start/contacts.bin"));// 打印printContacts(contacts);//System.out.println(contacts.toString());}private static void printContacts(Contacts contacts) throws InvalidProtocolBufferException {for (int i = 0; i < contacts.getContactsCount(); i++) {System.out.println("--------------联系⼈" + (i + 1) + "-----------");PeopleInfo peopleInfo = contacts.getContacts(i);System.out.println("姓名: " + peopleInfo.getName());System.out.println("年龄: " + peopleInfo.getAge());int j = 1;for (PeopleInfo.Phone phone : peopleInfo.getPhoneList()) {System.out.println("电话" + (j++) + ": " + phone.getNumber() + " (" + phone.getType().name() + ")");}if (peopleInfo.hasData() && peopleInfo.getData().is(Address.class)) {Address address = peopleInfo.getData().unpack(Address.class);if (!address.getHomeAddress().isEmpty()) {System.out.println("家庭地址:" + address.getHomeAddress());}if (!address.getUnitAddress().isEmpty()) {System.out.println("单位地址:" + address.getUnitAddress());}}}}
}

运行结果:

10.oneof类型

如果消息中有很多可选字段,并且将来同时只有⼀个字段会被设置,那么就可以使⽤ oneof 加强这个⾏为,也能有节约内存的效果。

升级通讯录⾄2.3版本

通讯录2.3版本想新增联系⼈的其他联系⽅式,⽐如qq或者微信号⼆选⼀,我们就可以使⽤oneof字
段来加强多选⼀这个⾏为。oneof字段定义的格式为: oneof 字段名 { 字段1; 字段2; ... } 更新contacts.proto(通讯录2.3),更新内容如下:

syntax = "proto3";
package start;option java_multiple_files = true; // 编译后⽣成的⽂件是否分为多个⽂件
option java_package = "com.example.start"; // 编译后⽣成⽂件所在的包路径
option java_outer_classname = "ContactsProtos"; // 编译后⽣成的proto包装类的类名import "google/protobuf/any.proto";message PeopleInfo{string name = 1;int32 age = 2;message Phone {string number = 1; // 电话号码enum PhoneType {MP = 0; // 移动电话TEL = 1; // 固定电话}PhoneType type = 2; // 类型}repeated Phone phone = 3; // 电话google.protobuf.Any data = 4;oneof other_contact{string qq = 5;string wechat = 6;}
}message Contacts{repeated PeopleInfo contacts = 1;
}message Address{string home_address = 1;string unit_address = 2;
}

TestWrite.java

package testcode;import com.example.start.Address;
import com.example.start.Contacts;
import com.example.start.PeopleInfo;
import com.google.protobuf.Any;import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Scanner;public class TestWrite {public static void main(String[] args) throws IOException {Contacts.Builder contactsBuilder = Contacts.newBuilder();// 读取已存在的contactstry {contactsBuilder.mergeFrom(newFileInputStream("src/main/java/com/example/start/contacts.bin"));} catch (FileNotFoundException e) {System.out.println("contacts.bin not found. Creating a new file.");}// 新增⼀个联系⼈contactsBuilder.addContacts(addPeopleInfo());// 将新的contacts写回磁盘FileOutputStream output = new FileOutputStream("src/main/java/com/example/start/contacts.bin");contactsBuilder.build().writeTo(output);output.close();}private static PeopleInfo addPeopleInfo() {Scanner scan = new Scanner(System.in);PeopleInfo.Builder peopleBuilder = PeopleInfo.newBuilder();System.out.println("-------------新增联系⼈-------------");System.out.print("请输⼊联系⼈姓名: ");String name = scan.nextLine();peopleBuilder.setName(name);System.out.print("请输⼊联系⼈年龄: ");int age = scan.nextInt();peopleBuilder.setAge(age);scan.nextLine();for (int i = 0; ; i++) {System.out.print("请输⼊联系⼈电话" + (i + 1) + "(只输⼊回⻋完成电话新 增): ");String number = scan.nextLine();if (number.isEmpty()) {break;}PeopleInfo.Phone.Builder phoneBuilder = PeopleInfo.Phone.newBuilder();phoneBuilder.setNumber(number);System.out.print("选择此电话类型 (1、移动电话 2、固定电话) : ");int type = scan.nextInt();scan.nextLine();switch (type) {case 1:phoneBuilder.setType(PeopleInfo.Phone.PhoneType.MP);break;case 2:phoneBuilder.setType(PeopleInfo.Phone.PhoneType.TEL);break;default:System.out.println("⾮法选择,使⽤默认值!");break;}peopleBuilder.addPhone(phoneBuilder);}Address.Builder addressBulider = Address.newBuilder();System.out.println("请输入联系人家庭地址:");String homeAddress = scan.nextLine();addressBulider.setHomeAddress(homeAddress);System.out.println("请输入联系人单位地址");String unitAddress = scan.nextLine();addressBulider.setUnitAddress(unitAddress);peopleBuilder.setData(Any.pack(addressBulider.build()));System.out.println("请选择要添加的其他联系方式(1.qq号 2.微信号):");int otherContact = scan.nextInt();scan.nextLine();if (1 == otherContact) {System.out.println("请输入qq号:");String qq = scan.nextLine();peopleBuilder.setQq(qq);} else if (2 == otherContact) {System.out.println("请输入微信号");String wechat = scan.nextLine();peopleBuilder.setWechat(wechat);} else {System.out.println("无效选择,设置失败!");}System.out.println("-------------添加联系⼈成功-------------");return peopleBuilder.build();}
}


运行结果:

 TestRead.java

package testcode;import com.example.start.Address;
import com.example.start.Contacts;
import com.example.start.PeopleInfo;
import com.google.protobuf.InvalidProtocolBufferException;import java.io.FileInputStream;
import java.io.IOException;public class TestRead {public static void main(String[] args) throws IOException {// 从磁盘⽂件⾥读取,并反序列化为 Message 实例Contacts contacts = Contacts.parseFrom(new FileInputStream("src/main/java/com/example/start/contacts.bin"));// 打印printContacts(contacts);//System.out.println(contacts.toString());}private static void printContacts(Contacts contacts) throws InvalidProtocolBufferException {for (int i = 0; i < contacts.getContactsCount(); i++) {System.out.println("--------------联系⼈" + (i + 1) + "-----------");PeopleInfo peopleInfo = contacts.getContacts(i);System.out.println("姓名: " + peopleInfo.getName());System.out.println("年龄: " + peopleInfo.getAge());int j = 1;for (PeopleInfo.Phone phone : peopleInfo.getPhoneList()) {System.out.println("电话" + (j++) + ": " + phone.getNumber() + " (" + phone.getType().name() + ")");}if (peopleInfo.hasData() && peopleInfo.getData().is(Address.class)) {Address address = peopleInfo.getData().unpack(Address.class);if (!address.getHomeAddress().isEmpty()) {System.out.println("家庭地址:" + address.getHomeAddress());}if (!address.getUnitAddress().isEmpty()) {System.out.println("单位地址:" + address.getUnitAddress());}}switch (peopleInfo.getOtherContactCase()) {case QQ:System.out.println("qq号:" + peopleInfo.getQq());break;case WECHAT:System.out.println("微信号:" + peopleInfo.getWechat());break;case OTHERCONTACT_NOT_SET:break;}}}
}

运行结果:

11.map类型

语法⽀持创建⼀个关联映射字段,也就是可以使⽤map类型去声明字段类型,格式为:
map<key_type, value_type> map_field = N;
要注意的是:

  • key_type 是除了float和bytes类型以外的任意标量类型。 value_type 可以是任意类型。
  • map字段不可以⽤repeated修饰
  • map中存⼊的元素是⽆序的

升级通讯录⾄2.4版本

最后,通讯录2.4版本想新增联系⼈的备注信息,我们可以使⽤map类型的字段来存储备注信息。
更新contacts.proto(通讯录2.4),更新内容如下:

contacts.proto

syntax = "proto3";
package start;option java_multiple_files = true; // 编译后⽣成的⽂件是否分为多个⽂件
option java_package = "com.example.start"; // 编译后⽣成⽂件所在的包路径
option java_outer_classname = "ContactsProtos"; // 编译后⽣成的proto包装类的类名import "google/protobuf/any.proto";message PeopleInfo{string name = 1;int32 age = 2;message Phone {string number = 1; // 电话号码enum PhoneType {MP = 0; // 移动电话TEL = 1; // 固定电话}PhoneType type = 2; // 类型}repeated Phone phone = 3; // 电话google.protobuf.Any data = 4;oneof other_contact{string qq = 5;string wechat = 6;}map<string, string> remark = 7;}message Contacts{repeated PeopleInfo contacts = 1;
}message Address{string home_address = 1;string unit_address = 2;
}

TestWrite.java

package testcode;import com.example.start.Address;
import com.example.start.Contacts;
import com.example.start.PeopleInfo;
import com.google.protobuf.Any;import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Scanner;public class TestWrite {public static void main(String[] args) throws IOException {Contacts.Builder contactsBuilder = Contacts.newBuilder();// 读取已存在的contactstry {contactsBuilder.mergeFrom(newFileInputStream("src/main/java/com/example/start/contacts.bin"));} catch (FileNotFoundException e) {System.out.println("contacts.bin not found. Creating a new file.");}// 新增⼀个联系⼈contactsBuilder.addContacts(addPeopleInfo());// 将新的contacts写回磁盘FileOutputStream output = new FileOutputStream("src/main/java/com/example/start/contacts.bin");contactsBuilder.build().writeTo(output);output.close();}private static PeopleInfo addPeopleInfo() {Scanner scan = new Scanner(System.in);PeopleInfo.Builder peopleBuilder = PeopleInfo.newBuilder();System.out.println("-------------新增联系⼈-------------");System.out.print("请输⼊联系⼈姓名: ");String name = scan.nextLine();peopleBuilder.setName(name);System.out.print("请输⼊联系⼈年龄: ");int age = scan.nextInt();peopleBuilder.setAge(age);scan.nextLine();for (int i = 0; ; i++) {System.out.print("请输⼊联系⼈电话" + (i + 1) + "(只输⼊回⻋完成电话新 增): ");String number = scan.nextLine();if (number.isEmpty()) {break;}PeopleInfo.Phone.Builder phoneBuilder = PeopleInfo.Phone.newBuilder();phoneBuilder.setNumber(number);System.out.print("选择此电话类型 (1、移动电话 2、固定电话) : ");int type = scan.nextInt();scan.nextLine();switch (type) {case 1:phoneBuilder.setType(PeopleInfo.Phone.PhoneType.MP);break;case 2:phoneBuilder.setType(PeopleInfo.Phone.PhoneType.TEL);break;default:System.out.println("⾮法选择,使⽤默认值!");break;}peopleBuilder.addPhone(phoneBuilder);}Address.Builder addressBulider = Address.newBuilder();System.out.println("请输入联系人家庭地址:");String homeAddress = scan.nextLine();addressBulider.setHomeAddress(homeAddress);System.out.println("请输入联系人单位地址");String unitAddress = scan.nextLine();addressBulider.setUnitAddress(unitAddress);peopleBuilder.setData(Any.pack(addressBulider.build()));System.out.println("请选择要添加的其他联系方式(1.qq号 2.微信号):");int otherContact = scan.nextInt();scan.nextLine();if (1 == otherContact) {System.out.println("请输入qq号:");String qq = scan.nextLine();peopleBuilder.setQq(qq);} else if (2 == otherContact) {System.out.println("请输入微信号");String wechat = scan.nextLine();peopleBuilder.setWechat(wechat);} else {System.out.println("无效选择,设置失败!");}for (int i = 0; ; i++) {System.out.println("请输入备注:" + (i + 1) + "标题(只输入回车完成备注新增):");String key = scan.nextLine();if (key.isEmpty()) {break;}System.out.println("请输入备注内容:");String value = scan.nextLine();peopleBuilder.putRemark(key, value);}System.out.println("-------------添加联系⼈成功-------------");return peopleBuilder.build();}
}

运行结果:



TestRead.java

package testcode;import com.example.start.Address;
import com.example.start.Contacts;
import com.example.start.PeopleInfo;
import com.google.protobuf.InvalidProtocolBufferException;import java.io.FileInputStream;
import java.io.IOException;
import java.util.Map;public class TestRead {public static void main(String[] args) throws IOException {// 从磁盘⽂件⾥读取,并反序列化为 Message 实例Contacts contacts = Contacts.parseFrom(new FileInputStream("src/main/java/com/example/start/contacts.bin"));// 打印printContacts(contacts);//System.out.println(contacts.toString());}private static void printContacts(Contacts contacts) throws InvalidProtocolBufferException {for (int i = 0; i < contacts.getContactsCount(); i++) {System.out.println("--------------联系⼈" + (i + 1) + "-----------");PeopleInfo peopleInfo = contacts.getContacts(i);System.out.println("姓名: " + peopleInfo.getName());System.out.println("年龄: " + peopleInfo.getAge());int j = 1;for (PeopleInfo.Phone phone : peopleInfo.getPhoneList()) {System.out.println("电话" + (j++) + ": " + phone.getNumber() + " (" + phone.getType().name() + ")");}if (peopleInfo.hasData() && peopleInfo.getData().is(Address.class)) {Address address = peopleInfo.getData().unpack(Address.class);if (!address.getHomeAddress().isEmpty()) {System.out.println("家庭地址:" + address.getHomeAddress());}if (!address.getUnitAddress().isEmpty()) {System.out.println("单位地址:" + address.getUnitAddress());}}switch (peopleInfo.getOtherContactCase()) {case QQ:System.out.println("qq号:" + peopleInfo.getQq());break;case WECHAT:System.out.println("微信号:" + peopleInfo.getWechat());break;case OTHERCONTACT_NOT_SET:break;}for (Map.Entry<String, String> entry : peopleInfo.getRemarkMap().entrySet()) {System.out.println(" " + entry.getKey() + " : " + entry.getValue());}}}
}

运行结果:


 

12.默认值

反序列化消息时,如果被反序列化的⼆进制序列中不包含某个字段,反序列化对象中相应字段时,就会设置为该字段的默认值。不同的类型对应的默认值不同:

  • 对于字符串,默认值为空字符串。
  • 对于字节,默认值为空字节。
  • 对于布尔值,默认值为false。
  • 对于数值类型,默认值为0。
  • 对于枚举,默认值是第⼀个定义的枚举值,必须为0。
  • 对于消息字段,未设置该字段。它的取值是依赖于语⾔。
  • 对于设置了repeated的字段的默认值是空的(通常是相应语⾔的⼀个空列表)。
  • 对于 消息字段 、 oneof字段 和 any字段 ,都有has⽅法来检测当前字段是否被设置。
     

13.更新消息

更新规则
如果现有的消息类型已经不再满⾜我们的需求,例如需要扩展⼀个字段,在不破坏任何现有代码的情况下更新消息类型⾮常简单。遵循如下规则即可:

  • 禁⽌修改任何已有字段的字段编号。
  • 若是移除⽼字段,要保证不再使⽤移除字段的字段编号。正确的做法是保留字段编号(reserved),以确保该编号将不能被重复使⽤。不建议直接删除或注释掉字段。
  • int32,uint32,int64,uint64和bool是完全兼容的。可以从这些类型中的⼀个改为另⼀个,⽽不破坏前后兼容性。若解析出来的数值与相应的类型不匹配,可能会被截断(例如,若将64位整数当做32位进⾏读取,它将被截断为32位)。
  • sint32和sint64相互兼容但不与其他的整型兼容。
  • string和bytes在合法UTF-8字节前提下也是兼容的。
  • bytes包含消息编码版本的情况下,嵌套消息与bytes也是兼容的。
  • fixed32与sfixed32兼容,fixed64与sfixed64兼容。
  • enum与int32,uint32,int64和uint64兼容(注意若值不匹配会被截断)。但要注意当反序
  • 列化消息时会根据语⾔采⽤不同的处理⽅案:例如,未识别的proto3枚举类型会被保存在消息中,但是当消息反序列化时如何表⽰是依赖于编程语⾔的。整型字段总是会保持其的值。
  • oneof:
    • 将⼀个单独的值更改为新oneof类型成员之⼀是安全和⼆进制兼容的。
    • 若确定没有代码⼀次性设置多个值那么将多个字段移⼊⼀个新oneof类型也是可⾏的。
    • 将任何字段移⼊已存在的oneof类型是不安全的。

移除老字段错误示例:

proto.update.client contacts.proto(移除字段之前)

syntax = "proto3";
package client;option java_multiple_files = true; // 编译后⽣成的⽂件是否分为多个⽂件
option java_package = "com.example.update.client"; // 编译后⽣成⽂件所在的包路径
option java_outer_classname = "ContactsProtos"; // 编译后⽣成的proto包装类的类名message PeopleInfo{string name = 1;int32 age = 2;message Phone {string number = 1; // 电话号码}repeated Phone phone = 3; // 电话}message Contacts{repeated PeopleInfo contacts = 1;
}

proto.update.service   contacts.proto(移除字段age)

(重行编译一下,使用maven插件) 

 TestWrite.java

package com.example.update.service;import com.example.update.service.Contacts.Builder;import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Scanner;public class TestWrite {public static void main(String[] args) throws IOException {Builder contactsBuilder = Contacts.newBuilder();// 读取已存在的contactstry {contactsBuilder.mergeFrom(newFileInputStream("src/main/java/com/example/service/contacts.bin"));} catch (FileNotFoundException e) {System.out.println("contacts.bin not found. Creating a new file.");}// 新增⼀个联系⼈contactsBuilder.addContacts(addPeopleInfo());// 将新的contacts写回磁盘FileOutputStream output = new FileOutputStream("src/main/java/com/example/update/contacts.bin");contactsBuilder.build().writeTo(output);output.close();}private static PeopleInfo addPeopleInfo() {Scanner scan = new Scanner(System.in);PeopleInfo.Builder peopleBuilder = PeopleInfo.newBuilder();System.out.println("-------------新增联系⼈-------------");System.out.print("请输⼊联系⼈姓名: ");String name = scan.nextLine();peopleBuilder.setName(name);//        System.out.print("请输⼊联系⼈年龄: ");
//        int age = scan.nextInt();
//        peopleBuilder.setAge(age);
//        scan.nextLine();System.out.print("请输⼊联系⼈生日: ");int bir = scan.nextInt();peopleBuilder.setBirthday(bir);scan.nextLine();for (int i = 0; ; i++) {System.out.print("请输⼊联系⼈电话" + (i + 1) + "(只输⼊回⻋完成电话新 增): ");String number = scan.nextLine();if (number.isEmpty()) {break;}PeopleInfo.Phone.Builder phoneBuilder = PeopleInfo.Phone.newBuilder();phoneBuilder.setNumber(number);peopleBuilder.addPhone(phoneBuilder);}System.out.println("-------------添加联系⼈成功-------------");return peopleBuilder.build();}
}

运行结果:

  

 TestRead.java

package com.example.update.client;import com.google.protobuf.InvalidProtocolBufferException;
import java.io.FileInputStream;
import java.io.IOException;public class TestRead {public static void main(String[] args) throws IOException {// 从磁盘⽂件⾥读取,并反序列化为 Message 实例Contacts contacts = Contacts.parseFrom(new FileInputStream("src/main/java/com/example/update/contacts.bin"));// 打印printContacts(contacts);}private static void printContacts(Contacts contacts) throws InvalidProtocolBufferException {for (int i = 0; i < contacts.getContactsCount(); i++) {System.out.println("--------------联系⼈" + (i + 1) + "-----------");PeopleInfo peopleInfo = contacts.getContacts(i);System.out.println("姓名: " + peopleInfo.getName());System.out.println("年龄: " + peopleInfo.getAge());int j = 1;for (PeopleInfo.Phone phone : peopleInfo.getPhoneList()) {System.out.println("电话" + (j++) + ": " + phone.getNumber());}}}
}

 运行结果:

结论:不能重复使用字段编号,不建议直接删除或注释掉字段。

14.保留字段reserved

如果通过删除或注释掉字段来更新消息类型,未来的⽤⼾在添加新字段时,有可能会使⽤以前已经
存在,但已经被删除或注释掉的字段编号。将来使⽤该.proto的旧版本时的程序会引发很多问题:数据损坏、隐私错误等等。确保不会发⽣这种情况的⼀种⽅法是:使⽤ reserved 将指定字段的编号或名称设置为保留项。当我们再使⽤这些编号或名称时,protocol buffer的编译器将会警告这些编号或名称不可⽤。举个例⼦:

message Message {// 设置保留项reserved 100, 101, 200 to 299;reserved "field3", "field4";// 注意:不要在⼀⾏ reserved 声明中同时声明字段编号和名称。// reserved 102, "field5";// 设置保留项之后,下⾯代码会告警int32 field1 = 100; //告警:Field 'field1' uses reserved number 100int32 field2 = 101; //告警:Field 'field2' uses reserved number 101int32 field3 = 102; //告警:Field name 'field3' is reservedint32 field4 = 103; //告警:Field name 'field4' is reserved
}

15.未知字段

在通讯录3.0版本中,我们向service⽬录下的contacts.proto新增了‘⽣⽇’字段,但对于client相
关的代码并没有任何改动。验证后发现新代码序列化的消息(service)也可以被旧代码(client)解析。并且这⾥要说的是,新增的‘⽣⽇’字段在旧程序(client)中其实并没有丢失,⽽是会作为旧程序的未知字段。

  • 未知字段:解析结构良好的protocol buffer已序列化数据中的未识别字段的表⽰⽅式。例如,当旧程序解析带有新字段的数据时,这些新字段就会成为旧程序的未知字段。
  • 本来,proto3在解析消息时总是会丢弃未知字段,但在3.5版本中重新引⼊了对未知字段的保留机制。所以在3.5或更⾼版本中,未知字段在反序列化时会被保留,同时也会包含在序列化的结果中。

代码示例:

service.contacts.proto

client.contacts.proto

 service.TestWrite.java

package com.example.update.service;import com.example.update.service.Contacts.Builder;import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Scanner;public class TestWrite {public static void main(String[] args) throws IOException {Builder contactsBuilder = Contacts.newBuilder();// 读取已存在的contactstry {contactsBuilder.mergeFrom(newFileInputStream("src/main/java/com/example/service/contacts.bin"));} catch (FileNotFoundException e) {System.out.println("contacts.bin not found. Creating a new file.");}// 新增⼀个联系⼈contactsBuilder.addContacts(addPeopleInfo());// 将新的contacts写回磁盘FileOutputStream output = new FileOutputStream("src/main/java/com/example/update/contacts.bin");contactsBuilder.build().writeTo(output);output.close();}private static PeopleInfo addPeopleInfo() {Scanner scan = new Scanner(System.in);PeopleInfo.Builder peopleBuilder = PeopleInfo.newBuilder();System.out.println("-------------新增联系⼈-------------");System.out.print("请输⼊联系⼈姓名: ");String name = scan.nextLine();peopleBuilder.setName(name);//        System.out.print("请输⼊联系⼈年龄: ");
//        int age = scan.nextInt();
//        peopleBuilder.setAge(age);
//        scan.nextLine();System.out.print("请输⼊联系⼈生日: ");int bir = scan.nextInt();peopleBuilder.setBirthday(bir);scan.nextLine();for (int i = 0; ; i++) {System.out.print("请输⼊联系⼈电话" + (i + 1) + "(只输⼊回⻋完成电话新 增): ");String number = scan.nextLine();if (number.isEmpty()) {break;}PeopleInfo.Phone.Builder phoneBuilder = PeopleInfo.Phone.newBuilder();phoneBuilder.setNumber(number);peopleBuilder.addPhone(phoneBuilder);}System.out.println("-------------添加联系⼈成功-------------");return peopleBuilder.build();}
}

运行结果:

client.TestRead.java 

package com.example.update.client;import com.google.protobuf.InvalidProtocolBufferException;import java.io.FileInputStream;
import java.io.IOException;public class TestRead {public static void main(String[] args) throws IOException {// 从磁盘⽂件⾥读取,并反序列化为 Message 实例Contacts contacts = Contacts.parseFrom(new FileInputStream("src/main/java/com/example/update/contacts.bin"));// 打印printContacts(contacts);}private static void printContacts(Contacts contacts) throws InvalidProtocolBufferException {for (int i = 0; i < contacts.getContactsCount(); i++) {System.out.println("--------------联系⼈" + (i + 1) + "-----------");PeopleInfo peopleInfo = contacts.getContacts(i);System.out.println("姓名: " + peopleInfo.getName());System.out.println("年龄: " + peopleInfo.getAge());int j = 1;for (PeopleInfo.Phone phone : peopleInfo.getPhoneList()) {System.out.println("电话" + (j++) + ": " + phone.getNumber());}System.out.println("未知字段内容:\n" + peopleInfo.getUnknownFields());}}
}

运行结果:

未知字段从哪获取?
在PeopleInfo.java 的PeopleInfo 类中,有个 getUnknownFields() ⽅法⽤来获取未知字段:

public final com.google.protobuf.UnknownFieldSet getUnknownFields() {...}

UnknownFieldSet类介绍

  • UnknownFieldSet包含在分析消息时遇到但未由其类型定义的所有字段。
public final class UnknownFieldSet implements MessageLite {private final TreeMap<Integer, Field> fields;public Map<Integer, Field> asMap() {...}public boolean hasField(int number) {...}public Field getField(int number) {...}// ----------------- 重写了 toString ----------------public String toString() {...}// ------------------------builder------------------public static final class Builder implements MessageLite.Builder {public Builder clear() {...}public Builder clearField(int number) {...}public boolean hasField(int number) {...}public Builder addField(int number, Field field) {...}public Map<Integer, Field> asMap() {...}}public static final class Field {private List<Long> varint;private List<Integer> fixed32;private List<Long> fixed64;private List<ByteString> lengthDelimited;private List<UnknownFieldSet> group;public List<Long> getVarintList() {...}public List<Integer> getFixed32List() {...}public List<Long> getFixed64List() {...}public List<ByteString> getLengthDelimitedList() {...}public List<UnknownFieldSet> getGroupList() {...}// 省略了 Field Builder : 是⼀些处理字段的⽅法,例如设置、获取、清理}
}

升级通讯录3.1版本---验证未知字段

更新 TestRead.java (通讯录3.1),在这个版本中,需要打印出未知字段的内容。更新的代码如下:

package com.example.update.client;import com.google.protobuf.InvalidProtocolBufferException;import java.io.FileInputStream;
import java.io.IOException;public class TestRead {public static void main(String[] args) throws IOException {// 从磁盘⽂件⾥读取,并反序列化为 Message 实例Contacts contacts = Contacts.parseFrom(new FileInputStream("src/main/java/com/example/update/contacts.bin"));// 打印printContacts(contacts);}private static void printContacts(Contacts contacts) throws InvalidProtocolBufferException {for (int i = 0; i < contacts.getContactsCount(); i++) {System.out.println("--------------联系⼈" + (i + 1) + "-----------");PeopleInfo peopleInfo = contacts.getContacts(i);System.out.println("姓名: " + peopleInfo.getName());System.out.println("年龄: " + peopleInfo.getAge());int j = 1;for (PeopleInfo.Phone phone : peopleInfo.getPhoneList()) {System.out.println("电话" + (j++) + ": " + phone.getNumber());}System.out.println("未知字段内容:\n" + peopleInfo.getUnknownFields());}}
}

其他⽂件均不⽤做任何修改,运⾏Client下的main函数可得如下结果:




前后兼容性
根据上述的例⼦可以得出,pb是具有向前兼容的。为了叙述⽅便,把增加了“⽣⽇”属的TestWirte.java称为“新模块”;未做变动的TestRead.java称为“⽼模块”。

  • 向前兼容:⽼模块能够正确识别新模块⽣成或发出的协议。这时新增加的“⽣⽇”属性会被当作未知字段(pb3.5版本及之后)。
  • 向后兼容:新模块也能够正确识别⽼模块⽣成或发出的协议。
  • 前后兼容的作⽤:当我们维护⼀个很庞⼤的分布式系统时,由于你⽆法同时升级所有模块,为了保证在升级过程中,整个系统能够尽可能不受影响,就需要尽量保证通讯协议的“向后兼容”或“向前兼容”。

16.选项option 

.proto⽂件中可以声明许多选项,使⽤option 标注。选项能影响proto编译器的某些处理⽅式。

选项分类:

选项的完整列表在 google/protobuf/descriptor.proto 中定义。部分代码:

syntax = "proto2"; // descriptor.proto 使⽤ proto2 语法版本
message FileOptions { ... } // ⽂件选项 定义在 FileOptions 消息中
message MessageOptions { ... } // 消息类型选项 定义在 MessageOptions 消息中
message FieldOptions { ... } // 消息字段选项 定义在 FieldOptions 消息中
message OneofOptions { ... } // oneof字段选项 定义在 OneofOptions 消息中
message EnumOptions { ... } // 枚举类型选项 定义在 EnumOptions 消息中
message EnumValueOptions { .. } // 枚举值选项 定义在 EnumValueOptions 消息中
message ServiceOptions { ... } // 服务选项 定义在 ServiceOptions 消息中
message MethodOptions { ... } // 服务⽅法选项 定义在 MethodOptions 消息中

由此可⻅,选项分为 ⽂件级、消息级、字段级 等等,但并没有⼀种选项能作⽤于所有的类型。

JAVA常⽤选项列举

java_multiple_files:编译后⽣成的⽂件是否分为多个⽂件,该选项为⽂件选项。
java_package:编译后⽣成⽂件所在的包路径,该选项为⽂件选项。
java_outer_classname:编译后⽣成的proto包装类的类名,该选项为⽂件选项。
allow_alias:允许将相同的常量值分配给不同的枚举常量,⽤来定义别名。该选项为枚举选项。
举个例⼦:

enum PhoneType {option allow_alias =true;MP =0;TEL =1;LANDLINE =1; // 若不加 option allow_alias = true; 这⼀⾏会编译报错
}

设置⾃定义选项:

https://protobuf.dev/programming-guides/proto2/

17.通讯录4.0实现---⽹络版

需求:

Protobuf还常⽤于通讯协议、服务端数据交换场景。那么在这个⽰例中,我们将实现⼀个⽹络版本的通讯录,模拟实现客⼾端与服务端的交互,通过Protobuf来实现各端之间的协议序列化。
需求如下:

  • 客⼾端:向服务端发送联系⼈信息,并接收服务端返回的响应。
  • 服务端:接收到联系⼈信息后,将结果打印出来。
  • 客⼾端、服务端间的交互数据使⽤Protobuf来完成。
     

 proto.internet.client.contacts.proto

syntax = "proto3";
package client;option java_multiple_files = true; // 编译后⽣成的⽂件是否分为多个⽂件
option java_package = "com.example.internet.client.dto"; // 编译后⽣成⽂件所在的包路径
option java_outer_classname = "ContactsProtos"; // 编译后⽣成的proto包装类的类名message PeopleInfoRequest {string name = 1; // 姓名int32 age = 2; // 年龄message Phone {string number = 1; // 电话号码enum PhoneType {MP = 0; // 移动电话TEL = 1; // 固定电话}PhoneType type = 2; // 类型}repeated Phone phone = 3; // 电话map<string, string> remark = 4; // 备注
}message PeopleInfoResponse {string uid = 1;
}

 proto.internet.service.contacts.proto

syntax = "proto3";
package service;option java_multiple_files = true; // 编译后⽣成的⽂件是否分为多个⽂件
option java_package = "com.example.internet.service.dto"; // 编译后⽣成⽂件所在的包路径
option java_outer_classname = "ContactsProtos"; // 编译后⽣成的proto包装类的类名message PeopleInfoRequest {string name = 1; // 姓名int32 age = 2; // 年龄message Phone {string number = 1; // 电话号码enum PhoneType {MP = 0; // 移动电话TEL = 1; // 固定电话}PhoneType type = 2; // 类型}repeated Phone phone = 3; // 电话map<string, string> remark = 4; // 备注
}message PeopleInfoResponse {string uid = 1;
}

com.example.internet.client.BytesUtils.java

package com.example.internet.client;public class BytesUtils {/*** 获取 bytes 有效⻓度* @param bytes* @return*/public static int getValidLength(byte[] bytes){int i = 0;if (null == bytes || 0 == bytes.length)return i;for (; i < bytes.length; i++) {if (bytes[i] == '\0')break;}return i;}/*** 截取 bytes* @param b* @param off* @param length* @return*/public static byte[] subByte(byte[] b,int off,int length){byte[] b1 = new byte[length];System.arraycopy(b, off, b1, 0, length);return b1;}
}

com.example.internet.client.ContactsClient.java

package com.example.internet.client;import com.example.internet.client.dto.PeopleInfoRequest;
import com.example.internet.client.dto.PeopleInfoResponse;
import com.example.internet.client.BytesUtils;
import java.io.*;
import java.net.*;
import java.util.Scanner;public class ContactsClient {private static final SocketAddress ADDRESS = new InetSocketAddress("localhost", 8888);public static void main(String[] args) throws IOException {// 创建客⼾端 DatagramSocketDatagramSocket socket = new DatagramSocket();// 构造 request 请求数据PeopleInfoRequest request = createRequest();// 序列化 requestbyte[] requestData = request.toByteArray();// 创建 request 数据报DatagramPacket requestPacket = new DatagramPacket(requestData,requestData.length, ADDRESS);// 发送 request 数据报socket.send(requestPacket);System.out.println("发送成功!");// 创建 response 数据报,⽤于接收服务端返回的响应byte[] udpResponse = new byte[1024];DatagramPacket responsePacket = new DatagramPacket(udpResponse,udpResponse.length);// 接收 response 数据报socket.receive(responsePacket);// 获取有效的 responseint length = BytesUtils.getValidLength(udpResponse);byte[] reqsponseData = BytesUtils.subByte(udpResponse, 0, length);// 反序列化 response,打印结果PeopleInfoResponse response = PeopleInfoResponse.parseFrom(reqsponseData);System.out.printf("接收到服务端返回的响应:%s", response.toString());}private static PeopleInfoRequest createRequest() {System.out.println("------输⼊需要传输的联系⼈信息-----");Scanner scan = new Scanner(System.in);PeopleInfoRequest.Builder peopleBuilder = PeopleInfoRequest.newBuilder();System.out.print("请输⼊联系⼈姓名: ");String name = scan.nextLine();peopleBuilder.setName(name);System.out.print("请输⼊联系⼈年龄: ");int age = scan.nextInt();peopleBuilder.setAge(age);scan.nextLine();for (int i = 0; ; i++) {System.out.print("请输⼊联系⼈电话" + (i + 1) + "(只输⼊回⻋完成电话新增): ");String number = scan.nextLine();if (number.isEmpty()) {break;}PeopleInfoRequest.Phone.Builder phoneBuilder = PeopleInfoRequest.Phone.newBuilder();phoneBuilder.setNumber(number);peopleBuilder.addPhone(phoneBuilder);}for (int i = 0; ; i++) {System.out.print("请输⼊备注" + (i + 1) + "标题 (只输⼊回⻋完成备注新增): ");String remarkKey = scan.nextLine();if (remarkKey.isEmpty()) {break;}System.out.print("请输⼊备注" + (i + 1) + "内容: ");String remarkValue = scan.nextLine();peopleBuilder.putRemark(remarkKey, remarkValue);}System.out.println("------------输⼊结束-----------");return peopleBuilder.build();}
}

com.example.internet.service.BytesUtils.java

package com.example.internet.service;public class BytesUtils {/*** 获取 bytes 有效⻓度** @param bytes* @return*/public static int getValidLength(byte[] bytes) {int i = 0;if (null == bytes || 0 == bytes.length)return i;for (; i < bytes.length; i++) {if (bytes[i] == '\0')break;}return i;}/*** 截取 bytes** @param b* @param off* @param length* @return*/public static byte[] subByte(byte[] b, int off, int length) {byte[] b1 = new byte[length];System.arraycopy(b, off, b1, 0, length);return b1;}
}

com.example.internet.service.ContactsService.java

package com.example.internet.service;import com.example.internet.service.dto.PeopleInfoRequest;
import com.example.internet.service.dto.PeopleInfoResponse;
import java.io.*;
import java.net.DatagramPacket;
import java.net.DatagramSocket;public class ContantsService {//服务器socket要绑定固定的端⼝private static final int PORT = 8888;public static void main(String[] args) throws IOException {// 创建服务端DatagramSocket,指定端⼝,可以发送及接收UDP数据报DatagramSocket socket = new DatagramSocket(PORT);// 不停接收客⼾端udp数据报while (true){System.out.println("等待接收UDP数据报...");// 创建 request 数据报,⽤于接收客⼾端发送的数据byte[] udpRequest = new byte[1024];// 1m=1024kb, 1kb=1024byte,//UDP最多64k(包含UDP⾸部8byte)DatagramPacket requestPacket = new DatagramPacket(udpRequest, udpRequest.length);// 接收 request 数据报,在接收到数据报之前会⼀直阻塞,socket.receive(requestPacket);// 获取有效的 requestint length = BytesUtils.getValidLength(udpRequest);byte[] requestData = BytesUtils.subByte(udpRequest, 0, length);// 反序列化 requestPeopleInfoRequest request = PeopleInfoRequest.parseFrom(requestData);System.out.println("接收到请求数据:");System.out.println(request.toString());// 构造 responsePeopleInfoResponse response = PeopleInfoResponse.newBuilder().setUid("111111111").build();// 序列化 responsebyte[] responseData = response.toByteArray();// 构造 response 数据报,注意接收的客⼾端数据报包含IP和端⼝号,要设置到响应//的数据报中DatagramPacket responsePacket = new DatagramPacket(responseData, responseData.length, requestPacket.getSocketAddress());// 发送 response 数据报socket.send(responsePacket);}}
}

运行结果:

19.序列化能⼒对⽐验证

在这⾥让我们分别使⽤PB与JSON的序列化与反序列化能⼒,对值完全相同的⼀份结构化数据进⾏不同次数的性能测试。为了可读性,下⾯这⼀份⽂本使⽤JSON格式展⽰了需要被进⾏测试的结构化数据内容:


20.总结:

总结:

  1. XML、JSON、ProtoBuf都具有数据结构化和数据序列化的能⼒。
  2. XML、JSON更注重数据结构化,关注可读性和语义表达能⼒。ProtoBuf更注重数据序列化,关注效率、空间、速度,可读性差,语义表达能⼒不⾜,为保证极致的效率,会舍弃⼀部分元信息。
  3. ProtoBuf的应⽤场景更为明确,XML、JSON的应⽤场景更为丰富。

相关文章:

ProtoBuf3语法详解

目录&#xff1a; 需求&#xff1a;字段规则消息类型的定义与使用通讯录2.0的写⼊实现TestRead.java(通讯录2.0)TestRead.java(通讯录2.0) 另⼀种验证⽅法--toString()enum类型升级通讯录⾄2.1版本Any类型oneof类型map类型默认值更新消息保留字段reserved未知字段选项option 通…...

尚硅谷css3笔记

目录 一、新增长度单位 二、新增盒子属性 1.border-box 怪异盒模型 2.resize 调整盒子大小 3.box-shadow 盒子阴影 案例&#xff1a;鼠标悬浮盒子上时&#xff0c;盒子有一个过度的阴影效果 三、新增背景属性 1.background-origin 设置背景图的原点 2.background-clip 设置背…...

ppt转pdf免费的工具哪个好用?免费PPT转换为PDF的方法分享

在我们的工作和学习中&#xff0c;将PPT文件转换为PDF格式对于分享和储存具有重要意义。PPT文件是一种常用的演示工具&#xff0c;用于展示和传达信息。然而&#xff0c;PPT文件在不同的平台和设备上可能存在格式兼容性的问题&#xff0c;而且文件大小较大&#xff0c;不方便共…...

IDEA常用工具配置

IDEA常用工具&配置 如果发现插件市场用不了&#xff0c;可以设置Http Proxy&#xff0c;在该界面上点击”Check connection“并输入的地址&#xff1a;https://plugins.jetbrains.com/ 。 一、常用插件 1、MybatisX Mybaits Plus插件&#xff0c;支持java与xml互转 2、F…...

hive--给表名和字段加注释

1.建表添加注释 CREATE EXTERNAL TABLE test(loc_province string comment 省份,loc_city string comment 城市,loc_district string comment 区,loc_street string comment 街道,)COMMENT 每日数据处理后的表 PARTITIONED BY (par_dt string) ROW FORMAT SERDEorg.apache.had…...

AutoSAR系列讲解(深入篇)13.4-Mcal Dio代码分析(上)

目录 一、文件结构 二、动态代码 1、arxml文件 2、Dio_Cfg.h 3、Dio_PBCfg.c 4、小结 考虑了一下,觉得还是有必要拿出一个代码来具体分析一下,所以我们以最简单的DIO来举例子。但是如果直接贴上源码,可能会有一些版权问题,...

基于Mybatis Plus的SQL输出拦截器。完美的输出打印 SQL 及执行时长、statement

我们需要想办法打印出完成的SQL&#xff0c;Mybatis为我们提供了 org.apache.ibatis.plugin.Interceptor接口&#xff0c;我们来实现该接口做一些打印SQL的工作 package org.springjmis.core.mp.plugins;import com.baomidou.mybatisplus.core.toolkit.CollectionUtils; impor…...

C++ STL list

✅<1>主页&#xff1a;我的代码爱吃辣 &#x1f4c3;<2>知识讲解&#xff1a;C之 STL list介绍和模拟实现 ☂️<3>开发环境&#xff1a;Visual Studio 2022 &#x1f4ac;<4>前言&#xff1a;上次我们详细的介绍了vector&#xff0c;今天我们继续来介绍…...

Django图书商城系统实战开发-实现订单管理

Django图书商城系统实战开发-实现订单管理 简介 在本教程中&#xff0c;我们将继续基于Django框架开发图书商城系统&#xff0c;这次的重点是实现订单管理功能。订单管理是一个电子商务系统中非常重要的部分&#xff0c;它涉及到用户下单、支付、发货以及订单状态的管理等方面…...

POJ 3421 X-factor Chains 埃氏筛法+质因子分解+DFS

一、思路 我们先用埃氏筛法&#xff0c;找出1048576范围内的素数&#xff0c;其实找出1024以内的就够了&#xff0c;但是1048576也不大&#xff0c;所以无所谓了。 然后把输入的数字不断的判断与每个素数是否整除&#xff0c;然后把输入的数变为很多个素数相乘的形式&#xf…...

【积水成渊】9 个CSS 伪元素

大家好&#xff0c;我是csdn的博主&#xff1a;lqj_本人 这是我的个人博客主页&#xff1a; lqj_本人_python人工智能视觉&#xff08;opencv&#xff09;从入门到实战,前端,微信小程序-CSDN博客 最新的uniapp毕业设计专栏也放在下方了&#xff1a; https://blog.csdn.net/lbcy…...

【002】学习笔记之typescript的【任意类型】

任意类型 顶级类型&#xff1a;any类型和 unknown 类型 any类型 声明变量的时候没有指定任意类型默认为any任意类型都可以赋值给any&#xff0c;不需要检查类型。也是他的弊端如果使用any 就失去了TS类型检测的作用 unknown 类型 TypeScript 3.0中引入的 unknown 类型也被认为…...

题目:2574.左右元素和的差值

​​题目来源&#xff1a; leetcode题目&#xff0c;网址&#xff1a;2574. 左右元素和的差值 - 力扣&#xff08;LeetCode&#xff09; 解题思路&#xff1a; 按题目要求模拟即可。 解题代码&#xff1a; class Solution {public int[] leftRightDifference(int[] nums) {i…...

成集云 | 用友U8采购请购单同步钉钉 | 解决方案

源系统成集云目标系统 方案介绍 用友U8是中国用友集团开发和推出的一款企业级管理软件产品。具有丰富的功能模块&#xff0c;包括财务管理、采购管理、销售管理、库存管理、生产管理、人力资源管理、客户关系管理等&#xff0c;可根据企业的需求选择相应的模块进行集…...

爬虫的代理IP池写哪里了?

亲爱的程序员小伙伴们&#xff0c;想要提高爬虫效率和稳定性&#xff0c;组建一个强大的代理IP池是非常重要的一步&#xff01;今天我就来和你分享一下&#xff0c;代理IP池到底应该写在哪里&#xff0c;以及如何打造一个令人瞩目的代理IP池&#xff01;准备好了吗&#xff1f;…...

CSS变形与动画(三):animation帧动画详解(用法 + 四个例子)

文章目录 animation 帧动画使用定义例子1 字母例子2 水滴例子3 会动的边框例子4 旋转木马 animation 帧动画 定义好后作用于需要变化的标签上。 使用 animation-name 设置动画名称 animation-duration: 设置动画的持续时间 animation-timing-function 设置动画渐变速度 anim…...

Ubuntu发布java版本

1、连接服务器 2、进入目录 cd /usr/safety/app/3、上传jar文件 4、杀掉原java进程 1. 查看当前java进程 2. ps -ef|grep java 3. ycmachine:/usr/safety/app$ ps -ef|grep java root 430007 1 6 01:11 pts/0 00:02:45 /usr/local/java/jdk1.8.0_341/bin/j…...

Java反射机制是什么?

Java反射机制是 Java 语言的一个重要特性。 在学习 Java 反射机制前&#xff0c;大家应该先了解两个概念&#xff0c;编译期和运行期。 编译期是指把源码交给编译器编译成计算机可以执行的文件的过程。在 Java 中也就是把 Java 代码编成 class 文件的过程。编译期只是做了一些…...

legacy-peer-deps的作用

加入ui组件库&#xff0c;以element-ui为例子 安装命令&#xff1a; npm i element-ui -S 如果安装不上&#xff0c;是因为npm版本问题报错&#xff0c;那么就使用以下命令 npm i element-ui -S --legacy-peer-deps那么legacy-peer-deps的作用是&#xff1f; 它是用于绕过pee…...

卷积操作后特征图尺寸,感受野,参数量的计算

文章目录 1、输出特征图的尺寸大小2、感受野的计算3、卷积核的参数量 1、输出特征图的尺寸大小 如果包含空洞卷积&#xff0c;即扩张率dilation rate不为1时&#xff1a; 2、感受野的计算 例如&#xff0c;图像经过两个3*3&#xff0c;步长为2的卷积后感受野为&#xff1a; co…...

C/C++ 注意点补充

C/C 注意点补充 地址与指针函数缺省 地址与指针 p的值是a的地址值&#xff0c;p的类型是int*&#xff0c;p的值是十六进制表示的地址值 所以可以直接把地址值通过强制转换 转换为地址p 如上图&#xff01;&#xff01;&#xff01; int a10; int *p&a; printf("%#p\n&…...

Python实时监控键盘的输入并打印出来

要实现Python实时监控键盘的输入并打印出来&#xff0c;可以使用pynput模块。 首先&#xff0c;需要安装pynput模块&#xff1a; pip install pynput 然后&#xff0c;可以编写以下代码来实现实时监控键盘输入并打印出来的功能&#xff1a; from pynput import keyboard# 定…...

LaWGPT零基础部署win10+anaconda

准备代码&#xff0c;创建环境 # 下载代码 git clone https://github.com/pengxiao-song/LaWGPT cd LaWGPT # 创建环境 conda create -n lawgpt python3.10 -y conda activate lawgpt pip install -r requirements.txt # 启动可视化脚本&#xff08;自动下载预训练模型约15GB…...

糖尿病视网膜病变,黄斑病变,年龄相关检测研究(Matlab代码)

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…...

管理类联考——逻辑——真题篇——按知识分类——汇总篇——一、形式逻辑——选言——相容选言——或——第一节 推结论

第五章 选言命题:相容选言-或;不相容选言-要么要么 第一节 相容选言-或-推结论-A或B为真,则非A→B,非B→A(否一则肯一) 真题(2010-28)-相容选言-或-推结论-(1)A或B为真,A为假:得B为真(否一则肯一); 28.域控制器储存了域内的账户、密码和属于这个城市的计算机三…...

MySQL数据库——图形化界面工具(DataGrip),SQL(2)-DML(插入、修改和删除数据)

目录 图形化界面工具&#xff08;DataGrip&#xff09; 下载及安装 启动及连接 使用 创建数据库 创建表结构 编写SQL DML 插入 更新和删除 1.修改数据 2.删除数据 总结 图形化界面工具&#xff08;DataGrip&#xff09; 下载及安装 DataGrip下载链接&#xff1a;…...

【Git】(五)切换分支

1、切换分支 git checkout newBranch 2、如果需要保留本地修改 ​git status git add . git commit --amend git checkout newBranch 3、强制切换分支 放弃本地修改&#xff0c;强制切换。 git checkout -f newBranch...

LVS集群和nginx负载均衡

目录 1、基于 CentOS 7 构建 LVS-DR 群集。 2、配置nginx负载均衡。 1、基于 CentOS 7 构建 LVS-DR 群集。 1.部署LVS负载调度器 1>安装配置工具 [rootnode6 ~]# yum install -y ipvsadm 2>配置LVS虚拟IP&#xff08;VIP地址&#xff09; [rootnode6 ~]# ifconfig ens…...

mysql 03.查询(重点)

先准备测试数据&#xff0c;代码如下&#xff1a; -- 创建数据库 DROP DATABASE IF EXISTS mydb; CREATE DATABASE mydb; USE mydb;-- 创建student表 CREATE TABLE student (sid CHAR(6),sname VARCHAR(50),age INT,gender VARCHAR(50) DEFAULT male );-- 向student表插入数据…...

arcpy读取csv、txt文件

目录 前置&#xff1a;文件读写模式步骤一&#xff1a;设置工作空间和文件路径步骤二&#xff1a;创建要素类步骤三&#xff1a;读取CSV文件并导入数据步骤四&#xff1a;读取txt文件并导入数据总结 当涉及到地理信息系统&#xff08;GIS&#xff09;数据处理时&#xff0c;有时…...