Protocol Buffers!高效数据通信协议

Protocol Buffers!高效数据通信协议

前言

在现代软件开发中,尤其是在分布式系统、微服务架构和跨平台应用中,不同组件或服务之间需要进行高效且可靠的数据交换

传统的数据格式如XML和JSON虽然易于人类阅读,但在序列化和反序列化的效率上存在不足,特别是在处理大量小数据包时性能问题尤为突出

这种场景下,Google开源的Protocol Buffers(简称Protobuf)便成为了众多开发者的选择

Protobuf是一种语言中立、平台无关、可扩展 的机制,用于序列化结构化数据

它通过定义一个描述消息类型的语言来支持多种编程语言之间的数据交换,并且能够提供比XML和JSON更紧凑的数据存储以及更快的解析速度

本文将详细介绍Protobuf的优缺点、使用场景,同时也会给出详细的使用教程及与JSON的性能比较案例,帮助读者更好地理解和运用这一强大序列化方式

背景

Protocol Buffers 最初由Google设计并实现,旨在解决内部大规模数据传输的问题

随着技术的发展,Protobuf逐渐发展成为一个开源项目,并被广泛应用于各种需要高效数据交换的应用场景中

作为一种二进制格式,Protobuf不仅能够有效地减少数据传输量,还能够在不同的编程语言之间无缝转换,使得它成为构建跨语言服务的理想选择

Protobuf的核心思想是首先定义数据结构(称为"message"),然后使用Protobuf编译器根据这些定义生成对应于目标语言的源代码

这种设计允许开发者专注于业务逻辑而非底层的数据传输细节,极大地提高了开发效率

特点

优点

  • 更紧凑的数据存储:相比JSON和XML,Protobuf采用二进制格式,占用空间更小
  • 更快的解析速度:优化了编码解码过程,加快了数据处理速度
  • 跨语言的兼容性:支持多种主流编程语言,便于构建跨语言的服务架构
  • 定义一次类 自动生成 多语言代码:只需维护一套接口定义文件即可自动生成各语言的源代码
  • 设计考虑兼容性,版本升级不影响:新增字段不会影响旧版本的程序运行,保证了系统的向前兼容性

缺点

  • 不适合处理单个超大数据量:Protobuf不支持压缩,且内存中可能出现多副本,导致内存消耗较大
  • 二进制无法直接比较两个数据是否相等:需先解析再比较,增加了额外的操作步骤
  • 对科学计算的大规模多维浮点数组场景支持不佳:对于特定领域如科学计算中的大规模矩阵运算等场景,Protobuf的表现不如专门的库
  • 为面向对象语言设计,非面向对象语言支持差:某些特性可能在非面向对象语言中难以充分利用
  • 消息不含结构信息,需要定义文件来解析 :必须依赖预先定义好的.proto文件才能正确解析接收到的消息内容

根据以上特点,我们可以总结出Protobuf的适用和不适用场景:

  1. 追求更快的性能、占用更小的空间、跨语言适用
  2. 大量小数据包适用,而不是单个超大数据包不适用(比如:大日志包)
  3. 需要可视化、直接比较数据是否相同、多维浮点数组科学计算、无法先定义消息结构等场景不适用

工作流程

ProtoBuf工作流程如下:

  1. 先定义ProtoBuf源文件数据结构 .proto
  2. 再通过ProtoBuf编译器生成目标语言的源代码(如Java)
  3. 使用代码进行序列化与序列化(使用前编译)

使用教程

安装ProtoBuf编译器

ProtoBuf编译器用于将ProtoBuf源代码编译成目标语法源代码,记住安装的目录

官网安装教程: protobuf.dev/installatio...

Maven配置

案例中使用Java Maven项目,先导入Maven依赖:

xml 复制代码
<dependencies>
    <!-- Protobuf runtime -->
    <dependency>
        <groupId>com.google.protobuf</groupId>
        <artifactId>protobuf-java</artifactId>
        <version>3.25.3</version>
    </dependency>

    <!-- Jackson JSON -->
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-databind</artifactId>
        <version>2.17.0</version>
    </dependency>
</dependencies>

JSON依赖是用于比较性能测试的

同时需要自定义Maven任务,在编译阶段通过ProtoBuf编译器编译出Java源代码

xml 复制代码
<build>
    <!-- Maven 构建配置 -->
    <plugins>
        <!-- 执行自定义构建任务 -->
        <plugin>
            <artifactId>maven-antrun-plugin</artifactId>
            <version>3.1.0</version>
            <executions>
                <!-- 执行配置:生成 Protobuf 的 Java 源代码 -->
                <execution>
                    <id>generate-protobuf-sources</id>
                    <!-- 绑定到 Maven 生命周期的 generate-sources 阶段 -->
                    <phase>generate-sources</phase>
                    <configuration>
                        <target>
                            <!-- 执行 protoc 编译器生成 Java 代码 -->
                            <exec executable="D:\protobuf\bin\protoc.exe">
                                <!-- 指定 proto 文件搜索路径 -->
                                <arg value="--proto_path=src/main/java/protobuf/proto"/>
                                <!-- 指定 Java 输出目录 -->
                                <arg value="--java_out=src/main/java"/>
                                <!-- 列出需要编译的 .proto 文件 -->
                                <arg value="src/main/java/protobuf/proto/Product.proto"/>
                            </exec>
                        </target>
                    </configuration>
                    <goals>
                        <!-- 在 run 目标下执行 -->
                        <goal>run</goal>
                    </goals>
                </execution>
            </executions>
        </plugin>
    </plugins>
