📝

Protocol Buffersのwire formatを直接触ってみる

2024/05/13に公開

Protocol Buffersは基本的にwire formatというバイナリ形式でシリアライズされます。
https://protobuf.dev/programming-guides/encoding/

wire formatを直接触ってどうなるかをGoで試してみました。

protoは下記の定義を使いました。

syntax = "proto3";

option go_package = "/my_proto";

enum Status {
  STATUS_ACTIVE = 0;
  STATUES_INACTIVE = 1;
}

message Param1 {
  int32 id = 1;
  string name = 2;
  Status status = 3;
}

message Param2 {
  int32 id_2 = 1;
  string name_2 = 2;
  Status status_2 = 3;
}

wire formatはfieldの型が同じであれば基本マッピングできるはずなので、Param1のメッセージをParam2であってもシリアライズできるはずなので、下記コードは動きます。

package main

import (
	"fmt"
	"io"
	"os"
	"proto_sample/my_proto"

	"google.golang.org/protobuf/proto"
)

func main() {
	p1 := my_proto.Param1{
		Id:     1,
		Name:   "gopher",
		Status: my_proto.Status_STATUES_INACTIVE,
	}
	fmt.Println("P1 Unmarshaled data:", p1.String())

	file, err := os.Create("output.bin")
	if err != nil {
		fmt.Println("ファイルの作成に失敗しました:", err)
		return
	}
	defer file.Close()

	data, err := proto.Marshal(&p1)
	if err != nil {
		fmt.Println("データのマーシャリングに失敗しました:", err)
		return
	}
	_, err = file.Write(data)
	if err != nil {
		fmt.Println("ファイルへの書き込みに失敗しました:", err)
		return
	}

	fr, err := os.Open("output.bin")
	if err != nil {
		return
	}
	defer fr.Close()

	fileData, err := io.ReadAll(fr)
	if err != nil {
		return
	}

	p2 := my_proto.Param2{}
	proto.Unmarshal(fileData, &p2)
	fmt.Println("Unmarshaled data:", p2.String())
}

結果は下記の通り

p1 Unmarshaled data: id:1  name:"gopher"  status:STATUES_INACTIVE
p2 Unmarshaled data: id_2:1  name_2:"gopher"  status_2:STATUES_INACTIVE

field名を変えてもシリアライズは可能です。

生成したバイナリを見てみます。

% xxd output.bin
00000000: 0801 1206 676f 7068 6572 1801            ....gopher..

wire formatはkey-valueになっていて、keyの部分は、TLVになっていて、field_numberとwire typeなどが格納されている。
ロジックは下記を参照してください。
https://protobuf.dev/programming-guides/encoding/#structure

最初の08は field_number 1で wire_type VARIANT(0)を表し、次の01はint32の値を示す。
12 は field_number 2で wire_type LEN(2)を表していて、次の 06 は長さ、そして6バイトの gopher の文字が入ります。

gopherは ASCII文字コードで表すと 676f 7068 6572

18 は field_number 3で wire_type VARIANT(0)を表していて、次の01はenumの1を格納してあります。

ファイルに出力したwire formatの内容を書き換えて読み込んでみます。

% xxd output_1.bin
00000000: 0803 1206 676f 7068 6572 1801            ....gopher.

2バイト目を 01 から 03に変えています。

これを読み込んでみます。

package main

import (
	"fmt"
	"io"
	"os"
	"proto_sample/my_proto"

	"google.golang.org/protobuf/proto"
)

func main() {
	fr, err := os.Open("output_1.bin")
	if err != nil {
		return
	}
	defer fr.Close()

	fileData, err := io.ReadAll(fr)
	if err != nil {
		return
	}

	p2 := my_proto.Param2{}
	proto.Unmarshal(fileData, &p2)
	fmt.Println("p2 Unmarshaled data:", p2.String())
}

結果は下記のように、id_2の値が3に変化しています。

p2 Unmarshaled data: id_2:3 name_2:"gopher" status_2:STATUES_INACTIVE

整数値はBase 128 Varints方式でエンコーディングされるので、もっと大きい値で試すとよかったかもですが、一旦シンプルに確認するために小さい値で実行しました。
https://protobuf.dev/programming-guides/encoding/#varints

Discussion