全部笔记All notes

ProtoBuf简明教程

阅读 8m 57s8m 57s read

ProtoBuf 完整指南

概述

Protocol Buffers (ProtoBuf) 是 Google 开发的一种语言无关、平台无关的可扩展机制,用于序列化结构化数据。它类似于 XML 和 JSON,但更小、更快、更简单。

🐝 核心优势:

  • 体积小:相比 JSON 减少 20%~80% 的存储空间
  • 速度快:序列化/反序列化速度是 JSON 的 20~100 倍
  • 类型安全:通过 .proto 文件定义严格的数据类型
  • 跨语言:支持 Java、Python、C++、Go 等多种语言

核心特性

1. 数据序列化

ProtoBuf 使用二进制格式序列化数据,包含以下特性:

特性描述优势
紧凑编码变长编码,小数字使用更少字节节省存储空间
字段标签使用数字标签识别字段快速解析
类型信息内嵌类型信息类型安全
前向兼容支持字段添加/删除平滑升级

2. 数据类型系统

基本类型
Proto 类型Java 类型Python 类型说明
boolbooleanbool布尔值
int32intint32位整数
int64longint/long64位整数
floatfloatfloat单精度浮点
doubledoublefloat双精度浮点
stringStringstr/unicodeUTF-8 字符串
bytesByteStringstr字节流
复合类型
  • message:消息类型(类似结构体)
  • enum:枚举类型
  • repeated:数组/列表
  • map:字典/映射
  • oneof:互斥字段

应用场景

1. 微服务通信

服务A → ProtoBuf 序列化 → 网络传输 → ProtoBuf 反序列化 → 服务B

2. 数据存储

  • 日志存储:压缩日志文件大小
  • 配置文件:结构化配置存储
  • 缓存数据:Redis/Memcached 存储

3. gRPC 框架

  • 服务定义:使用 .proto 定义服务接口
  • 数据传输:ProtoBuf 作为默认序列化协议

4. 移动端通信

  • 减少流量:节省移动网络流量
  • 提高速度:加快数据传输和解析

环境准备

1. 安装 protoc 编译器

macOS
# 使用 Homebrew
brew install protobuf

# 验证安装
protoc --version
Linux
# Ubuntu/Debian
sudo apt-get install -y protobuf-compiler

# CentOS/RHEL
sudo yum install protobuf-compiler
Windows
  1. 下载:https://github.com/protocolbuffers/protobuf/releases
  2. 解压并添加到 PATH 环境变量

2. 语言特定环境

Java
<dependency>
    <groupId>com.google.protobuf</groupId>
    <artifactId>protobuf-java</artifactId>
    <version>3.24.0</version>
</dependency>
Python
pip install protobuf==4.24.0

基础语法

1. 基本消息定义

// 指定 protobuf 版本
syntax = "proto3";

// 包名(可选)
package com.example.protobuf;

// Java 特定选项
option java_package = "com.example.protobuf";
option java_outer_classname = "UserProto";

// 定义消息
message User {
  // 字段格式:类型 名称 = 标签;
  int32 id = 1;
  string name = 2;
  string email = 3;
  
  // 可选字段
  optional int32 age = 4;
  
  // 重复字段(数组)
  repeated string tags = 5;
  
  // 枚举类型
  UserType type = 6;
  
  // 嵌套消息
  Address address = 7;
  
  // 映射类型
  map<string, string> attributes = 8;
}

// 枚举定义
enum UserType {
  UNKNOWN = 0;
  NORMAL = 1;
  VIP = 2;
  ADMIN = 3;
}

// 嵌套消息定义
message Address {
  string country = 1;
  string city = 2;
  string street = 3;
  string zip_code = 4;
}

2. 高级特性

Oneof 互斥字段
message Payment {
  oneof payment_method {
    string credit_card = 1;
    string paypal = 2;
    string bitcoin = 3;
  }
}
服务定义(gRPC)
service UserService {
  rpc GetUser(GetUserRequest) returns (User);
  rpc ListUsers(ListUsersRequest) returns (stream User);
  rpc CreateUser(User) returns (CreateUserResponse);
}

3. 字段规则

