1. 概述
本文将深入探讨 Google Protocol Buffers(protobuf)中的 packed repeated 字段。Protocol Buffers 是一种高效的语言中立、平台中立的数据结构序列化方案。在 protobuf 中,repeated
关键字用于定义可容纳多个值的字段。
为提升重复字段的序列化效率,protobuf 引入了 packed
选项。它通过特殊编码技术进一步压缩消息体积,本文将详细解析其原理与实现。
2. 重复字段
在讨论 packed
选项前,我们先理解 repeated
标签的含义。以 repeated.proto
文件为例:
syntax = "proto3";
option java_multiple_files = true;
option java_package = "com.baeldung.grpc.repeated";
package repeated;
message PackedOrder {
int32 orderId = 1;
repeated int32 productIds = 2 [packed = true];
}
message UnpackedOrder {
int32 orderId = 1;
repeated int32 productIds = 2 [packed = false];
}
service OrderService {
rpc createOrder(UnpackedOrder) returns (UnpackedOrder){}
}
该文件定义了两个消息类型 PackedOrder
和 UnpackedOrder
,以及一个 OrderService
服务。**productIds
字段的 repeated
标签表明它可存储多个整数值,类似于集合或数组**。自 protobuf v2.1.0 起,重复字段默认启用 packed
选项。为演示效果,我们显式设置 packed = false
。
✅ 关键特性:
- 修改
packed
选项无需调整业务代码 - 仅影响序列化时的内部编码方式
实现 OrderService
的 RPC 方法:
public class OrderService extends OrderServiceGrpc.OrderServiceImplBase {
@Override
public void createOrder(UnpackedOrder unpackedOrder, StreamObserver<UnpackedOrder> responseObserver) {
List productIds = unpackedOrder.getProductIdsList();
if(validateProducts(productIds)) {
int orderID = insertOrder(unpackedOrder);
UnpackedOrder createdUnpackedOrder = UnpackedOrder.newBuilder(unpackedOrder)
.setOrderId(orderID)
.build();
responseObserver.onNext(createdUnpackedOrder);
responseObserver.onCompleted();
}
}
}
protoc 插件会自动生成 getProductIdsList()
方法获取重复字段元素列表,无论是否启用 packed
选项。最后生成订单 ID 并返回客户端。
调用 RPC 的测试代码:
@Test
void whenUnpackedRepeatedProductIds_thenCreateUnpackedOrderAndInvokeRPC() {
UnpackedOrder.Builder unpackedOrderBuilder = UnpackedOrder.newBuilder();
unpackedOrderBuilder.setOrderId(1);
Arrays.stream(fetchProductIds()).forEach(unpackedOrderBuilder::addProductIds);
UnpackedOrder unpackedOrderRequest = unpackedOrderBuilder.build();
UnpackedOrder unpackedOrderResponse = orderClientStub.createOrder(unpackedOrderRequest);
assertInstanceOf(Integer.class, unpackedOrderResponse.getOrderId());
}
编译时 protoc 插件会为 UnpackedOrder
生成 Java 类。通过流式处理调用 addProductIds()
填充重复字段。所有重复字段(无论是否打包)都会生成前缀为 add
的方法,最后调用 RPC 获取订单 ID。
3. 打包重复字段
打包重复字段的核心差异在于序列化前的编码方式。先看序列化方法:
void serializeObject(String file, GeneratedMessageV3 object) throws IOException {
try(FileOutputStream fileOutputStream = new FileOutputStream(file)) {
object.writeTo(fileOutputStream);
}
}
该方法调用 GeneratedMessageV3
的 writeTo()
方法将对象序列化到文件。PackedOrder
和 UnpackedOrder
均继承此方法。
对比序列化后的文件大小:
@Test
void whenSerializeUnpackedOrderAndPackedOrderObject_thenSizeofPackedOrderObjectIsLess() throws IOException {
UnpackedOrder.Builder unpackedOrderBuilder = UnpackedOrder.newBuilder();
unpackedOrderBuilder.setOrderId(1);
Arrays.stream(fetchProductIds()).forEach(unpackedOrderBuilder::addProductIds);
UnpackedOrder unpackedOrder = unpackedOrderBuilder.build();
String unpackedOrderObjFileName = FOLDER_TO_WRITE_OBJECTS + "unpacked_order.bin";
serializeObject(unpackedOrderObjFileName, unpackedOrder);
PackedOrder.Builder packedOrderBuilder = PackedOrder.newBuilder();
packedOrderBuilder.setOrderId(1);
Arrays.stream(fetchProductIds()).forEach(packedOrderBuilder::addProductIds);
PackedOrder packedOrder = packedOrderBuilder.build();
String packedOrderObjFileName = FOLDER_TO_WRITE_OBJECTS + "packed_order.bin";
serializeObject(packedOrderObjFileName, packedOrder);
long sizeOfUnpackedOrderObjectFile = getFileSize(unpackedOrderObjFileName);
long sizeOfPackedOrderObjectFile = getFileSize(packedOrderObjFileName);
long sizeReductionPercentage = (sizeOfUnpackedOrderObjectFile - sizeOfPackedOrderObjectFile) * 100/sizeOfUnpackedOrderObjectFile;
logger.info("Packed field saved {}% over unpacked field", sizeReductionPercentage);
assertTrue(sizeOfUnpackedOrderObjectFile > sizeOfPackedOrderObjectFile);
}
创建包含相同产品 ID 的 unpackedOrder
和 packedOrder
对象,序列化后比较文件大小。结果符合预期:未打包对象文件大于打包对象文件。
控制台输出:
Packed field saved 29% over unpacked field
本例中 20 个产品 ID 的打包版本节省了 29% 空间。随着元素数量增加,压缩率会进一步提升并趋于稳定。
⚠️ 注意:packed
选项仅适用于基本数值类型。
4. 编码对比:未打包 vs 打包字段
使用 protoscope 工具 检查序列化文件内容。protoscope 是一种可读的文本格式,用于展示 protobuf 底层线缆格式。
查看 unpacked_order.bin
:
#cat unpacked_order.bin | protoscope -explicit-wire-types
1:VARINT 1
2:VARINT 266
2:VARINT 629
2:VARINT 725
2:VARINT 259
2:VARINT 353
2:VARINT 746
more elements...
输出以键值对展示字段编号和值。**productId
(字段编号 2)的每个值都作为独立 VARINT
线缆类型编码**,即每条键值对记录单独编码。
查看 packed_order.bin
:
#cat packed_order.bin | protoscope -explicit-wire-types -explicit-length-prefixes
1:VARINT 1
2:LEN 38 `fc06c0058e047293069702ea04c203ba0165c005d601da02dc02a307a804f101ca019a02df03`
启用 packed
选项后,所有值被整体编码为单个 LEN
线缆记录(38 字节):
fc 06 c0 05 8e 04 72 93 06 97 02 ea 04 c2 03 ba 01 65 c0 05 d6 01 da 02 dc 02 a3 07 a8 04 f1 01 ca 01 9a 02 df 03
📌 编码细节可参考:
5. 总结
本文深入分析了 protobuf 中重复字段的 packed
选项:
- 打包字段通过整体编码显著减少体积
- 提升序列化性能
- 仅适用于基本数值类型(VARINT/I32/I64)
完整代码示例见 GitHub 仓库。