一般的なシリアル化プロトコル/フォーマット
下の表から、一般的なシリアライゼーション・プロトコル/フォーマットの違いを簡単に見ることができます。
- ProtoBuf
- Json/Yaml/Toml
- XML
- Bson
- MessagePack
- Apache Thrift
- ......
はじめに
プロトバルバッファ プロトバルバッファはデータ構造をシリアライズするためのプロトコルで、2つの部分から構成されています。
- メタデータをバイナリ形式にコンパイルする方法を指定します。
- インターフェース記述言語のセットと、付属のコードジェネレーターが含まれています。
以下はProtobufの例で、2回に分けて詳しく説明します。
// protobuf インターフェース説明ファイル
syntax = "proto3"; // インターフェースは、使用されるprotobufのバージョンを記述する。`proto3`
// HelloMan Service
// これはサービスへのインターフェイスを宣言する,
// これはオブジェクト指向におけるインターフェースの概念に似ている。
// このインターフェースは入出力のフォーマットも指定する。
service HelloMan {
rpc SayHello (Request) returns (Response) {}
}
// リクエストボディのフィールドは1つだけで、nameという文字列型である。
message Request {
string name = 1;
}
// レスポンスボディには、文字列型のフィールドhelloが1つだけある。
message Response {
string hello = 1;
}
ProtobufはGoogleによって最初に開発され、GRPCのシリアライゼーションプロトコルとして使用されました。 その後、より多くのRPCライブラリがシリアライゼーションプロトコルとしてProtobufをサポートするようになりました。
Protocal buffer インターフェース記述言語
プロトカル・バッファのインターフェース記述言語は、 proto 3と proto2の 2つのバージョンに分かれており、proto3ではいくつかの機能が追加され、proto2からいくつかの変更が加えられています。
基本的な例
// protobuf インターフェース説明ファイル
syntax = "proto3"; // インターフェースは、使用されるprotobufのバージョンを記述する。`proto3`
// HelloMan Service
// これはサービスへのインターフェイスを宣言する,
// これはオブジェクト指向におけるインターフェースの概念に似ている。
// このインターフェースは入出力のフォーマットも指定する。
service HelloMan {
rpc SayHello (Request) returns (Response) {}
}
// リクエストボディには2つのフィールドがある,
// フィールドの1つはnameで、文字列型である。,
// もう1つのフィールドは文字列型のhelloである。
message Request {
string name = 1;
int hello = 2;
}
// また、レスポンスボディには、文字列型のフィールドhelloが1つだけある。
message Response {
string hello = 1;
}
フィールドは以下のフォーマットで定義されます。
[ "repeated" ] type fieldName "=" fieldNumber [ "[" fieldOptions "]" ] ";"
// repeated type fieldName "=" fieldNumber [fieldOptions];
// repeated string name = 1 ;
冒頭の 繰り返し フラグは通常、フィールドの値が配列であることを示すために使用されます。 さらに、フィールド番号 fieldNumber の後に、いくつかのオプションを定義することができます。
複雑なフィールド記述も以下のキーワードで実現できます。
- oneof
- map
- reserved
- enum
protobuf
以下は、.protoファイルで使用されているキーワードの説明です(記述順)。
syntax
構文は、protobuf構文のバージョンを定義するために使用されます。
// proto3
syntax = "proto3";
// proto2
syntax = "proto2"
import
importは通常、他の.protoファイルを参照するために使用され、importの後にキーワードを続けることで、ファイルとの関係をさらに絞り込むことができます。
// 通常の単一レベル参照
import "First.proto"
// 複数参照を許可する
import public "First.proto"
// 存在しない
import weak "First.proto"
weak
つまり、インポート時にエラーは発生しませんが、存在しないオブジェクトや構造体が後で使用された場合、同じエラーが報告されます。
public
publicは複数レベルの参照によく使われます。 イメージは@hanschen より
- シナリオ1では、my.protoはSecond.protoファイルの内容を使用できません。
- シナリオ2では、my.protoはSecond.protoの内容を使用することができます。
package
package キーワードは proto ファイルの名前空間の役割を果たし、メッ セージ・タイプ間で名前が衝突するのを防ぎ、同じ名前のメッセー ジをパッケージによって区別できるようにします。
一方、Java用PackageやGo用Packageのように、言語固有のPackage名を生成するのにも使えます。
syntax = "proto3";
package meta;
message
message Request {
string name = 1;
int hello = 2;
}
MessageはProtobufインターフェイス記述言語で最もよく使われるキーワードの1つで、すべてのデータ転送はMessage単位で行われます。C言語ファミリやGo言語に慣れている方なら、これがStructの概念に非常に似ていることがすぐにわかるでしょう。
service
service HelloMan {
rpc SayHello (Request) returns (Response) {}
}
message Request {
string name = 1;
int hello = 2;
}
message Response {
string hello = 1;
}
option
主なオプションは4つのカテゴリーに分かれています。
より一般的なoptimize_forのようなファイルレベルのオプションは、以下のように外部空間で定義する必要があります。
option optimize_for = CODE_SIZE;
メッセージ・レベルのオプションは、メッセージの中で定義する必要があります。
message HelloWorld { string name = 1; option message_set_wire_format = true; option deprecated = true; // を使うことで、その関数が非推奨であることを示すことができる。 }
フィールドレベルのオプションは、前述のフィールド構造を思い出してください。フィールドレベルのオプションは、以下のようにフィールドの最後に定義する必要があります。
message HelloWorld { string name = 1 [ packed = true, deprecated=true]; }
最後のオプションは oneofOptions, EnumOptions オプションです。
- File >> FileOptions
- Message >> MessageOptions
- Field >> FieldOptions
- 最後の型は, OneofOptions,EnumOptions,EnumValueOptions,ServiceOptions,MethodOptions
同時に、オプションのカスタマイズも可能で、指定したレベルにいくつかのカスタムオプションを追加できます。
import "google/protobuf/descriptor.proto";
extend google.protobuf.MessageOptions {
string my_option = 51234;
}
message MyMessage {
option (my_option) = "Hello world!";
}
共通フィールドキーワード
まず、データ型の定義です。
- 数値型
- double
- float
- int32
- int64
- uint32
- uint64
- sint32
- sint64
- 固定フットプリントを持つ数値型。
- fixed32
- fixed64
- sfixed32
- sfixed64
- おそらく、メッセージをシリアライズするときに違いが出るのではないでしょうか? // TODOのチェックが必要ですが、自由に追加してください!
- ブール型 ブール型
- : string
- bytes byte
- messageType メッセージタイプ
- enumType 列挙型
oneof
このキーワードを使用すると、このキーワード・セットに表示できるフィールドの数を最大1つに制限できます。
syntax = "proto3";
package abc;
message OneofMessage {
oneof test_oneof {
string name = 4;
int64 value = 9;
}
}
一方、proto3はフィールドが設定されているか、デフォルト値が自動的に使われるかを見分ける方法がないため、データがフィールドを含んでいるかどうかさえ見分けることができません。protobufのgo実装では、デフォルトでフィールドにomitemptyタグが付けられており、フィールドが空の場合は送信から省略されます。
goのprotobufの実装では、oneofとして宣言されたフィールドはデフォルトで構造体に対応し、この値が設定されていない場合、受信した値はnilになり、設定されている場合は通常の値になります。
また、oneofフィールドは、繰り返される
map
map 型は基本的に go の map 型と同じで、php の連想配列と同じ KV キー・値ペアです。 map 型の書式は上記の通常の書式とは少し異なり、ここでは Java 形式の角括弧が使用されています。
"map" "<" keyType "," valueType ">" mapName "=" fieldNumber ["[" fieldOptions"]"]
map<int64,string> values = 1;
対応するマップフィールドは、繰り返される
reserved
reserved はあまり使用されないキーワードで、Message レベルのキーワードで、現在の Message が特定のフィールドや FieldNumber を使用しないことを宣言するために使用できます。 例を以下に示します。
また、protocでコンパイルするときにエラーが発生するので、protoファイルでは定義せず、reservedを宣言することをお勧めします。
syntax = "proto3";
package abc;
message AllNormalypes {
reserved 2, 4 to 6;
reserved "field14", "field11";
double field1 = 1;
// float field2 = 2;
int32 field3 = 3;
// int64 field4 = 4;
uint32 field5 = 5;
// uint64 field6 = 6;
sint32 field7 = 7;
sint64 field8 = 8;
fixed32 field9 = 9;
fixed64 field10 = 10;
// sfixed32 field11 = 11;
sfixed64 field12 = 12;
bool field13 = 13;
// string field14 = 14;
bytes field15 = 15;
}
enum
// Languageフィールドが "Java"、"PHP"、"Rust "の値のみを取ることを指定する。
enum Language {
Java = 0;
PHP = 1;
Rust = 1;
}
また、protの定義において、enumと同じ値や同じ名前のenumを指定することはできません。
package example;
// example 1
// Error: "A" is already defined in "example".
enum A {
A = 0;
B = 1;
C = 2;
}
// -----------------------------
// example 2
// Error: "B" is already defined in "example".
enum A {
B = 0;
C = 1;
}
enum B {
D = 0;
}
// -----------------------------
// example 3
// Error: "C" is already defined in "example".
enum A {
B = 0;
C = 1;
}
enum D {
C = 0;
E = 1;
}
さらに、Messageにallow_aliasオプションを設定することができ、これによりFieldNumbersの重複が許可され、同じFieldNumbersを持つフィールドは互いにエイリアスされます。例えば、以下の例では、BとCは互いのエイリアスです。
enum A {
option allow_alias = true;
B = 0;
C = 0;
}
さらに、列挙値には厳格なルールがあり、最初の値は0でなければならず、定義されていなければなりません。例えば、以下の最初の例はエラーになりますが、2番目の例は問題なく動作します。
// example 1
enum A {
B = 1; // これは0でなければならない
C = 0;
}
// --------------
// example 2
enum B {
C = 0;
D = 10;
E = 100;
F = 1;
}
他のメッセージをフィールドとして参照するタイプ
以下の SearchResponse では、フィールド・タイプとして Result を使用しています。
message Result {
string url = 1;
string title = 2;
repeated string snippets = 3;
}
message SearchResponse {
repeated Result results = 1;
} // ^^^^^^
ネストされた定義
多くの場合、部分構造がその親によってのみ使用される場合、goにおけるstructの入れ子定義と同様に、入れ子定義とみなすことができます。
message SearchResponse {
message Result {
string url = 1;
string title = 2;
repeated string snippets = 3;
}
repeated Result results = 1;
}
unknown
このフィールドは通常、あなた自身の.protoファイルを書くときには使用されず、フィールドインタプリタによって認識されないフィールドタイプに遭遇したときに、proto3によってunknownとマークされます。
any
Any 型は、プロトで定義された型処理を介さずに、ユーザが独自にデータを処理することを可能にします。 Any 型は、バイトでシリアライズされたメッセージを提示し、一意の識別子と型のメタデータとして URL を含みます。
google/protobuf/any.protoただし、Any型を使用するには、次の例のように型を導入する必要があります。
import "google/protobuf/any.proto";
message ErrorStatus {
string message = 1;
repeated google.protobuf.Any details = 2;
//^^^^^^^^ バイトはgoのように配列でなければならないので、ここで繰り返しを省略することはできない。[]bytes
}
proto3 主な変更点
- 必須フラグを削除します
- フィールドのデフォルトを削除
- 追加されたマップタイプ
メッセージタイプの更新
protobufは既存のサービスに影響を与えることなくこのような変更をサポートしますが、特定のルールに従う必要があります:
- 既存のフィールドのフィールド番号を変更しないでください。
- 新しいフィールドを追加したとき、古いシステムでシリアライズされたデータは新しいフォーマットでもパースできます。 古いシステムでもメッセージの値を解析することができますが、新しく追加されたフィールドは破棄されます!
- このフィールドは削除することもできますが、今後使用しないように予約しておくことをお勧めします。
- int32、uint32、int64、uint64、bool型はすべて互換性があります。
- sint32 と sint64 は互換性がありますが、他の整数型とは互換性がありません。
- 文字列は、バイトがUTF-8の正当なバイトであれば、バイトと互換性があります。
- bytesがメッセージのエンコードされたバージョンを含む場合、埋め込みタイプはbytesと互換性があります。
- fixed32 および sfixed32、fixed64、sfixed64 を参照してください。
- enum int32, uint32, int64, uint64 形式に対応します。
- 単一の値を新しいoneof型のメンバに変更することは安全で、バイナリ互換性があります。フィールドのグループを新しいoneofフィールドに変更することも、グループのフィールドが1つだけ設定されていることを確認すれば安全です。フィールドを既存のoneofフィールドに移すことは安全ではありません。
protobuf Go 実装されている拡張型
golang/protobuf Repoのptypesフォルダで、以下の拡張が行われました。
- wrappers
- duration とタイムスタンプ
- empty
wrapper
proto3ではdefaultとmandatoryキーワードが削除されたため、一部のシナリオでは、Structはパース後に、フィールドが元々0に設定されていたのか、あるいはomitempty Json Tagのデフォルト使用によりフィールドが埋められずデフォルト値の0が使用されたのかを判別することができません。 これは、異種アプリケーションとの通信やレガシーシステムとのやり取りでよく発生する問題です。. ラッパーは、以下の例で示されるように、proto3構文のこの欠陥を修正するために導入されました。
これが、SMS送信サービスを例にした、仮想的な.protoファイルの定義です。
syntax = "proto3";
package example;
// SMSサービス
service PhoneMessageServiceAo {
rpc SendPhoneMessage (PhoneMessageRequest) returns (PhoneMessageResponse) {}
}
// リクエスト構造
message PhoneMessageRequest {
string phoneNumber = 1;
bool international = 2;
}
// レスポンス構造
message PhoneMessageResponse {
bool success = 1;
string phoneMessageId = 2;
}
すると、次のような .pb.go ファイルが生成されます。
package example
//
type PhoneMessageRequest struct {
PhoneNumber string `protobuf:"bytes,1,opt,name=phoneNumber,proto3" json:"phoneNumber",omitempty`
International bool `protobuf:"varint,2,opt,name=international,proto3" json:"international",omitempty`
//
}
//
type PhoneMessageResponse struct {
Success bool `protobuf:"varint,1,opt,name=success,proto3" json:"success",omitempty`
PhoneMessageId string `protobuf:"bytes,2,opt,name=phoneMessageId,proto3" json:"phoneMessageId",omitempty`
//
}
// ......省略
そして、全体のプロセスは、任意のエラーメッセージなしで、本当に正常に実行されているため、テスト学生のテストは、公式まで、合格し、最終的にユーザーのフィードバック、またはある日、管理者の権限を持って、SMSサービスプロバイダのユーザーセンターをチェックし、唯一のエラーメッセージのレコードを参照してくださいし、我々はこの問題があることを知っているでしょう。 しかし、この時点ではまだ問題を見つけることができない、我々は見つけるために一歩一歩する必要があるので、まず、SMS送信サービスの書き込みの最初から、ログに問題が見つからなかったチェック、コードを介して歩いて問題が見つからなかった、私はそれがサービスの問題ではないと思います。 その後、我々は唯一の問題を見つけるために、サービス発信者のビジネスコードをチェックしに行くことができる、幸運を前提として、そこに1つだけのサービス発信者は、一度チェックした後、最終的に問題が問題のデフォルトのタイプであることがわかったので、それを修正し、テストが合格し、オンラインに移動します。
このようなメカニズムがいかに間違えやすいかを示す悪い例があります。 RPC入力検出で検出できたはずのエラーが、この仕組みのために場所を特定するのに多くの時間を要しました。
しかし、googleはまた、この問題を発見し、彼らはGoの実装に、ラッパーのクラスを追加しました、ラッパーは、名前が示すように、ラッパーは、元の型は、構造体に結果のフィールドの型は、フィールドが設定されていないように、ラッピングの層を行うには、nilのデフォルト値を取得する代わりに、結果である可能性のある値です。 wrapperは,以下の型をラップします.
- double => DoubleValue
- float => FloatValue
- int64 => Int64Value
- uint64 => UInt64Value
- int32 => Int32Value
- uint32 => UInt32Value
- bool => BoolValue
- string => StringValue
- bytes => BytesValue
紙に書いてあるほど簡単ではありません。 では、生成して使ってみましょう。
まず、.protoファイルのタイプを変更する必要があります。
syntax = "proto3";
package example;
// ラッパーを導入する.proto
import "google/protobuf/wrappers.proto";
service PhoneMessageServiceAo {
rpc SendPhoneMessage (PhoneMessageRequest) returns (PhoneMessageResponse) {
}
}
message PhoneMessageRequest {
string phoneNumber = 1;
// googleに修正する.protobuf.BoolValue
google.protobuf.BoolValue international = 2;
}
message PhoneMessageResponse {
bool success = 1;
string phoneMessageId = 2;
}
次にターミナルを開いて.pb.goファイルを生成すると、この構造が.pb.goの中に生成されていることがわかります。
package example
//
type PhoneMessageRequest struct {
PhoneNumber string `protobuf:"bytes,1,opt,name=phoneNumber,proto3" json:"phoneNumber",omitempty`
International *wrappers.BoolValue `protobuf:"bytes,2,opt,name=international,proto3" json:"international",omitempty`
//
}
//
type PhoneMessageResponse struct {
Success bool `protobuf:"varint,1,opt,name=success,proto3" json:"success",omitempty`
PhoneMessageId string `protobuf:"bytes,2,opt,name=phoneMessageId,proto3" json:"phoneMessageId",omitempty`
//
}
// ......省略
type BoolValue struct {
// The bool value.
Value bool `protobuf:"varint,1,opt,name=value,proto3" json:"value,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
彼は元のデータをBoolValue.Valueに保存するので、Internationalの値が設定されているかどうかを判断することができ、もし設定されていなければ、Internationalの値はnilになり、もし設定されていれば、Internationalの値はnilにはなりません。 これにより、上記の例で述べた問題を回避することができます。
timestamp と持続時間
//[colobu.com/2019/10/03/...]...
empty
もしあなたの関数がパラメータを必要としないのであれば、この型を使ってプロトファイルでそれをマークすることができます。
syntax = "proto3";
package example;
import "google/protobuf/empty.proto";
service PhoneMessageServiceAo {
rpc SendPhoneMessage (google.protobuf.Empty) returns (google.protobuf.Empty) {}
}
を参照してください。
Protobuf究極のチュートリアル
言語ガイド
プロトコル・バッファ・マニュアル
[プロト3のデフォルトとオプション]
[Protobufのオプション機能]
google/protobuf/descriptor.proto
golang/protobuf/ptypes