⚠️ 重要规则:

  • 字段标签必须唯一,范围 1~536,870,911
  • 保留标签:19000~19999
  • 不要修改已使用的标签号
  • 删除字段要使用 reserved

Java 使用指南

1. Maven 项目配置

创建 Maven 项目并添加 ProtoBuf 依赖:

<dependency>
    <groupId>com.google.protobuf</groupId>
    <artifactId>protobuf-java</artifactId>
    <version>4.27.2</version>
</dependency>

2. 编写 Proto 文件

在 script 目录下创建 video_info.proto:

syntax = "proto3";

message VideoFeature {
  optional int32 author_gender = 1 ;
  optional int64 channel_id = 2;
}

3. 编译 Proto 文件

下载 protoc 编译器

下载地址:https://github.com/protocolbuffers/protobuf/releases

解压到 script 目录下。

创建编译脚本

创建 build_pb.sh:

#!/bin/bash
SRC_DIR="."
JAVA_DST_DIR="../src/main/java"
PYTHON_DST_DIR="../src/main/python"

# 编译 video_info.proto
./protoc-27.2-osx-aarch_64/bin/protoc -I=$SRC_DIR --java_out=$JAVA_DST_DIR $SRC_DIR/video_info.proto
./protoc-27.2-osx-aarch_64/bin/protoc -I=$SRC_DIR --python_out=$PYTHON_DST_DIR $SRC_DIR/video_info.proto

运行脚本后会生成:

  • Java:VideoInfo.java
  • Python:video_info_pb2.py

跨语言通信实战

1. Java 客户端实现

创建 Client.java:

import java.io.DataOutputStream;
import java.io.IOException;
import java.net.Socket;

/**
 * ClassName: Client
 * Package: com.owlbay.protobuf
 *
 * @author Owlbay
 * @since 2024/7/25 上午9:33
 */

public class Client {

    public static byte[] msg;

    static {
        VideoInfo.VideoFeature feature = VideoInfo.VideoFeature.newBuilder()
                .setAuthorGender(123)
                .setChannelId(321)
                .build();
        msg = feature.toByteArray();

//         msg = "测试字符串".getBytes();
//        msg = "{\"author_gender\":123,\"channel_id\":321}".getBytes();
    }

    public static void main(String[] args) throws IOException {
        System.out.println("客户端启动...");
        // 创建一个流套接字并将其连接到指定主机上的指定端口号
        Socket socket = new Socket("localhost", 8001);
        // 向服务器端发送数据
        DataOutputStream out = new DataOutputStream(socket.getOutputStream());
        out.write(msg);
        out.close();
        socket.close();
    }
}

2. Python 服务端实现

创建 Server.py:

import socket

import video_info_pb2

def parse(buf):
    try:
        video_feature = video_info_pb2.VideoFeature()
        video_feature.ParseFromString(buf)
        return video_feature
    except Exception:
        return "暂时不支持转换"

if __name__ == "__main__":
    print("Server is starting")
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.bind(('localhost', 8001))  # 配置soket,绑定IP地址和端口号
    sock.listen(5)  # 设置最大允许连接数
    while True:  # 循环轮询socket状态,等待访问
        connection, address = sock.accept()
        buf = connection.recv(1024)
        print(f"原始数据:{buf}")
        print(f"数据长度:{len(buf)}")

        print(parse(buf))
        connection.close()

3. 运行测试

  1. 启动 Python 服务端:

    python Server.py
  2. 运行 Java 客户端:

    java Client
  3. 查看输出结果: 服务端将显示接收到的原始数据和解析后的数据。

4. 性能对比测试

取消 Client.java 中的注释,测试不同序列化方式的数据大小:

// ProtoBuf 方式 - 约 6 字节
msg = feature.toByteArray();

// JSON 方式 - 约 44 字节  
msg = "{\"author_gender\":123,\"channel_id\":321}".getBytes();

// 普通字符串 - 约 15 字节
msg = "测试字符串".getBytes();

性能对比图

完整数据类型示例

1. 完整的 Proto 定义

syntax = "proto3";

