Java中Protobuf中重複欄位的打包
1. 概述
在本教學中,我們將討論 Google 協定緩衝區 (protobuf) 訊息中的打包重複欄位。 Protocol Buffers 協助定義高度最佳化的語言中立和平台中立的資料結構,以實現極其高效的序列化。在 protobuf 中, repeated關鍵字有助於定義可以保存多個值的欄位。
此外,為了在重複欄位的序列化過程中實現更高的最佳化,protobuf 中引入了一個新的選項packed 。它應用特殊的編碼技術來進一步減小訊息的大小。
讓我們對此進行更多探索。
2. 重複字段
在討論repeated欄位上的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){}
}
該文件定義了兩種訊息類型 (DTO) PackedOrder和UnpackedOrder以及一個名為OrderService的服務。 productIds欄位上的repeated標籤強調它可以具有多個整數類型的值,類似於集合或陣列。從protobuf v2.1.0開始, packed選項預設為重複欄位為true 。因此,為了停用打包行為,我們現在明確使用選項packed = false來專注於重複功能。
有趣的是,如果我們修改重複欄位並添加packed = true選項,我們不需要調整程式碼即可使其工作。唯一的差異是內部 gRPC 庫在序列化期間如何對字段進行編碼。我們將在接下來的部分中討論這個問題。
讓我們定義具有 RPC createOrder()的OrderService :
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 Maven 外掛程式自動產生方法getProductIdsList()用於取得重複欄位中的元素清單。無論是打包字段還是未打包字段,這都適用。最後,我們將產生的orderID設定在UnpackedOrder物件中,並將其傳回給客戶端。
現在讓我們呼叫 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 Maven 插件編譯程式碼時,它會為 proto 檔案中定義的UnpackedOrder訊息類型產生 Java 類別檔案。我們在迭代 Stream 時多次呼叫addProductIds()方法,以填入UnpackedOrder物件中的重複欄位productIds 。一般來說,在 proto 檔案的編譯過程中,會為所有重複的欄位名稱建立一個類似的方法,並以文字add為前綴。這適用於所有重複字段,無論是打包還是未打包。
之後,我們呼叫 RPC createOrder()回傳欄位orderId 。
3. 重複字段打包
到目前為止,我們知道打包重複欄位與重複欄位的不同主要在於序列化之前的編碼過程。為了理解編碼技術,我們首先來看看如何序列化 proto 檔案中定義的PackedOrder和UnpackedOrder訊息類型:
void serializeObject(String file, GeneratedMessageV3 object) throws IOException {
try(FileOutputStream fileOutputStream = new FileOutputStream(file)) {
object.writeTo(fileOutputStream);
}
}
方法serializeObject()呼叫GeneratedMessageV3類型的物件中的writeTo()方法,將其序列化到檔案系統。
PackedOrder和UnpackedOrder訊息類型從其父GeneratedMessageV3類別繼承writeTo()方法。因此,我們將使用serializeObject()方法將它們的實例寫入檔案系統:
@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物件。然後,我們序列化這兩個物件並比較它們的檔案大小。該程式還使用productID的打包版本來計算物件中檔案大小的減少百分比。如預期的那樣,包含unpackedOrder物件的檔案比包含packedOrder物件的檔案大。
現在讓我們來看看程式的控制台輸出:
Packed field saved 29% over unpacked field
此範例具有 20 個產品 ID,表示packedOrder物件的檔案大小減少了29 %。此外,隨著產品 ID 的增加,節省的費用也會提高並最終穩定下來。
當然,打包重複欄位會帶來更好的效能。但是,我們只能對原始數字類型使用packed選項。
4. 編碼未打包字段與打包字段
之前,我們建立了兩個檔案unpacked_order.bin和packed_order.bin分別對應UnpackedOrder和PackedOrder物件。我們將使用protoscope 工具來檢查這兩個檔案的編碼內容。 Protscope 是一種簡單的、人類可編輯的語言,可以幫助我們查看傳輸中訊息的低階 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...
protoscope指令將編碼的協定緩衝區轉儲為文字。在文字中,欄位及其值以鍵值格式表示,其中鍵是repeated.proto檔案中定義的欄位編號。具有鍵2的productId欄位會重複,每個值都表示為VARINT有線格式類型。這意味著鍵值對定義的每筆記錄都是單獨編碼的。
同樣,讓我們看看 protoscope 文字格式的packed-order.bin的內容:
#cat packed_order.bin | protoscope -explicit-wire-types -explicit-length-prefixes
1:VARINT 1
2:LEN 38 `fc06c0058e047293069702ea04c203ba0165c005d601da02dc02a307a804f101ca019a02df03`
有趣的是,一旦我們在productId欄位上啟用了packed選項,gRPC庫就會將它們編碼在一起以進行序列化。它將其表示為具有 38 個十六進位位元組的單一LEN有線格式記錄:
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
我們不會討論protobuf 訊息的編碼,因為官方網站已經詳細介紹了它。我們也可以參考其他網站來詳細了解編碼演算法。
5. 結論
在本文中,我們探討了 protobuf 中重複欄位的packed選項。壓縮字段的元素被編碼在一起,因此它們的大小大大減少。這可以透過更快的序列化來提高效能。需要注意的是,我們只能將原始數位連線類型(例如VARINT 、 I32或I64型別)宣告為packed 。
與往常一樣,本文中使用的程式碼可以在 GitHub 上找到。