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

protobuf的学习

1. 认识protobuf

        序列化概念回顾:

如何实现序列化:xml,json,pb(protobuf)

        pb:  帮助我们实现序列化的一种手段。
PB的特点 :
        语言无关、平台无关:即ProtoBuf支持Java、C++、Python等多种语言,支持多个平台

        高效:即比XML更小、 更快、 更为简单。
        扩展性、兼容性好:你可以更新数据结构,而不影响和破坏原有的旧程序。

使用特点:ProtoBuf是需要依赖通过编译生成的JAVA代码使用的

pb能够通过对message(只定义属性)就可以编译生成相关的java代码。

1.1 windows安装

        Protocol Buffers v21.11,win64版本。

将exe文件目录配置到环境变量中。E:\anzhuangchengxu\protobf\protoc-21.11-win64\bin;

此时在w上安装好了pb的编译器。

此时,确实安装成功。

1.2 在linux(ubuntu环境)安装pb

安装文件C:/Users/缘客扫/Desktop/java笔记/109期精品课/protobuf/protobuf/ProtoBuf%20安装.pdf

         sudo apt-get install autoconf automake libtool curl make g++ unzip -y

下载pb仓库:

将all的连接复制使用指令wget 连接;

下载完成后获得压缩包,使用unzip 压缩包指令进行解压:

如下:

进入pb文件如下:

执行 ./autogen.sh:

修改安装⽬录,统⼀安装在 /usr/local/protobuf 下 ./configure --prefix=/usr/local/protobuf

执行make(15min)

执行make check

Ubuntu 18.04 swap分区扩展_ubuntu18.04 如何查看swapfile文件路径-CSDN博客

发现中途有问题,执行下面的指令

sudo make install

检查是否安装成功:

        对ProtoBuf的完整学习,将使⽤项⽬推进的⽅式,即对于ProtoBuf知识内容的展开,会对 ⼀个项⽬进⾏⼀个版本⼀个版本的升级去讲解ProtoBuf对应的知识点。

1.3 pb的使用流程

需求1:

        编写第⼀版本的通讯录1.0。在通讯录1.0版本中,将实现:

         • 对⼀个联系⼈的信息使⽤PB进⾏序列化,并将结果打印出来。

         • 对序列化后的内容使⽤PB进⾏反序列,解析出联系⼈信息并打印出来。

         • 联系⼈包含以下信息:姓名、年龄。

        通过通讯录1.0,我们便能了解使⽤ProtoBuf初步要掌握的内容,以及体验到ProtoBuf的完整使⽤流 程。

下载下面的三个插件:

步骤1:创建.proto⽂件

        更新contacts.proto(通讯录1.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;
}

步骤2:编译contacts.proto⽂件,⽣成JAVA⽂件 编译的⽅式有两种:

        使⽤命令⾏编译;

protoc -I src/main/proto/start --java_out src/main/java contacts.proto

成功编译出java文件。

        使⽤maven插件编译:加入相关依赖

 <plugin>
                <groupId>org.xolstice.maven.plugins</groupId>
                <artifactId>protobuf-maven-plugin</artifactId>
                <version>0.6.1</version>
                <configuration>
                    <!-- 本地安装的protoc.exe的目录 -->
                    <protocExecutable>E:\anzhuangchengxu\protobf\protoc-21.11-win64\bin\protoc.exe</protocExecutable>
                    <!-- proto文件放置的目录,默认为/src/main/proto -->
                    <protoSourceRoot>${project.basedir}/src/main/proto</protoSourceRoot>
                    <!-- 生成文件的目录,默认生成到target/generated-sources/protobuf/ -->
                    <outputDirectory>${project.basedir}/src/main/java</outputDirectory>
                    <!-- 是否清空目标目录,默认值为true。这个最好设置为false,以免误删项目文件!!! -->
                    <clearOutputDirectory>false</clearOutputDirectory>
                </configuration>
            </plugin>

