目录:
- 需求:
- 字段规则
- 消息类型的定义与使用
- 通讯录2.0的写⼊实现
- TestRead.java(通讯录2.0)
- TestRead.java(通讯录2.0) 另⼀种验证⽅法--toString()
- enum类型
- 升级通讯录⾄2.1版本
- Any类型
- oneof类型
- map类型
- 默认值
- 更新消息
- 保留字段reserved
- 未知字段
- 选项option
- 通讯录4.0实现---⽹络版
- 序列化能⼒对⽐验证
- 总结:
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();
// 读取已存在的contacts
try {
contactsBuilder.mergeFrom(new
FileInputStream("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; //固定电话
}
要注意枚举类型的定义有以下⼏种规则:
- 0值常量必须存在,且要作为第⼀个元素。这是为了与proto2的语义兼容:第⼀个元素作为默认值,且值为0。
- 枚举类型可以在消息外定义,也可以在消息体内定义(嵌套)。
- 枚举的常量值在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();
// 读取已存在的contacts
try {
contactsBuilder.mergeFrom(new
FileInputStream("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();
// 读取已存在的contacts
try {
contactsBuilder.mergeFrom(new
FileInputStream("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();
// 读取已存在的contacts
try {
contactsBuilder.mergeFrom(new
FileInputStream("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();
// 读取已存在的contacts
try {
contactsBuilder.mergeFrom(new
FileInputStream("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();
// 读取已存在的contacts
try {
contactsBuilder.mergeFrom(new
FileInputStream("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 100
int32 field2 = 101; //告警:Field 'field2' uses reserved number 101
int32 field3 = 102; //告警:Field name 'field3' is reserved
int32 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();
// 读取已存在的contacts
try {
contactsBuilder.mergeFrom(new
FileInputStream("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 {
// 创建客⼾端 DatagramSocket
DatagramSocket socket = new DatagramSocket();
// 构造 request 请求数据
PeopleInfoRequest request = createRequest();
// 序列化 request
byte[] 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);
// 获取有效的 response
int 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);
// 获取有效的 request
int length = BytesUtils.getValidLength(udpRequest);
byte[] requestData = BytesUtils.subByte(udpRequest, 0, length);
// 反序列化 request
PeopleInfoRequest request = PeopleInfoRequest.parseFrom(requestData);
System.out.println("接收到请求数据:");
System.out.println(request.toString());
// 构造 response
PeopleInfoResponse response = PeopleInfoResponse.newBuilder().setUid("111111111").build();
// 序列化 response
byte[] responseData = response.toByteArray();
// 构造 response 数据报,注意接收的客⼾端数据报包含IP和端⼝号,要设置到响应
//的数据报中
DatagramPacket responsePacket = new DatagramPacket(responseData, responseData.length, requestPacket.getSocketAddress());
// 发送 response 数据报
socket.send(responsePacket);
}
}
}
运行结果:
19.序列化能⼒对⽐验证
在这⾥让我们分别使⽤PB与JSON的序列化与反序列化能⼒,对值完全相同的⼀份结构化数据进⾏不同次数的性能测试。为了可读性,下⾯这⼀份⽂本使⽤JSON格式展⽰了需要被进⾏测试的结构化数据内容:
20.总结:
总结:
- XML、JSON、ProtoBuf都具有数据结构化和数据序列化的能⼒。
- XML、JSON更注重数据结构化,关注可读性和语义表达能⼒。ProtoBuf更注重数据序列化,关注效率、空间、速度,可读性差,语义表达能⼒不⾜,为保证极致的效率,会舍弃⼀部分元信息。
- ProtoBuf的应⽤场景更为明确,XML、JSON的应⽤场景更为丰富。