2
2

More than 1 year has passed since last update.

Protocol Buffersの基本的な内容に関してまとめてみた

Posted at

初めに

業務でgrpcを使用する機会があったので、プロトコルバッファをこちらのサイトを見ながら学習しました。
その内容をアウトプットのため投稿します。
また、使用する言語はGoでproto3を使用します。

概要

プロトコルバッファは、構造化されている型付けされたデータを言語やプラットフォームに依存せずシリアライズすることができます。

定義言語(インタフェース定義言語)・シリアライズ形式・各言語向けランタイムライブラリ・プロトコンパイラ生成コードの4要素からなる[2]。
出典:Wikipedia

簡単な流れ

プロトコルバッファを使用する際の簡単な流れは以下です。

  1. .protoファイルにデータの定義を行う。
  2. 指定した開発言語のオブジェクトを生成する。
  3. バイナリファイルに変換されてデータを送受信する。

特徴

プロトコルバッファの特徴は以下です。

  • バイナリファイルでデータの送受信がされるためjsonに比べて軽量である。
  • 型安全である。
  • さまざまな言語に変換可能である。
  • データを追加・削除する際に既存のデータを壊すことがない。
  • 数メガバイトを超えるデータには向かない。
  • gRPCに使用できる。

インタフェース定義言語のメリット

プロトコルバッファは.protoファイルにデータが事前に定義されているので、フロント・モバイルアプリとバックエンド間や、マイクロサービス化されたシステムで、最初に相互の認識を合わせて開発することができます。(仕様書の様な役割を果たせる)
これはスキーマ駆動開発と呼ばれています。

定義方法

プロトコルバッファの.protoファイルのデータ定義方法を記載します。

syntax

まず最初にsyntaxを定義します。
冒頭にも述べましたが、今回はproto3を使用します。
この記述がないとproto2を使用するとみなされるので、proto3を使用したい場合は記載が必要です。

syntax = "proto3";

message

messageはGoにコンパイルする際に構造体になるデータで、複数定義することができます。

基本形

message メッセージの名前 {
     フィールド名 = フィールド番号
     フィールド名 = フィールド番号
}

実際に定義すると以下のような記載になります。

message Request {
    string s = 1;
    int32 i = 2;
}

message Response {
 ...
}

フィールド番号

フィールド番号は一意の値である必要があります。
1 ~ 15の数値は、フィールド番号とフィールドの型を含めてエンコードに1バイトかかり、16~2047のフィールド番号は2バイトかかります。
そのため、頻繁に使用するフィールドには1 ~ 15の数値を割り振った方が良いでしょう。
また、1 ~ 536,870,911までの数値を使用できますが、19000 ~ 19999は予約番号であり使用することはできません。

コメント

コメントは///**/で記載可能です。

// commet
/* comment */

Scalar型

int32stringboolなどの型があります。
こちらに詳細が記載してあるのでご確認ください。

列挙型

列挙型は、定義したいずれかのフィールドが含まれている必要があります。
詳細な定義のルールは以下です。

  • フィールド名は大文字で記載する。
  • 0 を数値のデフォルト値として使用できるようにするため、最初のフィールドのフィールド番号は0にする。

基本形

enum 名前 {
    フィールド名 = タグ番号;
    フィールド名 = タグ番号;
}

先ほど使用していたmessageに追加してみます。

message Request {
    string s = 1;
    int32 i = 2;
    // Scalar型と同様に扱うことができる
    em em = 3;
}
// 追加
enum em {
    UNKNOW = 0;
    ONE = 1;
    TWO = 2;
}

Requestメッセージに列挙型であるem型のemというフィールドを追加しました。
最初のフィールドは使用しないことが多いのでUNKNOWというフィールドにしています。

repeated

repeatedフィールドでは配列を扱うことができます。

repeated  フィールド名 = フィールド番号;

先ほど使用していたmessageに追加してみます。

message Request {
    string s = 1;
    int32 i = 2;
    em em = 3;
    // 1つ以上のstring型の文字列が使用できる
    repeated string arr = 4;
}

enum em {
    UNKNOW = 0;
    ONE = 1;
    TWO = 2;
}

map

