2011-12-15 11 views
0

私は同じ送信/受信インフラストラクチャを使用して、さまざまな時間に異なるフォーマットのメッセージを渡すことができるようにするために、TCPクライアント/サーバー設定を使用しています。事前シリアル化メッセージオブジェクト - 実装?

、クライアントからサーバーに送信されたメッセージの2つのタイプがあるかもしれない:

  • TIME_SYNC_REQUEST:サーバーのゲームの時間を要求します。メッセージタイプ以外の情報は含まれません。

  • UPDATE:(最後に接続されたものではない場合は)最後に更新された後に発生したゲームの状態をすべて記述して、サーバーがそのデータモデルを更新できるようにします。動的言語で

(メッセージタイプは、ヘッダに含まれる、任意のデータがメッセージの本文に含まれる。)

、Iは、AbstractMessageタイプを作成し、導出したいです余分なデータメンバーを収容していないTimeSyncRequestMessageと、必要なメンバー(プレーヤーの位置など)をすべて含むUpdateMessageという2つの異なるメッセージタイプがあり、ソケットsend()のために実際に何が必要なのかを見るために反射を使用します。クラス名は型を記述しているので、そのために追加のメンバは必要ありません。

C++の場合パフォーマンス上の理由から、dynamic_castを使用して上記の方法を反映させたくありません。構成可能なアプローチを使用する必要があります。ダミーメンバーはの可能なデータを入力してください。データとchar messageType?別の可能性は、異なるタイプのリストに異なるメッセージタイプを保持することだと思います。これは唯一の選択ですか?それ以外の場合は、それをシリアル化するときまで、メッセージ情報を格納するために他に何ができますか?

答えて

1

おそらく、メッセージクラスにシリアル化を行うことができます - シリアル化インターフェイスを定義し、各メッセージがこのインターフェイスを実装します。したがって、シリアル化して送信するときは、AbstractMessage :: Serialize()を呼び出してシリアル化されたデータを取得します。

+0

動的言語では、リフレクションを使用して、実際にシリアル化する必要があるものを確認します。 C++では、インターフェイス関数を呼び出し、シリアル化されたデータを取得します(派生クラスは、シリアル化する必要のあるデータを知っている必要があります)。違いはなんですか? – Shawnone

+0

OK。インタフェース関数は 'void(*)'を返しますか?それはインターフェイス関数なので、私はそれが必要だと思います。 –

+0

ベクトル、またはstruct {char *、length}になります。形式は "messageCategory(固定長)、messageID(固定長)、シリアライズされたデータ"です。 – Shawnone

0

一般的なアプローチは、すべてのメッセージに単純にヘッダーを付けることです。

struct header 
{ 
    int msgid; 
    int len; 
}; 

その後ストリームは、ヘッダとメッセージデータの両方が含まれます:たとえば、あなたはこのようになります。ヘッダ構造を持っているかもしれません。ヘッダーの情報を使用して、ストリームから正しい量のデータを読み込み、それがどのタイプであるかを判断することができます。

残りのデータのエンコード方法とクラス構造の設定方法は、アーキテクチャによって大きく異なります。各ホストが同じで、同じコードを実行するプライベートネットワークを使用している場合は、構造体のバイナリダンプを使用できます。それ以外の場合は、おそらくGoogle Protobufを使用してシリアライズされた、または各タイプの可変長データ構造を持ちます。擬似コードで

、メッセージの受信側では次のようになります。あなたのシリアライズに依存のような機能が見える「読み」とは何

read_header(header); 
    switch(header.msgid) 
    { 
    case TIME_SYNC: 
     read_time_sync(ts); 
     process_time_sync(ts); 
     break; 

    case UPDATE: 
     read_update(up); 
     process_update(up); 
     break; 

    default: 
     emit error 
     skip header.len; 
     break; 
    } 

。 Googleの基本は、基本的なデータ構造を持ち、さまざまな言語で作業する必要がある場合は、まともです。 C++のみを使用し、すべてのコードが同じデータ構造ヘッダーを共有できる場合は、ブーストのシリアライズが有効です。

+0

お手数をおかけしていただきありがとうございます。おそらく私の質問ではっきりと分かりませんでした。「本来、中間データ構造が必要になってから、シリアル化して送信するまでには、インターリムに保存する最も効率的な方法は何ですか?あなたは受信側で何が起こるかを指しています、そしてそれは大きな説明です、私の現在の質問への答えではありません。 –

+0