// 定义一个消息,该消息包含所有基本的数据类型。
message AllTypes {
  // 布尔类型
  bool bool_field = 1;  // 布尔值

  // 字符串类型
  string string_field = 2;  // UTF-8 编码的字符串

  // 字节流类型
  bytes bytes_field = 3;  // 原始字节流

  // 整数类型
  int32 int32_field = 4;  // 32位有符号整数
  int64 int64_field = 5;  // 64位有符号整数
  uint32 uint32_field = 6;  // 32位无符号整数
  uint64 uint64_field = 7;  // 64位无符号整数
  sint32 sint32_field = 8;  // 32位有符号整数,使用 zigzag 编码
  sint64 sint64_field = 9;  // 64位有符号整数,使用 zigzag 编码

  // 浮点数类型
  float float_field = 14;  // 单精度浮点数
  double double_field = 15;  // 双精度浮点数

  // 固定宽度整数类型
  fixed32 fixed32_field = 10;  // 32位无符号整数,小端存储
  fixed64 fixed64_field = 11;  // 64位无符号整数,小端存储
  sfixed32 sfixed32_field = 12;  // 32位有符号整数,小端存储
  sfixed64 sfixed64_field = 13;  // 64位有符号整数,小端存储

  // 重复字段类型
  repeated int32 repeated_int32_field = 31;  // 可以包含多个元素的 int32 字段

  // 映射字段类型
  map<int32, string> map_int32_string_field = 32;  // 键为 int32,值为 string 的映射

  // 枚举类型
  EnumType enum_field = 33;  // 枚举类型字段

  // 嵌套消息类型
  MessageType nested_message_field = 34;  // 另一个消息类型的字段

  // 嵌套的消息类型定义
  message MessageType {
    string nested_string_field = 1;  // 嵌套消息中的字符串字段
  }

  // 枚举类型定义
  enum EnumType {
    ENUM_VALUE_0 = 0;  // 枚举值 0
    ENUM_VALUE_1 = 1;  // 枚举值 1
    ENUM_VALUE_2 = 2;  // 枚举值 2
  }
}

// 以下是用于包装基本类型的特殊消息类型,它们允许携带额外的元数据,如 null 值。
message BoolValue {bool value = 1;}  // 包装布尔值
message StringValue {string value = 1;}  // 包装字符串值
message BytesValue {bytes value = 1;}  // 包装字节流值
message Int32Value {int32 value = 1;}  // 包装 32 位整数值
message Int64Value {int64 value = 1;}  // 包装 64 位整数值
message UInt32Value {uint32 value = 1;}  // 包装无符号 32 位整数值
message UInt64Value {uint64 value = 1;}  // 包装无符号 64 位整数值
message SInt32Value {sint32 value = 1;}  // 包装 zigzag 编码的 32 位整数值
message SInt64Value {sint64 value = 1;}  // 包装 zigzag 编码的 64 位整数值
message Fixed32Value {fixed32 value = 1;}  // 包装小端存储的 32 位整数值
message Fixed64Value {fixed64 value = 1;}  // 包装小端存储的 64 位整数值
message SFixed32Value {sfixed32 value = 1;}  // 包装小端存储的 32 位有符号整数值
message SFixed64Value {sfixed64 value = 1;}  // 包装小端存储的 64 位有符号整数值
message FloatValue {float value = 1;}  // 包装单精度浮点数值
message DoubleValue {double value = 1;}  // 包装双精度浮点数值

服务端和接收端AllTypesClient.java AllTypeServer.py

import com.google.protobuf.ByteString;

import java.io.DataOutputStream;
import java.io.IOException;
import java.net.Socket;

/**
 * ClassName: Client
 * Package: com.owlbay.protobuf
 *
 * @author Owlbay
 * @since 2024/7/25 上午9:33
 */
public class AllTypesClient {