mapではkey-valueの連想配列のような型を定義できます。
keyはstringnumericboolのいずれかを使用できます。
repeatedフィールドは使用できません。

基本形

map<keyの型, Valueの型> フィールド名 = フィールド番号;

先ほど使用していたmessageに追加してみます。

message Request {
    string s = 1;
    int32 i = 2;
    em em = 3;
    repeated string arr = 4;
    // 追加
    map<string, MapValue> associative = 5;
}

enum em {
    UNKNOW = 0;
    ONE = 1;
    TWO = 2;
}
// 追加
message MapValue {}

keyがstring型でvalueがMapValuemessageのmapを作成することができました。

Oneof

Oneofは定義した複数の型のいずれかがを値として持ちます。

oneof フィールド名 {
         フィールド名 = フィールド番号;
         フィールド名 = フィールド番号;
}

先ほど使用していたmessageに追加してみます。

message Request {
    string s = 1;
    int32 i = 2;
    em em = 3;
    repeated string arr = 4;
    map<string, MapValue> associative = 5;
    // 追加
    oneof O {
        string OS = 6;
        OneofField OF = 7;
    }
}

enum em {
    UNKNOW = 0;
    ONE = 1;
    TWO = 2;
}

message MapValue {}
// 追加
message OneofField {}

Oフォールドはstring型、OneofField型のいずれかを受け入れます。

ゼロ値

定義しているフィールドに値が格納されていない場合に、型にはゼロ値が格納されます。
各型のゼロ値は以下の表を参照ください。

ゼロ値
strings 空の文字列
bytes 空のバイト
bools false
numeric 0
enums 最初に定義された列挙値で、0 にする必要がある
repeated 空のリスト

参照: https://developers.google.com/protocol-buffers/docs/proto3#default

messageのネスト

messageはネストすることができます。
ネストされたmessageは、.を用いてアクセスすることができます。
ネストされた子のmessageのスコープは親のmessage内であるため、親のmessage外でも別のmessageとして定義することができます。

message A {
    message B {}
}
// 上記のBというmessageとは別のmessageである
message B {}
// Bへアクセス
A.B

先ほど使用していたmessageに追加してみます。

message Request {
    string s = 1;
    int32 i = 2;
    em em = 3;
    repeated string arr = 4;
    map<string, MapValue> associative = 5;
    oneof O {
        string OS = 6;
        OneofField OF = 7;
    }
    // 追加
    Parent.Cheidlen nest = 8;
}

enum em {
    UNKNOW = 0;
    ONE = 1;
    TWO = 2;
}

message MapValue {}
message OneofField {}
// 追加
message Parent {
    message Cheidlen {}
}

import

プロトコルバッファでは、別のファイルのprotoファイルをimportして使用することができます。

import "パス.proto";

先ほどのファイルに別のファイルを追加してみます。

ディレクトリ構成は以下の様になります。

proto
├── another.proto
└── file.proto

file.protoが今までの使用していたファイルでanother.protoが新しく追加したファイルです。
カレントディレクトリはprotoディレクトリです。

では、another.protoにデータの定義をしていきます。

syntax = "proto3";

message Other {
    int32 i = 1;
}

Otherというmessageを定義しました。
上記をfile.protoで読み込んで使用します。

syntax = "proto3";

import "another.proto";

message Request {
    string s = 1;
    int32 i = 2;
    em em = 3;
    repeated string arr = 4;
    map<string, MapValue> associative = 5;
    oneof O {
        string OS = 6;
        OneofField OF = 7;
    }
    Parent.Cheidlen nest = 8;
    // 追加
    Other another = 9;
}

enum em {
    UNKNOW = 0;
    ONE = 1;
    TWO = 2;
}

message MapValue {}
message OneofField {}
message Parent {
    message Cheidlen {}
}
message Cheidlen {}

anotherというanother.protoのフィールドをfile.protoで読み込みました。

プロトコンパイルする際

実際にプロトコンパイルする際は、-I / --proto_pathオプションを使用してディレクトリを指定する必要があります。
今回は、カレントディレクトリであるprotoディレクトリを指定することで、正常にコンパイルすることができます。

package

packageを使用することで、各ファイルに名前競合を防ぐことができます。
同じ名称のmessageがある場合などに有用です。

package パッケージ名;