私は理解しています。シリアライズを強化するには、中間体を必要としないことになります。ネイティブタイプに対して「シリアライズ」メソッドを作成するだけです。もしあなたが本当に一時的なものを持っていれば、protobufもそれのために働くことができます(私はそれを使いました)。ネイティブデータと直列化されたprotobuf形式の中間の構造を作る必要はありません。 –

1

非常に高いパフォーマンス特性を持たない限り、私は自己記述メッセージフォーマットを使用します。これは通常、一般的な形式(たとえばkey = value)を使用しますが、特定の構造体を使用せず、代わりに既知の属性がメッセージの種類を記述し、そのメッセージ型に固有のロジックを使用してそのメッセージから他の属性を抽出できます。

このタイプのメッセージングでは下位互換性が向上しています。新しい属性を追加したい場合は追加して、古いクライアントは追加できます。固定構造を使用するメッセージングは​​あまりうまくいかない傾向があります。

EDIT:自己記述メッセージフォーマットの詳細。基本的にここでのアイデアは、フィールドの辞書を定義することです。これは、汎用メッセージに含まれるフィールドの世界です。デフォルトのメッセージにはいくつかの必須フィールドが含まれている必要があります。メッセージに他のフィールドが追加されるのはあなた次第です。シリアライゼーション/デシリアライゼーションは非常に簡単で、追加するすべてのフィールドを持つBLOBを構築し、もう一方の端ですべての属性(マップを想像してください)を持つコンテナを作成します。必須フィールドにはタイプを記述することができます。たとえば、辞書にメッセージタイプを指定することができます。これはすべてのメッセージに設定されます。このフィールドを調べて、このメッセージの処理方法を決定します。処理ロジックに入ると、ロジックがコンテナ(マップ)から必要とする他の属性を抽出して処理するだけです。

このアプローチは、最高の柔軟性を提供し、実際に変更されたフィールドのみを送信するようなことを可能にします。今この状態をどのように保つかはあなた次第ですが、メッセージと処理ロジックの間に1対1のマッピングがあるため、継承も合成も必要ありません。このタイプのシステムのスマートさは、フィールドをシリアル化する方法(フィールドがどの辞書の属性であるかを知るために逆シリアル化する方法)に由来します。このようなフォーマットの例として、FIXプロトコルを見てみましょう - 今私はゲームのためにこれを主張しませんが、アイデアは自己記述メッセージが何であるかを示すべきです。

EDIT2:完全な実装はできませんが、ここではスケッチです。

まず私が値型を定義してみましょう - これはフィールドに存在することができる値の典型的なタイプです:

typedef boost::variant<int32, int64, double, std::string> value_type; 

今、私はフィールド

struct field 
{ 
    int field_key; 
    value_type field_value;  
}; 

が今ここに私のメッセージがある説明コンテナ

struct Message 
{ 
    field type; 
    field size; 

    container<field> fields; // I use a generic "container", you can use whatever you want (map/vector etc. depending on how you want to handle repeating fields etc.) 
}; 

ここでは、というメッセージを作成したいとします...更新は、私に

boost::unique_ptr<Message> getTimeSyncMessage() 
{ 
    boost::unique_ptr<Message> msg(new Message); 
    msg->type = { dict::field_type, TIME_SYNC }; // set the type 

    // set other default attributes for this message type 

    return msg; 
} 

は今、私はより多くの属性を設定する適切なスケルトンを生成するファクトリを使用して、私は例えばサポートされているフィールドの辞書を必要とする場所です

namespace dict 
{ 
    static const int field_type = 1; // message type field id 

    // fields that you want 
    static const int field_time = 2; 
    : 
} 

は、だから今私が言うことができ、

boost::unique_ptr<Message> msg = getTimeSyncMessage(); 

msg->setField(field_time, some_value); 
msg->setField(field_other, some_other_value); 
: // etc. 

は今、あなたは送信する準備ができている、このメッセージの直列化は、単にコンテナをステップ実行し、ブロブに追加されます。 ASCIIエンコーディングまたはバイナリエンコーディングを使用することができます(要件に応じて、最初は前者から始め、後者は後者に移ります)。ここでは、引数のために

1=1|2=10:00:00.000|3=foo 

が、私は、あなたが自分の価値観では起こらない保証できる何か他のものを使用することができますフィールドを区切るために |を使用:だからのASCIIエンコードされたバージョンは、上記のようなものである可能性があり。バイナリ形式(これは関係ない)では、各フィールドのサイズをデータに埋め込むことができます。 、その後、記入 - (フィールド 1あなたはタイプを持ってたら)スケルトンを生成するためのファクトリメソッドを使用し、

