【Unityテクノロジー】Unity3D高度プログラミングネットワーク層解析データプロトコル原理

プロトコル パッケージの形式、json、msgpack、protobuf、およびカスタム形式
。プロジェクトのネットワーク層は構築中です。送信プロトコル TCP、UDP、およびアプリケーション層プロトコル HTTP を選択することに加えて、送信プロセス中のビジネス層プロトコルの形式。先ほど、TCP、UDP、HTTP の原理と応用について分析しましたが、ここでは、トランスポート層とアプリケーション層の上にあるビジネス層におけるネットワーク データ送信形式の選択とその長所と短所について理解します。ここでは、JSON、MessagePack、Protobuf の構成要素、シリアル化方法、逆シリアル化方法など、JSON、MessagePack、および Protobuf の原理を分析します。原理と基礎となる層の分析を通じて、ネットワーク データ プロトコルをより深く理解できます。徹底的にクリアに。

最も一般的な JSON 形式から始めて、ビジネス層プロトコルの背後にあるルールと原則を段階的に理解し、複雑なデータ形式と基礎となる実装を段階的に分析します。

===

JSON
JSON はもともと JavaScript Object Notation (JavaScript Object Notation) であり、徐々に誰もが受け入れて普及し、プロトコル データ形式になりました。これはテキスト情報を保存および交換するための構文であり、XML に似ていますが、XML よりも小さく、高速で、解析が簡単です。