如下图所示,显示编译成功。 

        如上所示,首先1步骤,进入静态方法,进行2步骤对于相关字段进行设置,第三部将构造好的类进行创建,最后使用创建好的类对象进行第四步的序列化和反序列化操作。

        所谓序列化产生的结果都是二进制数据。

需求代码:

package com.example.start;

import com.google.protobuf.InvalidProtocolBufferException;

import java.util.Arrays;

public class FastStart {
    public static void main(String[] args) throws InvalidProtocolBufferException {
        PeopleInfo p1 = PeopleInfo.newBuilder()
                .setName("上嘉路")
                .setAge(24)
                .build();

        // 序列化
        byte[] bytes = p1.toByteArray();
        System.out.println("序列化结果为:" + Arrays.toString(bytes));

        // 反序列化
        PeopleInfo p2 = PeopleInfo.parseFrom(bytes);
        System.out.println("反序列化结果为:");
        System.out.println("姓名:" + p2.getName());
        System.out.println("年龄:" + p2.getAge());
    }
}

2. 新增需求2.0

        这个部分会对通讯录进⾏多次升级,使⽤2.x 表⽰升级的版本,最终将会升级如下内容:

         • 不再打印联系⼈的序列化结果,⽽是将通讯录序列化后并写⼊⽂件中。

        • 从⽂件中将通讯录解析出来,并进⾏打印。

         • 新增联系⼈属性,共包括:姓名、年龄、电话信息、地址、其他联系⽅式、备注。

proto3语法中支持在多message并创建的,一支持在message中内嵌创建子message,且这样多个message中的字段编号可以相同。 

代码如下:

// 首行: 语法指定行
syntax = "proto3";
package proto3;

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

import "google/protobuf/any.proto";


// 定义联系人 message
message PeopleInfo {
  // 字段类型 字段名 = 字段唯一编号;
  string name = 1;
  int32 age = 2;
  message Phone {
    string number = 1;
  }
  repeated Phone phone = 3;
}


message Contacts {
  repeated PeopleInfo contacts = 1;
}

package com.example.proto3;

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.bin,反序列化出通讯录对象
//        Contacts contacts = Contacts.parseFrom(
//                new FileInputStream("src/main/java/com/example/proto3/contacts.bin"));
//        contactsBuilder = contacts.toBuilder();

        try {
            contactsBuilder.mergeFrom(
                    new FileInputStream("src/main/java/com/example/proto3/contacts.bin"));
        } catch (FileNotFoundException e) {
            System.out.println("contacts.bin not find, create new file");
        }

        // 向通讯录中新增一个联系人
        contactsBuilder.addContacts(addPeopleInfo());

        // 序列化通讯录,将结果写入文件中
        //将序列化的东西写入到文件流中
        FileOutputStream outputStream = new FileOutputStream(
                "src/main/java/com/example/proto3/contacts.bin");
        contactsBuilder.build().writeTo(outputStream);
        outputStream.close();
    }

    private static PeopleInfo addPeopleInfo() {
        PeopleInfo.Builder builder = PeopleInfo.newBuilder();
        Scanner scanner = new Scanner(System.in);
        System.out.println("--------------新增联系人-------------");
        System.out.print("请输入联系人姓名:");
        String name = scanner.nextLine();
        builder.setName(name);

        System.out.print("请输入联系人年龄:");
        int age = scanner.nextInt();
        scanner.nextLine();
        builder.setAge(age);

        for (int i = 0;; i++) {
            System.out.print("请输入联系人电话" + (i + 1) + "(只输⼊回⻋完成电话新增): ");
            String number = scanner.nextLine();
            if (number.isEmpty()) {
                break;
            }
            PeopleInfo.Phone.Builder phoneBuilder = PeopleInfo.Phone.newBuilder();
            phoneBuilder.setNumber(number);
            builder.addPhone(phoneBuilder);
        }
            System.out.println("-------------添加联系人结束-------------");
            return builder.build();

    }
}