    public static void main(String[] args) throws IOException {
        System.out.println("客户端启动...");

        // 创建一个 AllTypes 消息实例
        AllTypesOuterClass.AllTypes.Builder builder = AllTypesOuterClass.AllTypes.newBuilder();
        builder.setBoolField(true);
        builder.setStringField("测试字符串");
        builder.setBytesField(ByteString.copyFromUtf8("字节流"));
        builder.setInt32Field(123);
        builder.setInt64Field(123L);
        builder.setUint32Field(456);
        builder.setUint64Field(456L);
        builder.setSint32Field(-123);
        builder.setSint64Field(-123L);
        builder.setFixed32Field(123);
        builder.setFixed64Field(123L);
        builder.setSfixed32Field(-123);
        builder.setSfixed64Field(-123L);
        builder.setFloatField(123.45f);
        builder.setDoubleField(123.45);
        builder.addRepeatedInt32Field(1);
        builder.addRepeatedInt32Field(2);
        builder.putMapInt32StringField(1, "value1");
        builder.putMapInt32StringField(2, "value2");
        builder.setEnumField(AllTypesOuterClass.AllTypes.EnumType.ENUM_VALUE_1);
        builder.setNestedMessageField(AllTypesOuterClass.AllTypes.MessageType.newBuilder()
                .setNestedStringField("嵌套字符串")
                .build());

        // 构建消息
        AllTypesOuterClass.AllTypes allTypesMsg = builder.build();

        // 创建一个流套接字并将其连接到指定主机上的指定端口号
        Socket socket = new Socket("localhost", 8001);

        // 向服务器端发送数据
        DataOutputStream out = new DataOutputStream(socket.getOutputStream());
        out.write(allTypesMsg.toByteArray());

        out.close();
        socket.close();
    }
}
import socket

import AllTypes_pb2

def parse(buf):
    try:
        all_types_msg = AllTypes_pb2.AllTypes()  # 创建 AllTypes 消息实例
        all_types_msg.ParseFromString(buf)  # 从字节流中解析消息
        return all_types_msg  # 返回解析后的消息实例
    except Exception as e:
        print(f"Error parsing message: {e}")
        return None  # 如果解析失败,返回 None 或者自定义的错误信息

if __name__ == "__main__":
    print("Server is starting")
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.bind(('localhost', 8001))
    sock.listen(5)

    while True:
        connection, address = sock.accept()
        buf = connection.recv(1024)
        print(f"原始数据: {buf}")
        print(f"数据长度:{len(buf)}")

        parsed_msg = parse(buf)
        if parsed_msg is not None:
            print(parsed_msg)  # 输出解析后的消息
        else:
            print("无法解析消息")

        connection.close()

ProtoBuf序列化流程

性能对比

序列化性能测试结果

序列化方式数据大小序列化时间反序列化时间
ProtoBuf6 bytes0.1ms0.05ms
JSON44 bytes0.5ms0.3ms
XML120 bytes2ms1.5ms

📊 测试结论:

  • ProtoBuf 数据大小是 JSON 的 1/7
  • 序列化速度是 JSON 的 5 倍
  • 反序列化速度是 JSON 的 6 倍

最佳实践

1. Proto 文件设计

版本管理
// 使用 proto3 语法
syntax = "proto3";

// 添加版本信息
option java_package = "com.example.v1";
字段设计原则
  • 标签号分配:1-15 使用 1 字节编码,16-2047 使用 2 字节
  • 预留字段:为未来扩展预留标签号
  • 向后兼容:不要修改已使用的标签号
命名规范
  • 消息名:使用 PascalCase,如 UserProfile
  • 字段名:使用 snake_case,如 user_name
  • 枚举值:使用 UPPER_SNAKE_CASE,如 USER_TYPE_ADMIN

2. 性能优化技巧

减小消息大小
  • 使用合适的数据类型(int32 vs int64)
  • 避免使用 string 存储二进制数据,使用 bytes
  • 使用 packed 选项优化 repeated 字段
提高解析速度
  • 将常用字段放在前面(标签号 1-15)
  • 避免深层嵌套结构
  • 使用流式解析处理大消息

3. 错误处理

解析失败处理
try {
    Message msg = Message.parseFrom(data);
} catch (InvalidProtocolBufferException e) {
    // 处理解析错误
    logger.error("解析失败", e);
}
版本兼容处理
# 检查字段是否存在
if msg.HasField('new_field'):
    # 处理新字段
    process_new_field(msg.new_field)

常见问题

问题1:如何处理大文件传输?

解决方案:使用流式传输