JSON 自体は、文字列で構成される軽量のテキスト データ交換形式であり、言語に依存せず、自己記述型であるため、非常に理解しやすくなっています。同じくプレーン テキスト形式である XML と比較して、JSON は終了タグが必要なく、JSON が短く、JSON の解析と読み書きの速度が速くなります。JavaScript では、組み込みの JavaScript eval() メソッドを使用して次のことができます。 JSON も配列が使用でき、予約語 (&、<、>、'、") は使用されません。JSON の構文規則を見てみましょう。JSON データの記述形式は次のとおりです

。 /値のペア。名前と値のペアには、フィールド名 (二重引用符で囲まれた)、その後にコロン、値が含まれます:

"firstName" :
JSON データはカンマで区切られ、その値は数値、文字列、true および false の論理値、配列、オブジェクトになります。テキスト内の特定の形式を見てみましょう: 数値 (整数または浮動小数点数

)

{   " number" : 1 ,   "number2" : 11.5 }文字列 (二重引用符内) {   "str1" : "1",   "str2" : "11" }論理値 (true または false) {   "logic1" : true,   "logic2 " : false }配列 (角括弧内) {    "array1" : [1,2,3],    "array2" : [{"str1",1},{"str2",2},{33,44}] }オブジェクト (中括弧内) {   "obj1" : {1, "str1", true},   "obj2" : {"str2", 2, false},   "obj3" : null }ここで、オブジェクトは中括弧で囲まれており、そのオブジェクトには複数の名前と値のペアを含めることができます。






























{ "firstName":"John" , "lastName":"Doe" }
配列は角括弧内に記述され、複数のオブジェクトを含めることができます:

{     "employees": [         { "firstName":"John" , "lastName": "Doe " },         { "firstName":"Anna" , "lastName":"Smith" },         { "firstName":"Peter" , "lastName":"Jones" } ]     } JSONファイルは通常、ファイル タイプ " xxx 。json" は、名前を拡張して、json 形式のテキスト ファイルであることを示すために使用されます。HTTP プロトコルでは、端末ロジックの識別を容易にするために、Json 形式の MINE タイプも定義されています。JSON テキストの MIME タイプは "application/ json" (MIME (MultiPurpose Internet Mail Extensions) は、メッセージ コンテンツ タイプを記述するためのインターネット標準です。他の MIME メッセージには、テキスト、画像、オーディオ、ビデオ、その他のアプリケーション固有のデータが含まれています。また、通常のプログラミングでは多くの JSON パーサーがあります。 simpleJson、MiniJson、DataContractJsonSerializer、JArray、JObject などは非常に多用途で効率的なプラグインであり、JSON パーサーを作成するために「ホイールを構築」することもできます。その際には効率とパフォーマンスの問題も考慮する必要があります。 . バイナリ ストリーム プロトコル形式をカスタマイズする











ほとんどのネットワーク プロトコルにはある程度の汎用性があり、JSON が最も典型的なケースであり、XML、MessagePack、Protobuf などの他のプロトコルも比較的一般的ですが、ここで話しているカスタム バイナリ ストリーム プロトコルは、理論的にはまったく普遍的ではありません。その理由は、普遍性を念頭に置いて設計されていないためです。

データ文字列を保存するとき、データ文字列に含まれるデータやデータ型に関係なく、データ文字列を分析するときは、まずデータを解析する方法を知る必要があります。これがプロトコル形式を定義する目的です。 。簡単に言えば、データ文字列を受信したときに、このデータ文字列の内容を知るためにどのようなルールを使用するか、これがプロトコル ルールの目的です。JSON はそのようなルールを定式化しています。このルールは、単純な文字列 KEY-VALUE といくつかの補助記号 '{','}','[',']' で構成されています。このルールはより一般的であり、簡単に作成できます。これにより、JSON データを取得した人は誰でも、その中にどのようなデータが含まれているかを一目で知ることができます。

カスタム バイナリ プロトコル形式は普遍的ではありません。データを取得する誰もがその中身を知ることができるわけではなく、受信したデータを解析する方法を知っているのは契約の当事者 2 人だけです。カスタム クラッキングの場合、バイナリ ストリームの内容は推測することしかできません。なぜなら、プロトコル形式は策定時に双方が知るだけだからです (推測はそれほど難しくありませんが、多くのアシスタントは経験に頼ってデータの内容を推測します)。

カスタム バイナリ ストリーム プロトコル形式。次の 3 つの部分に分かれています:

データ サイズ | プロトコル番号 | 特定の
データ コード構造は次のように表現できます:

class Mssage
{   uint Size;   uint CommandID;   byte[] Data; }データ サイズ、プロトコル番号、特定data 、これら 3 つが完全なプロトコルの内容を構成します。もちろん、多くの場合、コマンド ID を特定のデータに含めることができます。






ここで、クライアントがサーバーに送信する必要があるデータ構造を持っていると仮定します:

class TestMsg
{   int test1;   float test2;   bool test3; }サーバーがデータを取得するとき、実際には現在のデータが何であるかわかりません。データが完全であるかどうかはわかりません。データの半分だけを取得することも、データの一部だけを取得することも可能です。したがって、最初に決定する必要があるのは、受信したデータ パケットの完全なサイズです。完全なパケット本体のサイズを知ることによってのみ、現在受信しているデータのサイズが完全であるかどうかを判断できます。待機して受け入れを続ける必要があります。データは現在でも解析できます。パケットの整合性を判断するには、まずバイナリ ストリームに 4 バイトを読み取り、合計 32 ビットの符号なし整数に結合する必要があります。これは、データ パケットのサイズが最大で 32 倍になる可能性があることを意味します。 1. バイト、この整数によって次のデータのサイズがわかります。たとえば、20 バイトを受信した後、最初の 4 バイトを読み取り、整数を形成した後、整数は 24 になります。これは、次の 16 バイトが不完全なパッケージ本体であることを示しており、後続のデータが到着するまで待機し続ける必要があるとします。 。次に決定する必要があるのは、受信したデータ パケットがどのプロトコル形式に属しているかです。そこで、4 バイトのデータを再度読み取り、符号なし整数の CommandID を形成します。これは、プロトコル番号を決定するために使用されます。この符号なし整数のプロトコル番号が 1002 であれば、次のデータはプロトコル番号 1002 のデータ形式であることを意味します。上記の TestMsg クラスがプロトコル番号 1002 のデータ本体であるとすると、パケット サイズの終わりまでのプロトコル番号に接続されているすべてのデータが TestMsg のデータとなり、このインスタンスとして抽出および解析できます。クラス。










この特定のデータを解析するときは、データが生成された順序に従って解析する必要があり、データが書き込まれる順序は、データが読み取られる順序と一致します。このバイナリ ストリーム データを生成するとき、最初に test1 変数をプッシュし、次に test2 変数をプッシュし、次に test3 変数をプッシュするという順序であるとします。このうち、test1 の変数は 4 バイトの整数、test2 の変数は 4 バイトの浮動小数点数、test3 の変数は 1 バイトのブール値であるため、次のようなバイト配列構造になります。xxxx|xxxx |

x
データ内に存在しない場合、このデータは 9 バイトで構成され、最初の 4 バイトが test1、中間の 4 バイトが test2、最後の 1 バイトが test3 になります。
ネットワークに送信されるデータ パケット TestMsg 全体の形式は次のとおりです。

13|1002|test1|test2|test3
上記の形式では、13 は次のデータ パケットのサイズ、1002 ビットのプロトコル番号、test1 test2 です。 test3 は特定のデータです。解析するときは、生成順に解析する必要もあります。最初に最初の 4 バイトを読み取って整数を形成し、test1 に割り当て、次に 4 バイトを読み取って浮動小数点数を形成し、test2 に割り当て、次に 1 を読み取ります。バイトが test3 に割り当てられ、データの解析が完了します。
int[]型データなど、配列形式のデータは生成時に独自に長さマークを付加する必要があり、長さを表す符号なし整数データを先に押し込んでから、すべてを押し込む必要があります。解析時に配列の内容を連続的に読み取ります。同じ逆の操作を行う場合、最初に 4 バイトの長さフラグを読み取り、次に N 個の特定のデータを連続して読み取ります。N は抽出された長さです。たとえば、int[] が 3 つの整数の配列である場合、バイナリは次のような影響を及ぼします:

xxxx|xxxx|xxxx|xxxx
配列内の最初の 4 バイトは長さデータ、次の 4 バイトは整数データです。

カスタム バイナリ ストリーム プロトコル形式は最も一般的ではない形式ですが、各データを最小の方法で定義できるため、最もトラフィックを節約できるプロトコル方式になります。たとえば、プロトコル番号は 4 バイトを必要とせず、サイズは2 バイトの 2 の 16 倍を表します - 1 で十分、65535 で十分です。長さは 2 バイトまたは 1 バイトで十分である限り、4 バイトを必要としない場合があります。一部のデータは int 整数を形成するのに 4 バイトを必要としません。 2 バイト配列 ushort で十分ですが、いくつかは組み合わせて使用​​することもできます。たとえば、プロトコル構造には 4 つのブール値があり、バイトとして渡すことができます。これらは完全に制御できます。パケットのサイズこれは、カスタム バイナリ プロトコル フォーマットの最も魅力的な部分でもあります。

カスタム バイナリ ストリーム プロトコル形式の最大の欠点は、汎用的ではなく、更新が難しいことです。プロトコル形式を変更する必要がある場合、古いプロトコル形式を解析できません。特に、新しいプロトコルが古いプロトコルを解析する場合、エラーが発生します。報告されます。ただし、この問題を解決するために、古いプロトコル形式を引き続き使用できるようにするために、バージョンによって決まるバージョン番号を表す 2 バイトの整数を各データ ヘッダーに追加します。古いプロトコルが新しいプロトコルと互換性を維持できるように、どのバージョンのプロトコルを読み取るかですが、これに対処するときは、初期化の問題、古いプロトコルにはないデータなどに注意する必要があります。新しいプロトコルは、論理エラーが発生しないように、可能な限りデフォルト値に初期化する必要があります。

MessagePack
MessagePack は JSON とカスタム バイナリ ストリーム間のプロトコル形式であり、その哲学は「JSON に似ていますが、高速で小さいです。」です。

JSON と同様に、MessagePack にも Key-Value 形式の Map マッピング タイプがあります。違いは、MessagePack がデータ部分に整数、浮動小数点数、ブール値などを含むバイト形式でデータを格納し、追加することです。マップ マッピング タイプの詳細 カスタム バイナリ ストリームのデータ タイプを含む、独立したタイプのデータ タイプ (KEY-VALUE の形式ではない)。

Map マッピング タイプは、MessagePack でよく使用されるデータ タイプでもあり、比較的一般的なストレージ フォーム タイプであり、その汎用性により多くのプログラマに愛されています。JSONを使ったことのあるプログラマなら、JSONが理解しやすく使いやすいことを知っているでしょう。MessagePackはJSONと同じように使えて、JSONよりもデータサイズが小さく、解析速度もJSONより速いのです。これは著者が言っている通りです。 JSON に似ていますが、高速で小さいです。」

非Map型のデータ部分は実際にはカスタムバイナリストリームの格納方法と同様ですが、カスタムバイナリデータストリームの「データサイズデータ」の形式を「型データ」に変更して格納します。 4 バイト 32 ビット整数:
+----------+--------+--------+--------+---- ---+
| 0xd2 |ZZZZZZZZ|ZZZZZZZZ|ZZZZZZZZ|ZZZZZZZZ|
+-------+-------+-------+-------+-- - ------+
最初のバイトの値 0xd2 は 32 ビット整数型を表します。これは、次の 4 バイトの組み合わせが整数型のデータであることを意味します。もう 1 つの例は、32 ビット浮動小数点数の格納形式です:

+--------+----------+----------+----------+-- ------+
| 0xca |XXXXXXXX|XXXXXXXX|XXXXXXXX|XXXXXXXX|
+--------+--------+--------+--------+--------+
最初のバイト値 0xca は 32 ビット浮動小数点数型を表します。これは、次の 4 バイトの組み合わせが浮動小数点数型のデータであることを意味します。など、nil、bool、8 ビット符号なし整数、16 ビット符号なし整数、32 ビット符号なし整数、64 ビット符号なし整数、8 ビット符号付き整数、16 ビット符号付き整数、32 ビット符号付き整数、 64 ビットの符号付き整数など、および 32 ビット浮動小数点数と 64 ビット浮動小数点数はすべて、これと同様の方法で表現されます。

実際、MessagePack は、これらの個別のデータ型をカスタム バイナリ ストリームで置き換えることができるため、これらの個別のデータ型を対象としていません。私たちが気にするのは、その Map 型データの形式定義です。まず、MessagePack の Map 型ストレージ メカニズムと Json の違い、なぜ JSON より速いのか、なぜ JSON より小さいのか、どのように保存され解析されるのかを見てみましょう。

Map の前に、配列型の形式を見てみましょう:

+----------+-------+----------+~~~~~~~~~~~ ~~~~~~+
| 0xdc |YYYYYYYY|YYYYYYYY| N オブジェクト |
+-------+-------+---------+~~~~~ ~ ~~~~~~~~~~~+
最初のバイト 0xdc の値は、合計 16 ビット長を格納できる配列であることを意味します。つまり、最大格納数は 2 16 回 -1 要素です。 array の後の 2 バイトは、後ろの要素の数を表す符号なし整数を形成するために結合されます。その後の N は、同じ型の要素のデータです。

N 要素が 32 ビット整数データであると仮定すると、上記の配列型の具体的な形式は次のとおりです。

+-------+-------+------- - +~~~~~~~~~~~~~~~~~+
| 0xdc |00000000|00000011| 0xd2|00001001|0xd2|00001101|...(3 オブジェクト)
+-------+-------+-------+ ~~~ ~~~~~~~~~~~~~~+
この配列では配列タイプが指定されていることがわかります。0xdc は配列を表し、次の 2 バイトは合わせて配列要素の数が 11 であることを示します。以下のデータは単一要素のデータ、つまり11個の整数データからなる配列であり、各データは「型データ」の形式で格納されます。実際、Map 型は配列型の変形であり、配列型を基に各要素に KEY 文字列を追加して、Map 型のデータ形式とします。Map の具体的な形式を見てみましょう: +
------ --+----------+----------+~~~~~~~~~~~~~~~~+
| 0xde |YYYYYYYY|YYYYYYYY| N*2 オブジェクト |
+--------+--------+--------+~~~~~~~~~~~~ ~~~~~+
最初のバイト値 0xde は、最大数 16 ビット (つまり、2 の 16 倍 - 1) のマップ タイプ データを表し、次に 2 バイトの組み合わせが要素の数を示します。 N に 2 つの要素を乗算した最後の部分はデータ要素です。2 要素ごとに Key-Value の組み合わせが含まれます。最初の要素は文字列 Key である必要があり、2 番目の要素は任意の個別のデータ型です。

公式の例で分析してみましょう。例えば、JSON 型のデータは: {

"compact":true, "schema":0} です。
このデータは MessagePack 内に Map 型のデータとして存在します。その形式は次のとおりです。

82|A7|'c'|'o'|'m'|'p'|'a'|'c'|'t'|C3|A6|'|'c'|'h'|'e '|'m'|'a'|00|
データ先頭の'82'のデータ、前半バイトの値が8で最大15要素のマップ型データであることを意味します。後半バイトの値は 2 で、合計 2 つの要素を表します。次に2番目のデータ「A7」ですが、「A」が前半バイトの値は次が31文字以内の文字列であることを意味し、後半バイトの値が7は文字列が7文字であることを意味します。次の 7 つの要素は、文字要素とキー位置の文字列です。そして、「C3」はKey-ValueのValue型のデータ部分であり、このValueはbool型の値となります。次に、「A6」は 2 番目の Key-Value データの結合を開始します。ここで、A は 31 要素内の文字列を表す前半のバイトで、後半の 6 バイトは文字列の長さが 6 つであることを表します。次の 6 要素はすべて Key データとして文字です。最後の「00」、最初の0は前半バイトで型が7ビット以内の整数であることを示し、次の後半の0はデータが0であることを意味します。データ全体を分析した後、MessagePack データと Json の {"compact":true, "schema": 0}データに対応します。

MessagePack のマップ全体は、このように「型データ」または「型サイズ データ」として格納されます。保存方法が整っているため、解析時にソートする必要がなく、シンボルや型を解析する必要もありません データの型をバイト単位で直接表現できます バイト単位で保存でき、文字列として保存することはありません使用するバイト数を減らすことができる場合 使用するバイト数を減らすようにし、可能であれば、可能な限り 1 バイトにまとめてください。したがって、JSON の場合、MessagePack は多くの解析を削減し、また多くのデータ フットプリントを削減するため、「JSON に似ています。ただし、高速で小さい。」と書かれているように、MessagePack は JSON よりも高速かつ小型になります。
プロトコル バッファ
Proto3 は Proto2 にさらに改良を加えていますが、プロトコル バッファの内部メカニズムを説明するベンチマークとして Proto2 を使用しています。MessagePack は、JSON 上で多くのデータ スペースとシリアル化と逆シリアル化の最適化を実行しています。実際、これは JSON とカスタム バイナリの混合物として見ることができ、JSON の Key-Value (キーと値のペア) を吸収するだけではありません。 ) は、理解しやすく汎用性があるという利点があり、カスタム バイナリ ストリーム形式の高いシリアル化および逆シリアル化パフォーマンスと小さな記憶領域の特性を吸収します。しかし、そうは言っても、MessagePack の Map 形式のデータ保存形式は結局 Key-Value 形式であり、その Key 値には依然として文字列が使用されており、文字列が保存領域を占有しすぎるという欠点は依然として免れません。

Google Protocol Buffer の登場により、この MessagePack の欠点は補われていますが、Google Protocol Buffer にも無視できない欠点があります。

Google Protocol Buffer (略して Protobuf) は、Google の内部混合言語データ標準であり、RPC システムおよび永続データ ストレージ システムで使用されます。Protobuf は、構造化データのシリアル化またはシリアル化に使用できる軽量で効率的な構造化データ ストレージ形式です。データストレージまたは RPC データ交換フォーマットに非常に適しています。通信プロトコル、データ ストレージ、その他の分野で使用でき、言語、プラットフォームに依存しない、スケーラブルなシリアル化された構造化データ形式です。Protocol Buffer は JSON や MessagePack よりも優れているとよく褒められますが、何がそんなに優れているのでしょうか? それを分析してみましょう。

データ プロトコルの選択の主な焦点は、使いやすいかどうか、シリアル化および逆シリアル化データのパフォーマンスが効率的かどうか、ストレージ容量が小さいかどうか、プロトコル変更後の互換性がより強いかどうかです。これらの特性について、Protocol Buffer がすべてを実行できるかどうかを以下で分析してみましょう。

プロトコル バッファのメッセージ定義
Protobuf のメッセージ定義では、ファイルを作成してそのファイルにメッセージ構造を書き込み、次に Protobuf 生成ツールを使用して、定義されたメッセージ ファイルを指定された言語のプログラム ファイルに生成する必要があります。プログラミング時にプログラムをデシリアライズし、Protobuf をデシリアライズします。

まず、拡張子 .proto のファイルを作成し、ファイル名を MyMessage.proto として、そのファイルに次の内容を保存します。

message LoginReqMessage {   required int64 acct_id = 1;   required string passwd = 2; }上記のメッセージ定義は単純なログインメッセージ定義ですが、内部の構造を説明します。message はメッセージ定義のキーワードであり、C# の struct/class に相当します。LoginReqMessage はメッセージの名前であり、構造体名またはクラス名に相当します。必須のプレフィックスは、フィールドが必須であることを示します。つまり、シリアル化および逆シリアル化の前にフィールドに値が割り当てられている必要があります。











必須と同様に、オプションと繰り返しの 2 つの同様のキーワードがあります。Optional は、フィールドがオプションであることを示します。つまり、シリアル化および逆シリアル化の前に割り当てが必要ないことを示します。オプションと比較して、repeat は主に配列を表す配列フィールドを表すために使用されます。(必須フィールドとオプション フィールドは Protobuf3 でキャンセルされ、未定義の型はすべてオプションです)

int64 と string はそれぞれ 64 ビット長の整数と文字列型のメッセージ フィールドを表します。
実際、Protobuf には型比較テーブルがあり、Protobuf のデータ型と他のプログラミング言語 (C#/Java/C++) で使用される型とを 1 対 1 に対応させるために使用されます。比較表には、さまざまなデータ シナリオでどのデータ型を使用するのがより効率的であるかも示されています。

acct_id と passwd はそれぞれメッセージ フィールド名を表し、C# のドメイン変数名に相当します。

ラベル番号 1 と 2 は、シリアル化後のバイナリ データ内でさまざまなフィールドが配置される場所を示します。

LoginReqMessage 構造体のインスタンス データがシリアル化される場合、acct_id が最初にデータ ストリームにプッシュされ、次に passwd がプッシュされます。passwd フィールドのシリアル化されたデータは acct_id の後に配置する必要があります。番号タグの値はバイナリ ストリーム内の位置を表し、この値を同じメッセージ内で繰り返すことはできないことに注意する必要があります。

また、Protocol Bufferにはメッセージをカスタマイズする際に注意が必要な最適化ルールがあり、1から15までのタグ値を持つフィールド、つまりタグ値と型情報のみをシリアル化した場合に最適化されます。 1 バイトを占め、タグ範囲は 16 です。2047 までは 2 バイトを占め、プロトコル バッファーがサポートできるフィールドの数は 2 の 29 乗マイナス 1、つまり 536870911 個のデータ変数です。この最適化ルールを考慮すると、メッセージ構造を設計する際には、シリアル化後のバイト サイズを効果的に節約できるように、繰り返し型のフィールド ラベルを可能な限り 1 ~ 15 に設定することを検討できます。

多層ネストされたプロトコル バッファー
単一のメッセージを定義することに加えて、同じ .proto ファイル内で複数のメッセージを定義することもできるため、多層ネストされたメッセージの定義を実現できます。 message Person {

    必須の       文字列名 = 1;       必須の int32 id = 2;       オプションの文字列 email = 3;       enum PhoneType {         MOBILE = 0;         HOME = 1;         WORK = 2;       }       message PhoneNumber {         必須の文字列番号 = 1;         オプションの PhoneType タイプ = 2 [default = HOME ];       }       繰り返される PhoneNumber Phones = 4;       繰り返される float Weight_recent_months = 100 [packed = true];     }     message AddressBook {




















proto ファイルに 3 つのメッセージ構造と 1 つの列挙構造を定義しました。ここで、AddressBook メッセージの定義には、フィールド変数として Person メッセージ タイプが含まれ、また、フィールド変数として、PhoneNumber メッセージ タイプが含まれてい       ます
    。
これは、私たちが通常プログラムするデータ構造のネストに非常に似ています。これらのデータ構造は、proto ファイルで集中的に定義されるだけでなく、独自の .proto ファイルで個別に定義することもできます。

プロトコル バッファーには、C++ の Include に相当する別のキーワード「import」キーワードが用意されているため、Proto 構造体を記述するときに同じ .proto ファイル内に多くの一般的なメッセージを定義でき、各モジュールの関数のメッセージ本文の定義は次のようになります。独自の独立したプロト ファイルで管理および定義することも、他の明確な方法で個別に定義することもできます。最後に、

    次 の       ような       インポートキーワードを使用して、動的インポートの方法で必要な構造ファイルをインポートできます。int32 id = 2;       オプションの文字列 email = 3;       enum PhoneType {         MOBILE = 0;         HOME = 1;         WORK = 2;       }       message PhoneNumber {











        必須の文字列番号 = 1;
        オプションの PhoneType タイプ = 2 [デフォルト = HOME];
      }

      繰り返し PhoneNumber 電話 = 4;繰り返しの     ;
      浮動小数点 Weight_recent_months = 100 [packed = true]     import "myproject/Person.proto"     message AddressBook {       repeated Person people = 1;     }次に、独自のモジュールに構造メッセージを記述し、必要なデータ型フィールドを定義してから、すべてのメッセージを Person 構造の proto ファイルにインポートします。輸入されています。「インポート」により、プロジェクト内のデータのチャンク化と階層化を簡単かつ明確に表現できます。必須修飾子、オプション修飾子、および繰り返し修飾子のルールProtobuf2 にはこれら 3 つの修飾子があり、各メッセージには必須タイプのフィールドが少なくとも 1 つある必要があり、データ内に少なくとも 1 つのデータが存在する必要があります。













required 修飾子は、フィールドが必須であることを示します。つまり、データ内のフィールドには、シリアル化および逆シリアル化の前に値が割り当てられている必要があります。また、各メッセージにはオプションのタイプのフィールドを 0 個以上含めることができます。オプションの修飾子は、フィールドがオプションであることを示します。つまり、シリアル化および逆シリアル化の前に代入が必要ありません。値が割り当てられていない場合、データは空です。繰り返し修飾子は、フィールドに 0 個以上の繰り返しデータ、つまり配列型シンボルを含めることができることを示します。なお、repeat は繰り返しのデータを表しており、よく使う配列やリストに相当するもので、値を代入することはできず、代入されていない場合は配列データが 0 個であることを意味します。

プロトコル バッファーの原理 - シリアル化と逆シリアル化
プロトコル バッファーがデータをどのように識別して保存するかが、そのシリアル化と逆シリアル化を理解するための鍵となります。JSON と MessagePack は両方とも、プログラム変数にマッピングされた接続ブリッジとして文字列 Key キー値を使用し、変数の文字列名を使用して対応する Key キー値が存在するかどうかを確認するため、多すぎる Key キーによるスペースの無駄が発生しません。値文字列は避けられません。

プロトコル バッファーは、キー キー値と変数マッピングの間の接続ブリッジとしてデジタル数値を使用します。各変数には、非反復ラベル番号 (デジタル数値) が必要です。これは、プロトコル バッファーのデジタル数値とそれに続く変数フィールドでマップされます。構造体をデータ内のデジタル数値に変換し、データを読み取ります。プロトコルバッファでは構造体変数ごとにラベル番号(つまり数値番号)が定義されており、この番号番号はプログラム変数と指定された数値データの間のマッピング関係を表します。

実際には、このルールだけでは十分ではなく、プログラムが読み込むときに、特定の変数がどのタグ番号に対応するかがわかりません。 name 変数が番号 1 のデータを読み取るようにプログラムに記述されていない限り、どの番号のデータを取得するか。Protobuf ではこの単純かつ大雑把な方法を採用しており、「プログラムに一生懸命書く」という方法で作業を楽にしています。「プログラムに致命的な書き込み」という荒っぽいやり方は、周辺ツールに最もこだわっています。「プログラムに致命的な書き込み」自体が危険なことなので、ツールを介してプログラムを自分で生成する方がはるかに優れています。生成されたプログラムが期待に応えられるように、いくつかのルールを使用し、いくつかのデータを設定し、設定を通じていつでも変更できるからです。Protobuf の周辺ツールは、多くの言語のシリアル化および逆シリアル化プログラム コードを生成するためのツールをカスタマイズします。.proto ファイルを構成ファイルとみなすことができ、Protobuf は .proto 構成ファイルに基づいてシリアル化プログラム ファイルを生成します。.proto ファイルを提供するだけで、さまざまな言語のプログラム コードを生成できます。コード内の変数の読み取り番号と格納番号は、周辺ツールを使用してプログラム内で「ハードコーディング」されます。私たちが呼び出すコードは、生成されます。周辺ツールを使用するため、.proto ファイル内の構造のみを気にする必要があります。

簡単にまとめると、シリアル化および逆シリアル化のために Protobuf によって生成されたコードがデータを読み取るとき、変数名とデジタル数値は、.proto ファイルの内容を通じてコードに「ハードコーディング」されます。特定の数値の場合、その数値のデータが指定された変数に解析されます。たとえば、先ほど説明した Protobuf データ構造の場合、プログラムは数値 1 のデータを読み取ると、そのデータを変数に書き込みます。 name 変数 途中で、name 変数をデータ ファイルに書き込む必要がある場合、最初に数値 1 が書き込まれ、これらの操作を記述するコードは Protobuf ツールによって完成されるため、心配する必要はありません。それについて。

具体的な例を見てみましょう。前述の AddressBook データ構造を使用して、プロトコル バッファ データをシリアル化します。
データを次のようにシリアル化します。

AddressBook アドレスブック;
人 人 = address_book.add_people();
person.set_id(1);
person.set_name("ジャック");
person.set_email("[email protected]");
Person.PhoneNumber 電話番号 = person->add_phones();
電話番号.set_number("123456");
電話番号.set_type(人物.HOME);
電話番号 = person.add_phones();
電話番号.set_number("234567");
電話番号.set_type(人物.










3c // 0x3c = 60、次の 60 バイトが Person データであることを示します。

// 以下は、繰り返される person 配列のデータ構造を入力します。
0a // (1 << 3) + 2 = 0a、person の最初のフィールド名ラベル番号は 1、2 は文字列 (string) に対応するタイプ番号
04 // 名前フィールドの文字列長は 4
4a 61 63 6b // "Jack" の ASCII コード

10 // (2 << 3) + 0 = 10、フィールド id のタグ番号は 2、0 は int32 に対応する型番号 01
// id の整数データは 1

1a // (3 << 3) + 2 = 1a、タグ番号フィールド email は 3 、 2 は
文字列に対応するタイプ番号 0b // 0x0b = 11 email フィールドの文字列の長さは 11
4a 61 63 6b 40 71 71 2e 63 6f 6d // "[email protected]"

    //最初の PhoneNumber、埋め込まれた Set メッセージ
    22 // (4 << 3) + 2 = 22、電話フィールド、ラベル番号は 4、2 は入れ子構造に対応するタイプ番号 0a // 0a = 10
    、次の 10 バイトは PhoneNumber 0a // (1 << 3) + 2 = 0a のデータです
    。PhoneNumber の番号、タグ番号は 1、2 は文字列
    06 に対応するタイプ番号 // 番号フィールドの文字列長は 6
    31 32 33 34 35 36 // "123456"
    10 // (2 << 3) + 0 = 10、PhoneType タイプ フィールド、0 は enum 01 に対応するタイプ番号です
    // HOME、enum は整数とみなされます

    // No. 2 PhoneNumber、ネストされたメッセージ
    22 0a 0a 06 32 33 34 35 36 37 10 00 //メッセージの解釈は上記と同じ、最後の 00 は MOBILE a2 06

// 1010 0010 0000 0110 varint メソッド、キーof Weight_recent_months
        // 010 0010 000 011 0 → 000 0110 0100 010 リトルエンディアン ストレージ
        // (100 << 3) + 2 = a2 06, 100 は、weight_recent_months のラベル番号です
        // 2 は、パックされた繰り返しのタイプ番号ですfield
0c // 0c = 12、次の 12 バイト float データの場合、4 バイトごとに 1 つのデータ
00 00 48 42 // float 50
00 00 50 42 // float 52
00 00 58 42 // float 54
上記のバイナリ データはコンパクトなバイト配列であり、分析中に分解するとより明確になります。1 行目の 0a は、(1 « 3) + 2 = 0a で生成されます。1 は人物のタグ番号、2 は埋め込み構造に対応する繰り返しタイプ番号です。これに続く 2 行目の 3c は 0x3c = 60 を意味し、次の 60 バイトのデータが個人データであることを意味します。したがって、次のステップでは、繰り返される Person 配列のデータ構造の内容を入力します。最初に遭遇するデータ 0a は (1 « 3) + 2 = 0a によって生成され、1 は Person の最初のフィールド名のラベル番号が 1 を表し、2 は文字列 (string) が 2 に対応する型番号を表します。次の 04 は、名前フィールドの文字列長が 4 であることを示し、その後に名前フィールド文字列の特定のデータが続きます。4a 61 63 6b は、「Jack」の ASCII コードです。次のデータ 10 は、(2 « 3) + 0 = 10 によって生成されます。2 はフィールド ID のタグ番号が 2 であることを意味し、0 は int32 に対応する型番号が 2 であることを意味します。次に int32 のデータ内容ですが、01 はデータ構造体のフィールド変数 id の整数データが​​ 1 であることを意味します。次は電子メール文字列コンテンツの文字列で、最初にラベル番号 + タイプ番号がマークされ、次にデータ サイズ、そして文字列の特定のデータが続きます。

次に、PhoneNumber の配列型ですが、まず、22 はタグ番号とタイプ番号を表し、(4 « 3) + 2 = 22、4 は電話フィールドのタグ番号が 4 であることを意味し、2 はタイプ番号を意味します入れ子構造に対応するのは 2 です。次に、0a = 10 となり、次の 10 バイトが PhoneNumber データであることを示します。すると、(1 « 3) + 2 = 0a により 0a が生成されます。1 は PhoneNumber の番号タグが 1 であることを意味し、2 は文字列に対応する型番号が 2 であることを意味します。次に、文字列長 06 は、数値フィールドの文字列長が 6 であることを示します。31 32 33 34 35 36 は文字列「123456」の ASCII エンコードです。以下は enum 型です。10 は (2 « 3) + 0 = 10 によって生成されます。2 は PhoneType 型フィールドのラベル番号を表し、0 は enum に対応する型番号です。次に enum の具体的なデータです。01 は列挙値 HOME を表し、すべての enum は整数とみなされます。2 番目の PhoneNumber も同じデータ形式です。

最後のデータは浮動小数点配列のweight_recent_monthsフィールドで、a2 06は(100 « 3) + 2 = a2 06によって生成されます。100はweight_recent_monthsのラベル番号、2はパックされた繰り返しフィールドのタイプ番号です。次の 0c は 0c = 12 を意味します。これは、次の 12 バイトが float 配列データであり、4 バイトごとに 1 つのデータがあることを意味します。したがって、次の 3 つのデータ、つまり各データの 4 バイトは浮動小数点数を直接表し、00 00 48 42 は float 50 を表し、00 00 50 42 は float 52 を表し、00 00 58 42 は float 54 を表します。

この例は、「プロトコル バッファ: バイナリ ファイルの読み取り」からのものです。バイナリ データ全体の分析は、ラベル番号 + タイプ番号、ヘッダーの識別子、およびデータ サイズの識別子という単純なルールに従います。次の形式で、オプションの識別子データとして特定の識別子に配置されます。

タグ番号+タイプ番号 | データサイズ | 特定データ
特定データ内に異種データがネストされている場合も、「タグ番号+タイプ番号 データサイズ 特定データ」の規則に従います。
前にシリアル化されたデータの内容を分析し、次に逆シリアル化プロセスを見ていきます。
バイナリ データ ストリームをプログラム オブジェクト データに逆シリアル化するプロセスにおいて、タグ番号と変数の間のマッピング関係は、プログラムによってコード内に「ハードコーディング」されます。引き続き上記の protobuf 構造を例として取り上げます。 Person 構造体 逆シリアル化プロセス:

public void MergeFrom(pb::CodedInputStream input) {   uint tag;   while ((tag = input.ReadTag()) != 0) {     switch(tag) {       デフォルト:         _unknownFields = pb::UnknownFieldSet .MergeFieldFrom (_unknownFields, input);         ブレーク;       ケース 1: {         name = input.ReadString();         ブレーク;       }       ケース 2: {         id = input.ReadInt32();         ブレーク; }













      }
      ケース 3: {         email = input.ReadString();         Break;       }       ケース 4: {         phones_.AddEntriesFrom(input, _repeat_phones_codec);         Break;       }       ケース 100: {         weight_recent_months_.AddEntriesFrom(input, _repeat_weight_recent_months_codec);         ak;       }     }   }上記のプロトコル バッファーによって生成されたコードから、すべてのオブジェクト変数が .proto ファイル内のラベル番号を使用して、データと変数のマッピング関係があるかどうかを識別していることがわかります。特定のデータを取得するときは、最初にラベル番号を使用してどの変数名へのマッピングを決定し、データを読み取り、変数の型に応じて値を割り当てます。プロトコル バッファーのデータ構造変更後の互換性の問題








        









実際の開発では、要件の変更によりメッセージ フォーマットを変更する必要があるアプリケーション シナリオが存在しますが、元のメッセージ フォーマットを依然として使用している一部のアプリケーションは、さまざまな理由によりアップグレードを希望しません。このプログラムでは、新旧のメッセージ フォーマットに基づいた新旧のクライアント プログラムがスムーズに実行できるように、メッセージ フォーマットを更新するときに特定のルールに従うことが求められます。注意すべきルールは次のとおりです。

既存のフィールドのタグ番号を変更しないでください。つまり、.proto ファイル内の構造メッセージ変数フィールドの後の番号を簡単に変更してはなりません。これにより、古いデータが確実に保存されます。プロトコルは、データから指定されたタグ番号を正しいデータから読み取り続けることができます。タグ番号を変更すると、新旧クライアント間で新旧データの互換性が失われます。

追加されるフィールドでは、オプションの繰り返し修飾子を使用する必要があります。これにより、古いデータを新しいデータ フィールドに追加できない場合に、新しいプロトコル データを古いデータ プロトコルでスムーズに解析できるようになります。オプションのタグや繰り返しタグを使用しない場合、メッセージを相互に受け渡すときに、古いプログラムと新しいプログラムの間でメッセージの互換性を保証できません。

元のメッセージでは、既存の必須フィールドは削除できません。省略可能なフィールドや繰り返しフィールドは削除できますが、以前に使用されていたタグ番号は保持する必要があり、新しいフィールドで再利用することはできません。古いプロトコルは実行時に古いタグ番号に独自のデータを追加したままであるため、新しいプロトコルが古いタグ番号を使用すると、新旧プロトコルのデータが誤って解析される問題が発生します。

int32、uint32、int64、uint64、bool などの型には互換性があり、sint32 と sint64 には互換性があり、string と bytes には互換性があり、fixed32 と sfixed32、fixed64 と sfixed64 には互換性があります。つまり、必要に応じて、元のフィールドでは、互換性を確保するために、元の型と互換性のある型にのみ変更できます。変更しないと、新旧のメッセージ形式の互換性が失われます。

プロトコルバッファーの利点
Protobuf は全体でバイナリ ストリーム形式を使用し、キー値の代わりに整数を使用して変数をマップします。これは、XML、Json、および MessagePack よりも小さくて高速です。

独自の .proto ファイルを自由に定義および作成して、そのファイルに独自のデータ構造を書き込むことができます。その後、Protobuf コード生成ツールによって生成された protobuf コードを使用して、シリアル化および逆シリアル化する必要がある protobuf データ構造の読み取りおよび書き込みを行うことができます。プログラムを再デプロイすることなくデータ構造を更新することもでき、Protobuf を使用してデータ構造を一度再記述するだけで、protobuf データはさまざまな言語で、またはさまざまなデータ ストリームから簡単に処理でき、読み書きできます。

Protobuf のプログラミング モードはフレンドリーで学びやすく、優れたドキュメントと例があるため、Protobuf を使用するときに複雑なドキュメント オブジェクト モデルを学習する必要はありません。シンプルで使いやすいツールを好む人にとって、Protobuf はさらに便利です。他のテクノロジーの力よりも魅力的です。

Protobuf のセマンティクスもより明確であり、XML パーサーや JSON パーサーなどが必要ないため、解析操作が簡素化され、解析の消費量が削減されます。

Protobuf データはバイナリ形式を使用し、JSON および XML の文字列に格納された数値はバイト ストレージに置き換えられるため、無駄なストレージ スペースが大幅に削減されます。MessagePack と比較して、Protobuf はキーの記憶領域を削減し、キーの元の式を文字列の整数式に置き換えます。これにより、記憶領域が削減されるだけでなく、逆シリアル化の速度も高速化されます。

プロトコルバッファ不足
XML や Json データ形式と比較すると、Protbuf には機能が単純で、複雑なデータ概念を表現するのに使用できないという欠点もあります。XML と Json はさまざまな業界標準の記述ツールとなっていますが、プレーン テキスト表現によりデータ形式はより使いやすくなっていますが、Protobuf はデータの送信と保存にのみ使用されており、さまざまな分野での汎用性はまだはるかに遅れています。XML と Json はある程度説明の必要がないため、人間が直接読み取ったり編集したりできますが、Protobuf はそうではありません。これはバイナリ モードで保存され、.proto 定義がない限り、Protobuf のコンテンツを直接読み取ることはできません。 。

 転載元:よく書いてあるのでメモを転載してください!Unity3D 高度なプログラミング ネットワーク層分析 データ プロトコルの原則 - Unity3D テクノロジー - ゲーム開発ネットワーク - ゲーム ソース コード プログラミング テクノロジーのウェブサイト! - ゲーム開発テクノロジーのウェブサイト! (yxkfw.com) icon-default.png?t=M4ADhttps://www.yxkfw.com/thread-70138-1-1.html

Guess you like

Origin blog.csdn.net/hack_yin/article/details/125315106