package com.example.proto3;

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 {

        // 读取文件,将读取的内容进行反序列化
        Contacts contacts = Contacts.parseFrom(
                new FileInputStream("src/main/java/com/example/proto3/contacts.bin"));

        // 打印
        printContacts(contacts);
        // System.out.println(contacts.toString());
    }

    private static void printContacts(Contacts contacts) throws InvalidProtocolBufferException {
        int i = 1;
        for (PeopleInfo peopleInfo : contacts.getContactsList()) {
            System.out.println("-----------联系人" + 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(contacts.toString());

3. 新增需求2.1

手机包含类型

枚举是用驼峰命名:

添加电话的类型:

// 首行: 语法指定行
syntax = "proto3";
package proto3;

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

import "google/protobuf/any.proto";

// 定义联系人 message
message PeopleInfo {
  // 字段类型 字段名 = 字段唯一编号;
  string name = 1;
  int32 age = 2;
  message Phone {
    string number = 1;
    // 0值必须存在,且作为第一个枚举常量的值
    // 枚举值范围:32位整数范围,不要设置负数
    enum PhoneType {
      MP = 0;  // 移动电话
      TEL = 1; // 固定电话
    }
    PhoneType type = 2;
  }
  repeated Phone phone = 3;
}
message Contacts {
  repeated PeopleInfo contacts = 1;
}

进行读写相关代码修改之后操作:

读取文件,获取相关内容:

反序列化的过程中,对于输入时没有进行设置类型的参数,会直接赋予默认值的类型。

any类型的message文件:在使用的时候将any类型理解为泛型。

4. 新增需求2.2

通讯录包含地址信息

在proto文件中添加相关字段:

message Address {
  string home_address = 1;
  string unit_address = 2;
}

写方法添加下面字段:

Address.Builder addressBuilder = Address.newBuilder();
        System.out.print("请输入联系人家庭地址:");
        String homeAddress = scanner.nextLine();
        addressBuilder.setHomeAddress(homeAddress);
        System.out.print("请输入联系人单位地址:");
        String unitAddress = scanner.nextLine();
        addressBuilder.setUnitAddress(unitAddress);
        builder.setData(Any.pack(addressBuilder.build()));
        System.out.println("-------------添加联系人结束-------------");

        return builder.build();

 

 内容读取方法添加:

  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());
                }
            }

5. 新增需求2.3

        新增其他需求,其他联系方式,qq或wx,oneof字段加强多选一的行为,且不支持repeated字段。

写操作:

读操作:

6. 新增需求2.4

使用map类型增加备注信息。

6. 默认值

• 对于字符串,默认值为空字符串。

• 对于字节,默认值为空字节。

• 对于布尔值,默认值为false。

• 对于数值类型,默认值为0。

• 对于枚举,默认值是第⼀个定义的枚举值,必须为0。

• 对于消息字段,未设置该字段。它的取值是依赖于语⾔。

• 对于设置了repeated的字段的默认值是空的(通常是相应语⾔的⼀个空列表)。

• 对于 消息字段、 oneof 字段 和 any 字段 ,都有has⽅法来检测当前字段是否被设置。

新增字段:注意新增的字段不和老字段冲突:名称和字段标号。