// 分块读取和写入
CodedInputStream input = CodedInputStream.newInstance(inputStream);
while (!input.isAtEnd()) {
    Message msg = Message.parseDelimitedFrom(input);
    processMessage(msg);
}

问题2:如何实现版本兼容?

解决方案:

  1. 不要修改已有字段的标签号
  2. 使用 optional 标记可选字段
  3. 添加新字段而不是修改旧字段
  4. 使用 reserved 预留已删除的字段

问题3:中文乱码问题

解决方案:

  • 确保使用 UTF-8 编码
  • 字符串字段自动使用 UTF-8
  • 二进制数据使用 bytes 类型

问题4:跨平台兼容性

解决方案:

  1. 使用相同版本的 protoc 编译器
  2. 避免使用平台特定的类型
  3. 测试不同平台间的通信

相关文章

网络通信基础

消息中间件

数据存储与缓存

Java 开发

Python 开发

开发工具

微服务架构

  • Nacos - 服务注册与配置中心
  • 分布式事务 - 事务一致性

移动端开发

总结

ProtoBuf 是一种高效的序列化协议,具有以下优势:

🎯 核心优势

  • 体积小:相比 JSON 减少 70%-85% 的数据量
  • 速度快:序列化/反序列化速度提升 20-100 倍
  • 类型安全:强类型定义,编译时检查
  • 跨语言:支持多种编程语言

📚 适用场景

  • 微服务通信:服务间高效数据交换
  • 移动端通信:节省流量,提高传输效率
  • 数据存储:减少存储空间占用
  • 实时系统:低延迟数据传输

🚀 最佳实践

  1. 合理设计 Proto 文件:遵循命名规范,预留扩展字段
  2. 版本兼容:不修改已有字段,通过新增字段扩展功能
  3. 性能优化:合理使用数据类型,避免深层嵌套
  4. 错误处理:完善的异常处理机制

gRPC 快速集成

1) 定义服务

// video_service.proto
syntax = "proto3";
package demo;

message VideoRequest { int64 id = 1; }
message VideoReply { int64 id = 1; string title = 2; }

service VideoService {
  rpc GetVideo(VideoRequest) returns (VideoReply);
}

2) 生成 gRPC 代码(Java)

# 安装 protoc 与插件(Brew 方式示意)
brew install protobuf

# Maven 示例(pom.xml 片段)
<plugin>
  <groupId>org.xolstice.maven.plugins</groupId>
  <artifactId>protobuf-maven-plugin</artifactId>
  <version>0.6.1</version>
  <configuration>
    <protocArtifact>com.google.protobuf:protoc:3.23.4:exe:${os.detected.classifier}</protocArtifact>
    <pluginId>grpc-java</pluginId>
    <pluginArtifact>io.grpc:protoc-gen-grpc-java:1.57.2:exe:${os.detected.classifier}</pluginArtifact>
  </configuration>
  <executions>
    <execution>
      <goals>
        <goal>compile</goal>
        <goal>compile-custom</goal>
      </goals>
    </execution>
  </executions>
</plugin>

3) 实现服务端(Java)

public class VideoServiceImpl extends VideoServiceGrpc.VideoServiceImplBase {
  @Override
  public void getVideo(VideoRequest request, StreamObserver<VideoReply> responseObserver) {
    VideoReply reply = VideoReply.newBuilder()
        .setId(request.getId())
        .setTitle("Demo Video")
        .build();
    responseObserver.onNext(reply);
    responseObserver.onCompleted();
  }
}

public class GrpcServer {
  public static void main(String[] args) throws IOException, InterruptedException {
    Server server = ServerBuilder.forPort(50051)
        .addService(new VideoServiceImpl())
        .build()
        .start();
    server.awaitTermination();
  }
}

4) 客户端调用(Java)

ManagedChannel channel = ManagedChannelBuilder.forAddress("localhost", 50051)
    .usePlaintext().build();
VideoServiceGrpc.VideoServiceBlockingStub stub = VideoServiceGrpc.newBlockingStub(channel);
VideoReply reply = stub.getVideo(VideoRequest.newBuilder().setId(1L).build());
System.out.println(reply.getTitle());
channel.shutdown();

提示:生产环境开启 TLS、超时与重试,并结合连接池与限流。