実際にfile.protoanother.protoにパッケージ名を追加してみます。
file.proto

syntax = "proto3";
// 追加
package file;

import "another.proto";

message Request {
    string s = 1;
    int32 i = 2;
    em em = 3;
    repeated string arr = 4;
    map<string, MapValue> associative = 5;
    oneof O {
        string OS = 6;
        OneofField OF = 7;
    }
    Parent.Cheidlen nest = 8;
    // 更新
    another.Other another = 9;
}

enum em {
    UNKNOW = 0;
    ONE = 1;
    TWO = 2;
}

message MapValue {}
message OneofField {}
message Parent {
    message Cheidlen {}
}
message Cheidlen {}

another.proto

syntax = "proto3";
// 追加
package another;

message Other {
    int32 i = 1;
}

今回は名前の競合が起きていないので付与する意味はありませんが、実装方法を残すため定義してみました。

option

optionを使用して、コンパイルされるGoファイルのpackageを指定します。

option go_package = "./pb";という記述をfile.protoanother.protoの両方に記載します。

file.proto

syntax = "proto3";
// 追加
option go_package = "./pb";

package file;

import "another.proto";

message Request {
    string s = 1;
    int32 i = 2;
    em em = 3;
    repeated string arr = 4;
    map<string, MapValue> associative = 5;
    oneof O {
        string OS = 6;
        OneofField OF = 7;
    }
    Parent.Cheidlen nest = 8;
    another.Other another = 9;
}

enum em {
    UNKNOW = 0;
    ONE = 1;
    TWO = 2;
}

message MapValue {}
message OneofField {}
message Parent {
    message Cheidlen {}
}
message Cheidlen {}

another.proto

syntax = "proto3";
// 追加
package another;

option go_package = "./pb";

message Other {
    int32 i = 1;
}

プロトコンパイル

最後に、下記コマンドを使用して今まで記載してきたprotoファイルをGoへコンパイルします。

protoc -I. --go_out=. *.proto
  • -I.はprotoファイルのimport文のパスを特定しています。
    • 今回はカレントディレクトリであるprotoディレクトリ内で作業しているので.を指定しています。
  • --go_out=.は、Goへ変換するコマンドです。.は出力されるファイルの場所を指定しています。
  • *.proto今回は、カレントディレクトリ配下の2つをコンパイルするので、全ての.protoの拡張子のファイルをコンパイルする様に設定しています。

実際に変換した後

├── another.proto
├── file.proto
└── pb
    ├── another.pb.go
    └── file.pb.go

実際に変換すると上記のようなディレクトリ構造になり、another.pb.gofile.pb.goファイルが生成されています。

ファイルの中に定義したmessageが構造体として定義されていることが分かります。

pb/file.pb.go
type Request struct {
	state         protoimpl.MessageState
	sizeCache     protoimpl.SizeCache
	unknownFields protoimpl.UnknownFields

	S           string               `protobuf:"bytes,1,opt,name=s,proto3" json:"s,omitempty"`
	I           int32                `protobuf:"varint,2,opt,name=i,proto3" json:"i,omitempty"`
	Em          Em                   `protobuf:"varint,3,opt,name=em,proto3,enum=file.Em" json:"em,omitempty"`
	Arr         []string             `protobuf:"bytes,4,rep,name=arr,proto3" json:"arr,omitempty"`
	Associative map[string]*MapValue `protobuf:"bytes,5,rep,name=associative,proto3" json:"associative,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
	// Types that are assignable to O:
	//
	//	*Request_OS
	//	*Request_OF
	O       isRequest_O      `protobuf_oneof:"O"`
	Nest    *Parent_Cheidlen `protobuf:"bytes,8,opt,name=nest,proto3" json:"nest,omitempty"`
	Another *Other           `protobuf:"bytes,9,opt,name=another,proto3" json:"another,omitempty"`
}
pb/another.pb.go
type Other struct {
	state         protoimpl.MessageState
	sizeCache     protoimpl.SizeCache
	unknownFields protoimpl.UnknownFields

	I int32 `protobuf:"varint,1,opt,name=i,proto3" json:"i,omitempty"`
}

以上。

参照サイト

Protocol Buffers ドキュメント
gRPC Golang

2
2
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
2
2