修改老字段:

        禁⽌修改任何已有字段的字段编号。

        • 若是移除⽼字段,要保证不再使⽤移除字段的字段编号。正确的做法是保留字段编号 (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文件在进行反编译数据时,不是按照字段名来进行编译的,而是按照字段标号来进行编译相关内容的。

 保留字段reserved:

         如果通过删除或注释掉字段来更新消息类型,未来的⽤⼾在添加新字段时,有可能会使⽤以前已经 存在,但已经被删除或注释掉的字段编号。将来使⽤该.proto的旧版本时的程序会引发很多问题:数 据损坏、隐私错误等等。

        确保不会发⽣这种情况的⼀种⽅法是:使⽤ reserved 将指定字段的编号或名称设置为保留项。当 我们再使⽤这些编号或名称时,protocolbuffer的编译器(在进行编译时)将会警告这些编号或名称不可⽤。reserved 可以一次性保留多个字段编号,期间使用逗号隔开。也可以保留字段命。

        未知字段:解析结构良好的protocolbuffer已序列化数据中的未识别字段的表⽰⽅式。例如,当 旧程序解析带有新字段的数据时,这些新字段就会成为旧程序的未知字段。

7. 前后兼容性

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

        • 向前兼容:⽼模块能够正确识别新模块⽣成或发出的协议。这时新增加的“⽣⽇”属性会被当作未 知字段(pb3.5版本及之后)。

        • 向后兼容:新模块也能够正确识别⽼模块⽣成或发出的协议。

        前后兼容的作⽤:当我们维护⼀个很庞⼤的分布式系统时,由于你⽆法同时升级所有模块,为了保证 在升级过程中,整个系统能够尽可能不受影响,就需要尽量保证通讯协议的“向后兼容”或“向前兼 容”。

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

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

java_multiple_files:编译后⽣成的⽂件是否分为多个⽂件,该选项为⽂件选项。

java_package:编译后⽣成⽂件所在的包路径,该选项为⽂件选项。

java_outer_classname:编译后⽣成的proto包装类的类名,该选项为⽂件选项。

allow_alias:允许将相同的常量值分配给不同的枚举常量,⽤来定义别名。该选项为枚举选项。

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

需求如下:
         客户端:向服务端发送联系人信息,并接收服务端返回的响应。

        服务端:接收到联系人信息后,将结果打印出来
        客户端、服务端间的交互数据使用Protobuf来完成

代码环境:Maven项⽬+ UDP数据报套接字编程

客户端和服务器交互逻辑:

代码和结果略。

9. 总结

序列化能⼒对⽐

         • 编解码性能:ProtoBuf>jackson>fastjson2。

        • 内存占⽤:ProtoBuf>jackson~=fastjson2。

all in all:

⼩结:

        1. XML、JSON、ProtoBuf都具有数据结构化和数据序列化的能⼒。

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

        3. ProtoBuf的应⽤场景更为明确,XML、JSON的应⽤场景更为丰富。

ps:本文仅用来自己整理学习时的笔记。 

相关文章:

  • 算法刷题记录——LeetCode篇(6) [第501~600题](持续更新)
  • 聊聊langchain4j的Tools(Function Calling)
  • mybatis集合映射association与collection
  • 常用的遍历方法用途和运用
  • QT学习笔记1
  • 【在数轴上找最优位置,使移动距离最短】
  • 【区块链 + 商贸零售】商小萌小程序 | FISCO BCOS 应用案例
  • uniapp路由跳转导致页面堆积问题
  • 51单片机和STM32 入门分析
  • RSA后台解密报错:javax.crypto.BadPaddingException: Message is larger than modulus
  • 4.1--入门知识扫盲,ISO知识体系介绍(看一遍,协议啥的全部记住)
  • Android Zygote的进程机制
  • nginx配置txt文件点击链接后下载
  • 【ES6新特性】默认参数常见用法
  • (C语言)斐波那契数列(递归求解)
  • uniapp-x vue 特性
  • 通过 API 将Deepseek响应流式内容输出到前端
  • 论文精度:Transformers without Normalization
  • 提示词模板
  • KNN算法性能优化技巧与实战案例
  • 美官方将使用华为芯片视作违反美出口管制行为,外交部回应
  • 湃书单|澎湃新闻编辑们在读的14本书:后工作时代
  • “行人相撞案”现场视频公布,法院:表述不当造成误导
  • 昆明一学校门外小吃摊占满人行道,城管:会在重点时段加强巡查处置
  • 重庆荣昌出圈背后:把网络流量变成经济发展的增量
  • 国家主席习近平在莫斯科出席红场阅兵式