直列化復元は、ブロブをステップでしょう(そう例えば|によってseperatingで)適切に各フィールドを抽出しますコンテナ内のすべての属性。 - あなたが特定の属性を取得したいとき後で、何か行うことができます:私はこれが唯一のスケッチである知っている

msg->getField(field_time); // this will return the variant - and you can use boost::get for the specific type. 

を、うまくいけば、それは自己記述形式の背後にある考え方を伝えます。基本的なアイデアが得られたら、実行できる最適化がたくさんありますが、それはまったく別のものです...

+0

それではもっと構成的なアプローチですか?もしそうであれば、ダミーメンバーは、ここがポインタであったとしても、ここでは必要になります。私はこれで生きることができると思う。私は10回のうち9回の相続よりも組成が好きです。あなたが見ることができる他の解決策はありませんか?それは本当に言語の基礎になります。 –

+0

@NickWiggill、いいえ、「ダミーメンバー」は必要ありません。メッセージは自己説明的なものであり、更新したいフィールドのみが含まれています。これは純粋にフォーマットに基づいたフォーマットデルタ(更新が必要な属性の更新のみ) – Nim

+0

コード例はどうですか?私が質問している理由は、その答えが実装上明確でないためです。 –

0

通常の方法は、メッセージタイプを送信してシリアル化されたデータを送信することです。
受信側では、メッセージタイプを受信し、そのタイプに基づいて、factory method(マップまたはスイッチケースを使用)を介してクラスをインスタンス化してから、オブジェクトにデータを直列化解除させます。

+0

最初に構築された1つのメッセージを表現するオブジェクトが1つ必要となり、数ミリ秒後にシリアル化されて送信されるため、データをシリアル化するまでにデータを保存するのが最適です。 「最善の方法」とは、主にアクセスの容易さと、副次的な空間効率を指します。 –

0

あなたのパフォーマンス要件はdynamic_castを除外するほど強力ですか?私は、一般的な構造のフィールドをテストする方が速くなる可能性はないので、メッセージごとに異なるリストだけを残すことはできません。どのような場合でもオブジェクトのタイプを他の方法で知る必要があります。しかし、あなたは抽象クラスへのポインタを持つことができ、それらのポインタに対して静的なキャストを行います。

dynamic_castの使用状況を再評価することをお勧めします。ネットワークアプリケーションにとって致命的ではないとは思いません。接続の送信端に

+0

ここには、dynamic_castパフォーマンスの[例](http://www.nerdblog.com/2006/12/how-slow-is-dynamiccast.html)があります。これはC++の人生のかなり受け入れられている事実です。申し訳ありませんが、特定の高周波動作で2倍の速度低下が私が受け入れるよりも多くあります。 –

+0

@NickWiggillあなたのポインタをありがとう。これは、使用する予定のコンパイラとアーキテクチャと同じですか?さらに、回避策を講じて同様のコストを負担しないと確信していますか? dynamic_castの特定の使用が残りのコードとネットワークの待ち時間に対してどれくらい重くなるでしょうか? – dsign

+0

gccの下にあるマルチプラットフォーム(Win、Deb Linux、MacOS)。同様の費用が発生しないことを私は非常に確信しています。最も簡単な回避策は、すべての異なるタイプのメッセージに対応するために必要な数のメンバーを持つメッセージオブジェクトを使用することです。私は約20hzでパケットを送信します。そして、ハイファイソフトウェアレイキャスター、そしてすべてのユニットAIと一般的なゲームロジック、そしてIOが世界のチャンクで流通するための膨大な処理があります。本当に、私はちょうどもっとエレガントな方法があるかどうかここで人々から知りたがっていました。 –

0

、私たちのメッセージを構築するために、我々はメッセージデータとは別のメッセージIDとヘッダを保つ:

  • MessagemessageCategorymessageIDを保持しているタイプです。
  • このような各Messageは、統一されたmessageQueueにプッシュされます。
  • messageCategoryそれぞれに関連するデータに対して、別個のハッシュが保持されます。これらには、そのタイプのメッセージごとに、messageIDと入力されたデータのレコードがあります。値の種類はメッセージカテゴリに依存するため、TIME_SYNCメッセージの場合は、たとえばstruct TimeSyncMessageDataとなります。

シリアライズ:

  • 我々は&送信をシリアル化したいデータを取得するために、messageIDにより、そのメッセージタイプに適切なハッシュを参照し、messageQueueからのメッセージをポップ。
  • シリアル番号&がデータを送信します。

利点:

  • 単一の汎用Message対象ではありません潜在的に使用されていないデータメンバー。
  • シリアル化に時間がかかる場合のデータ取得のための直感的な設定。
関連する問題