ProtoBuf3语法详解
目录:
- 需求:
- 字段规则
- 消息类型的定义与使用
- 通讯录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();// 读取已存在的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; //固定电话
}
要注意枚举类型的定义有以下⼏种规则:
- 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();// 读取已存在的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.总结:

总结:
- XML、JSON、ProtoBuf都具有数据结构化和数据序列化的能⼒。
- XML、JSON更注重数据结构化,关注可读性和语义表达能⼒。ProtoBuf更注重数据序列化,关注效率、空间、速度,可读性差,语义表达能⼒不⾜,为保证极致的效率,会舍弃⼀部分元信息。
- ProtoBuf的应⽤场景更为明确,XML、JSON的应⽤场景更为丰富。
相关文章:
ProtoBuf3语法详解
目录: 需求:字段规则消息类型的定义与使用通讯录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 盒子阴影 案例:鼠标悬浮盒子上时,盒子有一个过度的阴影效果 三、新增背景属性 1.background-origin 设置背景图的原点 2.background-clip 设置背…...
ppt转pdf免费的工具哪个好用?免费PPT转换为PDF的方法分享
在我们的工作和学习中,将PPT文件转换为PDF格式对于分享和储存具有重要意义。PPT文件是一种常用的演示工具,用于展示和传达信息。然而,PPT文件在不同的平台和设备上可能存在格式兼容性的问题,而且文件大小较大,不方便共…...
IDEA常用工具配置
IDEA常用工具&配置 如果发现插件市场用不了,可以设置Http Proxy,在该界面上点击”Check connection“并输入的地址:https://plugins.jetbrains.com/ 。 一、常用插件 1、MybatisX Mybaits Plus插件,支持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,Mybatis为我们提供了 org.apache.ibatis.plugin.Interceptor接口,我们来实现该接口做一些打印SQL的工作 package org.springjmis.core.mp.plugins;import com.baomidou.mybatisplus.core.toolkit.CollectionUtils; impor…...
C++ STL list
✅<1>主页:我的代码爱吃辣 📃<2>知识讲解:C之 STL list介绍和模拟实现 ☂️<3>开发环境:Visual Studio 2022 💬<4>前言:上次我们详细的介绍了vector,今天我们继续来介绍…...
Django图书商城系统实战开发-实现订单管理
Django图书商城系统实战开发-实现订单管理 简介 在本教程中,我们将继续基于Django框架开发图书商城系统,这次的重点是实现订单管理功能。订单管理是一个电子商务系统中非常重要的部分,它涉及到用户下单、支付、发货以及订单状态的管理等方面…...
POJ 3421 X-factor Chains 埃氏筛法+质因子分解+DFS
一、思路 我们先用埃氏筛法,找出1048576范围内的素数,其实找出1024以内的就够了,但是1048576也不大,所以无所谓了。 然后把输入的数字不断的判断与每个素数是否整除,然后把输入的数变为很多个素数相乘的形式…...
【积水成渊】9 个CSS 伪元素
大家好,我是csdn的博主:lqj_本人 这是我的个人博客主页: lqj_本人_python人工智能视觉(opencv)从入门到实战,前端,微信小程序-CSDN博客 最新的uniapp毕业设计专栏也放在下方了: https://blog.csdn.net/lbcy…...
【002】学习笔记之typescript的【任意类型】
任意类型 顶级类型:any类型和 unknown 类型 any类型 声明变量的时候没有指定任意类型默认为any任意类型都可以赋值给any,不需要检查类型。也是他的弊端如果使用any 就失去了TS类型检测的作用 unknown 类型 TypeScript 3.0中引入的 unknown 类型也被认为…...
题目:2574.左右元素和的差值
题目来源: leetcode题目,网址:2574. 左右元素和的差值 - 力扣(LeetCode) 解题思路: 按题目要求模拟即可。 解题代码: class Solution {public int[] leftRightDifference(int[] nums) {i…...
成集云 | 用友U8采购请购单同步钉钉 | 解决方案
源系统成集云目标系统 方案介绍 用友U8是中国用友集团开发和推出的一款企业级管理软件产品。具有丰富的功能模块,包括财务管理、采购管理、销售管理、库存管理、生产管理、人力资源管理、客户关系管理等,可根据企业的需求选择相应的模块进行集…...
爬虫的代理IP池写哪里了?
亲爱的程序员小伙伴们,想要提高爬虫效率和稳定性,组建一个强大的代理IP池是非常重要的一步!今天我就来和你分享一下,代理IP池到底应该写在哪里,以及如何打造一个令人瞩目的代理IP池!准备好了吗?…...
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 反射机制前,大家应该先了解两个概念,编译期和运行期。 编译期是指把源码交给编译器编译成计算机可以执行的文件的过程。在 Java 中也就是把 Java 代码编成 class 文件的过程。编译期只是做了一些…...
legacy-peer-deps的作用
加入ui组件库,以element-ui为例子 安装命令: npm i element-ui -S 如果安装不上,是因为npm版本问题报错,那么就使用以下命令 npm i element-ui -S --legacy-peer-deps那么legacy-peer-deps的作用是? 它是用于绕过pee…...
卷积操作后特征图尺寸,感受野,参数量的计算
文章目录 1、输出特征图的尺寸大小2、感受野的计算3、卷积核的参数量 1、输出特征图的尺寸大小 如果包含空洞卷积,即扩张率dilation rate不为1时: 2、感受野的计算 例如,图像经过两个3*3,步长为2的卷积后感受野为: co…...
PHP和Node.js哪个更爽?
先说结论,rust完胜。 php:laravel,swoole,webman,最开始在苏宁的时候写了几年php,当时觉得php真的是世界上最好的语言,因为当初活在舒适圈里,不愿意跳出来,就好比当初活在…...
什么是库存周转?如何用进销存系统提高库存周转率?
你可能听说过这样一句话: “利润不是赚出来的,是管出来的。” 尤其是在制造业、批发零售、电商这类“货堆成山”的行业,很多企业看着销售不错,账上却没钱、利润也不见了,一翻库存才发现: 一堆卖不动的旧货…...
vue3 字体颜色设置的多种方式
在Vue 3中设置字体颜色可以通过多种方式实现,这取决于你是想在组件内部直接设置,还是在CSS/SCSS/LESS等样式文件中定义。以下是几种常见的方法: 1. 内联样式 你可以直接在模板中使用style绑定来设置字体颜色。 <template><div :s…...
selenium学习实战【Python爬虫】
selenium学习实战【Python爬虫】 文章目录 selenium学习实战【Python爬虫】一、声明二、学习目标三、安装依赖3.1 安装selenium库3.2 安装浏览器驱动3.2.1 查看Edge版本3.2.2 驱动安装 四、代码讲解4.1 配置浏览器4.2 加载更多4.3 寻找内容4.4 完整代码 五、报告文件爬取5.1 提…...
什么是Ansible Jinja2
理解 Ansible Jinja2 模板 Ansible 是一款功能强大的开源自动化工具,可让您无缝地管理和配置系统。Ansible 的一大亮点是它使用 Jinja2 模板,允许您根据变量数据动态生成文件、配置设置和脚本。本文将向您介绍 Ansible 中的 Jinja2 模板,并通…...
docker 部署发现spring.profiles.active 问题
报错: org.springframework.boot.context.config.InvalidConfigDataPropertyException: Property spring.profiles.active imported from location class path resource [application-test.yml] is invalid in a profile specific resource [origin: class path re…...
代码随想录刷题day30
1、零钱兑换II 给你一个整数数组 coins 表示不同面额的硬币,另给一个整数 amount 表示总金额。 请你计算并返回可以凑成总金额的硬币组合数。如果任何硬币组合都无法凑出总金额,返回 0 。 假设每一种面额的硬币有无限个。 题目数据保证结果符合 32 位带…...
Java毕业设计:WML信息查询与后端信息发布系统开发
JAVAWML信息查询与后端信息发布系统实现 一、系统概述 本系统基于Java和WML(无线标记语言)技术开发,实现了移动设备上的信息查询与后端信息发布功能。系统采用B/S架构,服务器端使用Java Servlet处理请求,数据库采用MySQL存储信息࿰…...
MySQL 部分重点知识篇
一、数据库对象 1. 主键 定义 :主键是用于唯一标识表中每一行记录的字段或字段组合。它具有唯一性和非空性特点。 作用 :确保数据的完整性,便于数据的查询和管理。 示例 :在学生信息表中,学号可以作为主键ÿ…...
WPF八大法则:告别模态窗口卡顿
⚙️ 核心问题:阻塞式模态窗口的缺陷 原始代码中ShowDialog()会阻塞UI线程,导致后续逻辑无法执行: var result modalWindow.ShowDialog(); // 线程阻塞 ProcessResult(result); // 必须等待窗口关闭根本问题:…...