</build>

(也可以使用安装Maven protobuf 插件的形式)

定义Protobuf源文件

protobuf 复制代码
// 使用proto3
syntax = "proto3";

// 指定生成 Java 文件的包名
option java_package = "protobuf.model";
// 指定生成的 Java 外部类名(用于包裹所有 message)
option java_outer_classname = "ProductProto";
package protobuf.model;

// 商品信息的 protobuf 定义
message Product {
  // 商品唯一标识
  int32 id = 1;

  // 商品名称
  string name = 2;

  // 商品描述
  string description = 3;

  // 商品价格,单位为分(避免浮点误差)
  int32 price = 4;

  // 商品是否上架
  bool available = 5;
}

编译Protobuf并使用

使用Maven 的clear compile时,会执行定义的任务使用protobuf编译器编译源文件生成Java源文件ProductProto

Protobuf的序列化与反序列化常用于网络IO中,为了简单演示使用的是本地OS的文件IO,Java代码如下:

java 复制代码
package protobuf.test;


import protobuf.model.ProductProto;

import java.io.FileOutputStream;
import java.io.FileInputStream;

public class ProductApp {
    public static void main(String[] args) throws Exception {
        // 1. 创建一个 Product 实例
        ProductProto.Product product = ProductProto.Product.newBuilder()
                .setId(1001)
                .setName("无线鼠标")
                .setDescription("高精度无线鼠标,适用于办公和游戏")
                .setPrice(2599) // 单位:分
                .setAvailable(true)
                .build();


        // 2. 序列化到文件
        try (FileOutputStream fos = new FileOutputStream("product.bin")) {
            product.writeTo(fos);
            System.out.println("✅ Product 已序列化至 product.bin");
        }

        // 3. 从文件反序列化
        ProductProto.Product deserializedProduct;
        try (FileInputStream fis = new FileInputStream("product.bin")) {
            deserializedProduct = ProductProto.Product.parseFrom(fis);
            System.out.println("✅ 反序列化成功!");
        }

        // 4. 打印反序列化的对象
        System.out.println("📦 商品信息:");
        System.out.println("ID: " + deserializedProduct.getId());
        System.out.println("名称: " + deserializedProduct.getName());
        System.out.println("描述: " + deserializedProduct.getDescription());
        System.out.println("价格: " + (deserializedProduct.getPrice() / 100.0) + " 元");
        System.out.println("是否上架: " + (deserializedProduct.getAvailable() ? "是" : "否"));

//        byte[] bytes = product.toByteArray();

    }
}

运行的结果如下:

java 复制代码
✅ Product 已序列化至 product.bin
✅ 反序列化成功!
📦 商品信息:
ID: 1001
名称: 无线鼠标
描述: 高精度无线鼠标,适用于办公和游戏
价格: 25.99 元
是否上架: 是

如果是网络IO,可以通过调用toByteArray方法获取byte[]再去传输

JSON性能比较

使用JSON与Protobuf比较序列化、反序列化时的性能,Java代码如下:

java 复制代码
package protobuf.test;

import com.fasterxml.jackson.databind.ObjectMapper;
import protobuf.model.ProductJson;
import protobuf.model.ProductProto;

