LoginSignup
3
0

More than 1 year has passed since last update.

protobufのバイナリを眺めてみる

Posted at

protocol buffers のバイナリを眺めてみる

  • protocol buffers を利用することで色々いい感じになります
  • 複雑なことの必要がなく、簡単に利用することができます
  • ですが、高速なシリアライゼーションの恩恵を受ける上で、protobuf のバイナリを1度眺めておくことで、効率的なモデルの設計ができるようになるかもしれません
  • こちらの資料を参考にしています
    https://developers.google.com/protocol-buffers/docs/encoding
  • swift-protobuf を使っています
  • 検証に利用したコードはこちら

protocol buffers の使い方

  • proto ファイルでスキーマを定義します
syntax = "proto3";

message SearchRequest {
  string query = 1;
  int32 page_number = 2;
  int32 result_per_page = 3;
}
  • protoc を使って、swift のコードを作成します
protoc --swift_out=generated protos/*.proto 
  • swift のコードは指定した構造の struct がされます
struct SearchRequest {
  // SwiftProtobuf.Message conformance is added in an extension below. See the
  // `Message` and `Message+*Additions` files in the SwiftProtobuf library for
  // methods supported on all messages.
  var query: String = String()
  var pageNumber: Int32 = 0
  var resultPerPage: Int32 = 0
  var unknownFields = SwiftProtobuf.UnknownStorage()
  init() {}
}
  • この構造体を利用して、protobuf のバイナリを取得できます
var request = SearchRequest()
request.pageNumber = 1
let data = try request.serializedData()

protobuf のバイナリを眺めてみる

  • まずは最も簡単なスキーマとそれに対応するバイナリを見てみます
  • id が 1 の int64 型の number のプロパティを持つ Number1Entity です
message Number1Entity {
    int64 number = 1;
}
  • protoc を使って swift の構造体を作成し、number10 を設定してバイナリを生成します
var entity = Number1Entity()
entity.number = 10
let data: Data = try entity.serializedData()
  • バイナリは 080a となりました
  • 2進数表記だと、00001000 00001010 です
  • これをビッドごとの意味で色付けをしました

  • それぞれのパラメータについて見ていきます

Type

  • 1バイト目の下位3ビットには、Type が格納されています
  • 上の画像のオレンジ部分です
  • Type は、0, 1, 2, 5 の4種類の可能性があります
Type Meaning Used For
0 Varint int32, int64, uint32, uint64, sint32, sint64, bool, enum
1 64-bit fixed64, sfixed64, double
2 Length-delimited string, bytes, embedded messages, packed repeated fields
5 32-bit fixed32, sfixed32, float
  • Type によってこの後の処理が変わります
  • 080a の例では、Type は 0 なので、Varint です
  • 確かにスキーマで int64 と定義しています

ID

  • 1バイト目の2〜5ビットまでの4ビットに id が入っています
  • 上の画像の緑部分です
  • 緑の部分は 0001 なので、id は 1 となります
  • 確かにスキーマで、1と定義しています

Verint

  • 2バイト目には、データの本体が入っています
  • type が Verint なので、最上位ビットは further (後述)のフラグなので、下位7ビットにデータが入っています
  • 上の画像では紫の部分です
  • 0001010 = 10 なので代入した通りです

Verintの可変長数値表現

  • 先ほどまで見ていた構造体では、id が4ビット、データ本体が7ビットで表現されています
  • 当然15より大きいidを使いたいですし、127より大きいデータを使いたいです
  • そこで、protobufでは最上位ビットを使って、可変長数値表現を実現しています
  • 今度は、number に127より大きい数字である99999を代入してバイナリを生成してみます
var entity = Number1Entity()
entity.number = 99999
let data: Data = try entity.serializedData()
  • バイナリは、089f8d06 となりました
  • これを色分けしました

  • 1バイト目は変化がないですが、2バイト目以降のデータが変わっています
  • 2バイト目の最上位ビットを見ると1になっており、次のバイトまでデータが繋がっていることがわかります
  • 3バイト目の最上位ビットも立っているので、次のバイトまで繋がっています
  • 4バイト目の最上位ビットは立っていないので、このバイトで終わりであることが分かります
  • 2~4バイトのそれぞれ下位7ビットを、リトルエンディアンなので繋げます
  • 結果は、000011000011010011111 = 99999 で代入した値です
  • このことから、varintを利用する場合、7bitごとの数値が効率が良いことが分かります

idの可変長数値表現

  • 次に、id が99999の場合も見てみます
message Number99999Entity {
    int64 number99999 = 99999;
}
  • こちらのスキーマを使った構造体の number99999 に 10を代入してバイナリを生成します
var entity = Number99999Entity()
entity.number99999 = 10
let data: Data = try entity.serializedData()
  • 結果は f8e9300a でした
  • 色分けしました

  • id も可変長数値表現が使えます
  • これも緑の部分をリトルエンディアンで並び替えると、000011000011010011111 = 99999 となるので、スキーマで設定した通りになっていることが分かります
  • このことから、id は 4, 11, 18...ビットが効率良いことが分かります

64-bit, 32-bit

  • 次に、固定64bit, 32bitを見てみます
  • double, float, fixed64 などの型が含まれます
message Fixed64Entity {
    fixed64 number = 1;
}
  • fixed64 型の number プロパティに10を代入してバイナリを取得します
var entity = Fixed64Entity()
entity.number = 10
let data: Data = try entity.serializedData()
  • 結果は、0x090a00000000000000 でした
  • 色分けしました

  • 可変長数値表現ではなく、固定長で8バイトのデータになります
  • このことから、値がとても大きい場合や、負の数の頻度が高い場合は、固定長の方が有利であることが分かります

Length-delimited (文字列)

  • Length-delimited では文字列などを格納できます
  • まずは文字列を見てみます
message Text2Entity {
    string text = 2;
}
  • 文字列に "abc" を代入してバイナリを生成します
var entity = Text2Entity()
entity.text = "abc"
let data: Data = try entity.serializedData()
  • 結果は 1203616263 でした(16進数です)
  • 色分けしました

  • 今回の type は、2 なのでLength-delimited です(オレンジ部分)
  • id も 2 であることが分かります(緑部分)
  • Length-delimited の場合は、データ部分の最初に可変長数値表現でサイズ情報が入っています
  • 今回は、0000011 なので3バイトです(青部分)
  • 後ろの3バイトが文字列データです(紫部分)
  • 0x61 = 'a' なので、設定した文字列になっていることが分かります

2つのプロパティ

  • これまではプロパティが1つの場合を見てきました
  • 今回は、数値と文字列の2のプロパティを持つ message のバイナリを見ていきます
message TwoPropertyEntity {
    int64 number = 1;
    string text = 2;
}
  • number = 10, text = "abc" でバイナリを生成します
var entity = TwoPropertyEntity()
entity.number = 10
entity.text = "abc"
let data: Data = try entity.serializedData()
  • 結果は、080a1203616263 でした
  • 080a1203616263 に分割でき、これまで見た値と同値になっています
  • 080a は id が 1 の値が 10 のバイナリです
  • 1203616263 は id が 2 の値が "abc" のバイナリです
  • 色付けしました

  • protobuf はバイナリをくっ付けることで、2つのメッセージをマージできるようです

Oneof

  • protobuf には oneof という機能があります
  • oneof の中の1つのプロパティが設定できます
message OneOfEntity {
    int64 number = 1;
    oneof one {
        string ofText = 2;
        int64 ofNumber = 3;
    }
}
  • oneof のプロパティに ofNumber を設定してバイナリを生成しました
var entity = OneOfEntity()
entity.number = 10
entity.one = .ofNumber(44)
let data: Data = try entity.serializedData()
  • 結果は、080a182c でした
  • これを色付けしました

  • oneof は定義したうちの1つが設定されるだけで特にネストなどされず、通常のプロパティと同じ感じです

配列

  • int64 の配列を見てみます
message RepeatedNumberEntity {
    repeated int64 numbers = 1;
}
  • 配列には、10と130を設定します
var entity = RepeatedNumberEntity()
entity.numbers = [10, 130]
let data: Data = try entity.serializedData()
  • 結果は、0a030a8201 でした
  • 色付けしました

  • repeated なので、type は Length-delimited です
  • Length-delimited の body 部分(紫色部分)は varint になっています
  • 1バイト目は、1010 = 10
  • 2〜3バイト目は、10000010 = 130で指定した値になっていることが分かります

辞書式

  • 辞書式を見ていきます
message MapEntity {
    map<string, string> dictionary = 1;
}
  • バイナリを生成します
var entity = MapEntity()
entity.dictionary = [
    "a": "A",
    "b": "BB"
]
let data: Data = try entity.serializedData()
  • 結果は、0a060a01611201410a070a016212024242 でした
  • 色付けします

  • id が1の Length-delimited が2つ入っています
  • 0a0161120141 の部分に着目します

  • id が1に "a"、id が2に"A" が入っていることが分かります
  • もう1つ目のbody部分にもキーが id 1 に、値が id 2 に入っています
  • それぞれのキーと値のペアが同じ構造体が同じ id で入っていると辞書式となるようです

message のプロパティ

  • 1番最初に見た、Number1Entity をプロパティに持つ message です
message ParentEntity {
    Number1Entity child = 1;
}
  • バイナリを生成します
var child = Number1Entity()
child.number = 10

var entity = ParentEntity()
entity.child = child

let data: Data = try! entity.serializedData()
  • 結果は 0a02080a でした
  • 色付けしました

  • id が1で、typeがLength-delimitedです
  • body(紫色)部分は、080aです
  • これは、1番最初に見た、idが1の値がvarintの10のバイナリと同じです
  • つまり、messageのプロパティのbodyにはそのmessageのバイナリが入っているようです

まとめ

  • さまざまな protobuf のバイナリを生成し、眺めて見ました
  • バイナリ自体はかなりシンプルな構造でした
  • 実際に、値を変えて動かすと、バイナリとその構造が動いていくのが見えるので、とても楽しかったです
3
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
0