ProtoBuf3语法详解

目录:

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

1.需求:

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

2.字段规则

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

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

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

syntax = "proto3";
package start;

option java_multiple_files = true; // 编译后⽣成的⽂件是否分为多个⽂件
option java_package = "com.example.start"; // 编译后⽣成⽂件所在的包路径
option java_outer_classname = "ContactsProtos"; // 编译后⽣成的proto包装类的类名

message PeopleInfo{
  string name = 1;
  int32 age = 2;
  repeated string phone_numbers = 3;
}
  • PeopleInfo 消息中新增phone_numbers 字段,表⽰⼀个联系⼈有多个号码,所以将其设置为repeated。

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

定义:

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

使⽤

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

contacts.proto


 

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

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

syntax = "proto3";
package phone;

option java_multiple_files = true; // 编译后⽣成的⽂件是否分为多个⽂件
option java_package = "com.example.start"; // 编译后⽣成⽂件所在的包路径
option java_outer_classname = "PhoneProtos"; // 编译后⽣成的proto包装类的类名

message Phone{
  string number = 1;
}

contacts.proto

syntax = "proto3";
package start;

option java_multiple_files = true; // 编译后⽣成的⽂件是否分为多个⽂件
option java_package = "com.example.start"; // 编译后⽣成⽂件所在的包路径
option java_outer_classname = "ContactsProtos"; // 编译后⽣成的proto包装类的类名

import "start/phone.proto";


message PeopleInfo{
  string name = 1;

  int32 age = 2;

  repeated  phone.Phone phone = 3;
}

运行结果:

3.创建通讯录2.0版本

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

syntax = "proto3";
package start;

option java_multiple_files = true; // 编译后⽣成的⽂件是否分为多个⽂件
option java_package = "com.example.start"; // 编译后⽣成⽂件所在的包路径
option java_outer_classname = "ContactsProtos"; // 编译后⽣成的proto包装类的类名




message PeopleInfo{
  string name = 1;
  int32 age = 2;
  message Phone{
    string number = 1;
  }
  repeated Phone phone = 3;

}

message Contacts{
  repeated PeopleInfo contacts = 1;
}

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

在message 类中,主要包含:

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

在 Builder 类中,主要包含:

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

且在上述的例⼦中:

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

4.通讯录2.0的写⼊实现

TestWrite.java(通讯录2.0)

package testcode;

import com.example.start.Contacts;
import com.example.start.PeopleInfo;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Scanner;

public class TestWrite {
    public static void main(String[] args) throws IOException {
        Contacts.Builder contactsBuilder = Contacts.newBuilder();
        // 读取已存在的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;          //固定电话

}

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

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

定义时注意

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

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

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

 

8.升级通讯录⾄2.1版本

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

syntax = "proto3";
package start;

option java_multiple_files = true; // 编译后⽣成的⽂件是否分为多个⽂件
option java_package = "com.example.start"; // 编译后⽣成⽂件所在的包路径
option java_outer_classname = "ContactsProtos"; // 编译后⽣成的proto包装类的类名




message PeopleInfo{
  string name = 1;
  
  int32 age = 2;
  
  message Phone {
    string number = 1; // 电话号码
    
    enum PhoneType {
      MP = 0; // 移动电话
      TEL = 1; // 固定电话
    }
    
    PhoneType type = 2; // 类型
  }
  
  repeated Phone phone = 3; // 电话

}

message Contacts{
  repeated PeopleInfo contacts = 1;
}

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


更新TestWrite.java(通讯录2.1)

package testcode;

import com.example.start.Contacts;
import com.example.start.PeopleInfo;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Scanner;

public class TestWrite {
    public static void main(String[] args) throws IOException {
        Contacts.Builder contactsBuilder = Contacts.newBuilder();
        // 读取已存在的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.总结:

总结:

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
Protobuf3 中,可以通过自定义 option 来扩展 Protobuf3语法,实现一些特定的功能。在 Protobuf3 中,option 是一个关键字,用于在 .proto 文件中定义选项,可以用于定义消息、枚举、服务等。以下是 Protobuf3 自定义 option 的语法详解: 1. 定义 option 在 .proto 文件中,可以使用以下语法来定义 option: ``` option <option_name> = <option_value>; ``` 其中,`<option_name>` 是自定义的 option 名称,`<option_value>` 是 option 的值,可以是一个字符串、数字、布尔值等。 2. 使用 option 在使用自定义 option 时,可以将其加入到消息、枚举、服务等的定义中。以下是使用自定义 option 的语法: ``` <message/enum/service> <name> { option <option_name> = <option_value>; ... } ``` 其中,`<message/enum/service>` 是消息、枚举、服务的类型,`<name>` 是其名称,`<option_name>` 是自定义的 option 名称,`<option_value>` 是 option 的值。 3. 解析 option 在使用自定义 option 后,可以通过解析 option 来获取其值。以下是解析 option 的语法: ``` message <name> { ... optional <option_type> <option_name> = <field_number> [(option) = <option_value>]; ... } ``` 其中,`<name>` 是消息的名称,`<option_type>` 是 option 的类型,`<option_name>` 是自定义的 option 名称,`<field_number>` 是字段号,`(option) = <option_value>` 是 option 的值。 以上是 Protobuf3 自定义 option 的语法详解,希望能帮助你更好地理解和使用自定义 option。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

阿瞒有我良计15

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值