public class ComparePerformance {
    public static void main(String[] args) throws Exception {
        ObjectMapper mapper = new ObjectMapper();

        // JSON 对象
        ProductJson jsonObj = new ProductJson();
        jsonObj.id = 1;
        jsonObj.name = "Protobuf手机";
        jsonObj.description = "性能强劲";
        jsonObj.price = 199900;
        jsonObj.available = true;

        // Protobuf 对象
        ProductProto.Product protoObj = ProductProto.Product.newBuilder()
                .setId(1)
                .setName("Protobuf手机")
                .setDescription("性能强劲")
                .setPrice(199900)
                .setAvailable(true)
                .build();

        int loop = 100_000;
        byte[] jsonBytes = null;
        byte[] protoBytes = null;

        // warmup
        for (int i = 0; i < 1000; i++) {
            mapper.writeValueAsBytes(jsonObj);
            protoObj.toByteArray();
        }

        // JSON 序列化
        long t1 = System.nanoTime();
        for (int i = 0; i < loop; i++) {
            jsonBytes = mapper.writeValueAsBytes(jsonObj);
        }
        long t2 = System.nanoTime();

        // Protobuf 序列化
        long t3 = System.nanoTime();
        for (int i = 0; i < loop; i++) {
            protoBytes = protoObj.toByteArray();
        }
        long t4 = System.nanoTime();

        // JSON 反序列化
        long t5 = System.nanoTime();
        for (int i = 0; i < loop; i++) {
            mapper.readValue(jsonBytes, ProductJson.class);
        }
        long t6 = System.nanoTime();

        // Protobuf 反序列化
        long t7 = System.nanoTime();
        for (int i = 0; i < loop; i++) {
            ProductProto.Product.parseFrom(protoBytes);
        }
        long t8 = System.nanoTime();

        System.out.println("==== 性能对比(" + loop + " 次) ====");
        System.out.println("✅ JSON    序列化耗时: " + (t2 - t1) / 1_000_000 + " ms");
        System.out.println("✅ Protobuf序列化耗时: " + (t4 - t3) / 1_000_000 + " ms");
        System.out.println("✅ JSON    反序列化耗时: " + (t6 - t5) / 1_000_000 + " ms");
        System.out.println("✅ Protobuf反序列化耗时: " + (t8 - t7) / 1_000_000 + " ms");

        System.out.println("📦 JSON 数据大小:     " + jsonBytes.length + " 字节");
        System.out.println("📦 Protobuf 数据大小: " + protoBytes.length + " 字节");
    }
}

结果如下:

java 复制代码
==== 性能对比(100000 次) ====
✅ JSON    序列化耗时: 207 ms
✅ Protobuf序列化耗时: 45 ms
✅ JSON    反序列化耗时: 373 ms
✅ Protobuf反序列化耗时: 68 ms
📦 JSON 数据大小:     93 字节
📦 Protobuf 数据大小: 38 字节

可以发现在10W次的序列化与反序列化下,Protobuf的性能比JSON要快了几倍,并且数据也更加紧凑

更多用法可查看官网: protobuf.dev/programming...

总结

Protobuf是一种性能高效、占用空间紧凑、跨语言平台无关的二进制高效数据通信协议

使用Protobuf时先定义消息结构,再通过编译器编译为其他语言的源代码,再进行使用

Protobuf适用于分布式系统中,跨语言、大量小数据包通信的场景

由于Protobuf是二进制的数据流,因此可视性和直接比较数据的场景不适用

并且Protobuf无法被压缩,单个大数据量(比如大日志)的场景下也不适用

其他无法先定义消息结构,非面向对象语言,多维浮点数组的场景也不太适用

最后(一键三连求求拉~)

😁我是菜菜,热爱技术交流、分享与写作,喜欢图文并茂、通俗易懂的输出知识

📚在我的博客中,你可以找到Java技术栈的各个专栏:Java并发编程与JVM原理、Spring和MyBatis等常用框架及Tomcat服务器的源码解析,以及MySQL、Redis数据库的进阶知识,同时还提供关于消息中间件和Netty等主题的系列文章,都以通俗易懂的方式探讨这些复杂的技术点

🏆除此之外,我还是掘金优秀创作者、腾讯云年度影响力作者、华为云年度十佳博主....

👫我对技术交流、知识分享以及写作充满热情,如果你愿意,欢迎加我一起交流(vx:CaiCaiJava666),也可以持续关注我的公众号:菜菜的后端私房菜,我会分享更多技术干货,期待与更多志同道合的朋友携手并进,一同在这条充满挑战与惊喜的技术之旅中不断前行

🤝如果觉得菜菜写的不错,可以点赞、关注、收藏支持一下~

📖本篇文章被收入专栏 Java,感兴趣的同学可以持续关注喔

📝本篇文章笔记以及案例被收入 Gitee-CaiCaiJavaGithub-CaiCaiJava,除此之外还有更多Java进阶相关知识,感兴趣的同学可以starred持续关注喔~

相关推荐
RainbowSea14 分钟前
Windows 安装 RabbitMQ 消息队列超详细步骤(附加详细操作截屏)
后端
RainbowSea15 分钟前
Windows 11家庭版安装 Docker
后端
_码农1213816 分钟前
Spring IoC容器与Bean管理
java·后端·spring
哈基咩27 分钟前
Go 语言模糊测试 (Fuzz Testing) 深度解析与实践
开发语言·后端·golang
mCell27 分钟前
告别轮询!深度剖析 WebSocket:全双工实时通信原理与实战
后端·websocket·http
元气少女小圆丶29 分钟前
Mirror学习笔记
java·开发语言·学习
haruma sen36 分钟前
Spring面试
java·spring·面试
孫治AllenSun40 分钟前
【Java】使用模板方法模式设计EasyExcel批量导入导出
java·python·模板方法模式
天机️灵韵1 小时前
开源医院信息管理系统:基于若依框架的智慧医疗解决方案
java·开发语言·spring boot·spring cloud·github·开源项目
ClouGence1 小时前
从达梦到 StarRocks:国产数据库实时入仓实践
数据库·后端