多くのゲーム開発者は、特にソケットを操作するための独自のバイクを作成する場合、クライアント/サーバー通信プロトコルの記述と実装の問題に直面しています。 以下では、アプリケーションのさらなる使用とスケーリングのために、できるだけエレガントで便利な問題を解決するための私の試みについてお話します。 多くのコンパイル時の自動コード生成があり、実行時のピンチで穏やかに味付けされます。
問題の声明
クライアントとサーバーは常にメッセージを交換していますが、これらのメッセージは最初に準備、転送、そして読み取り可能な形式に復元する必要があります。 以下の簡単な図:

主な問題は、メッセージの読み取りと逆シリアル化の段階で発生し、バイトストリームが受信者に届きます。正しい逆シリアル化のためには、メッセージの構造、つまりタイプを知る必要があります。 型に対するすべての操作はコンパイル時に終了し、コンパイラの助けはもうありません。 最初に思い浮かぶ最も残忍な決定は、メッセージIDと特定の関数をバインドしてメッセージをアンパックする巨大なスイッチを作成することです。 プロトコルを処理するときにこの決定が頭痛の種になり、膨大な数のエラー検出が困難になる理由を説明する必要はないと思います。 この問題を解決します。
最初に、取得するものを決定する必要があります。
- メッセージIDと特定のメッセージハンドラクラスを1回バインドします。 そして、すべて、IDについてこれ以上覚えてはいけません。 ほぼこの方法で:
0 AMessage
1 BMessage
2 CMessage
- 使用エラーを正しく処理し、コンパイル段階でコードを台無しにする試みを防ぎます。 たとえば、C ++では、コンパイル時の構造を扱うときに明確なエラーメッセージを取得することはほとんど不可能です。
- 簡単な使用、1つの関数呼び出し、および処理可能なメッセージに変換されたバイトストリーム。
依存関係
私たちのプロジェクトは独自のシリアライザを使用し、シリアライザはコンパイル時間も積極的に使用します(これは別の投稿のトピックです)。 これらの呼び出しで、クラスとそのフィールドをバイトに、またはその逆に変換できる特定のブラックボックスがあることに同意します。
auto stream = serialize!(ByteBackend)(SomeObject, "name"); auto object = deserialize!(ByteBackend, SomeClass)(stream, "name");
また、表示を簡単にするために、メッセージが暗号化されておらず、シリアライザーがすべてのセキュリティ問題を解決すると単純に仮定します。メッセージが宣言された構造と一致しない場合、例外がスローされ、問題のメッセージは無視されます。
さらに進むコードはすべてdmd 2.060でテストされており、おそらく2.059(非常に不快な子供の病気D2)ではコンパイルされません。
メッセージ
各メッセージは、関数演算子がオーバーロードされ、パラメーターのないコンストラクターを持つ特定のクラスです(逆シリアル化の要件)。 最初の要件は形式化するのが簡単で、メッセージはすべてこのインターフェイスを実装する必要があります。
コード interface Message { void opCall(); }
メッセージの例:
class AMsg : Message { int a; string b; this() {} this(int pa, string pb) { a = pa; b = pb; } void opCall() { writeln("AMsg call with ", a, " ", b); } }
2番目のコンストラクターは、これに関するメッセージと、以下のパラメーターなしのコンストラクターの存在の確認に関するメッセージを作成するために必要です。
魔法を始める
C ++では、テンプレート化されたテンプレートパラメータを持つ多くの構造を使用しますが、Dでは、コンパイル時にコードを実行する他の方法があります。 できるだけコンパイル時のコードが実行可能ファイルに収まるように、テンプレートとミックスインを使用します。 全体として、すべてのコードはテンプレートミックスインに含まれ、別のアプリケーションまたは同じバージョンで簡単に再利用できます。
mixin template ProtocolPool(IndexType, SerializerBackend, pairs...) { }
IndexTypeは、使用するインデックスのタイプです。
SerializerBackendはシリアライザーのバックエンドです。別のアプリケーションでは、バイトまたはバイトではなくシリアル化の別のメカニズムが使用される可能性がありますが、xml / jsonが使用されます。
ペア...-最も興味深いパラメータ、ペアはここに記述されます:idとメッセージタイプ。 以下の例:
mixin ProtocolPool!(int, BinaryBackend, 0, AMsg, 1, BMsg, 2, CMsg );
エラー処理
しかし、ユーザーは何でもペアに詰め込み、この脆弱な契約に違反することができます。そうすれば、問題はそれほど長くかかりません。 正当性を確認する必要があります。 したがって、ペアを実行する別のテンプレートをテンプレートに挿入し、わかりやすいエラーメッセージでコンパイルを停止します。
コード template CheckPairs(tpairs...) { static if(tpairs.length > 1) { static assert(__traits(compiles, typeof(tpairs) ), "ProtocolPool expected index first, but got some type"); static assert(is(typeof(tpairs[0]) == IndexType), "ProtocolPool expected index first of type "~ IndexType.stringof~ " not a "~typeof(tpairs[0]).stringof); static assert(is(tpairs[1] : Message), "ProtocolPool expected class implementing Message"~ " interface following index not a "~tpairs[1].stringof); static assert(CountValInList!(tpairs[0], pairs) == 1, "ProtocolPool indexes must be unique! One message,"~ "one index."); enum CheckPairs = CheckPairs!(tpairs[2..$]); } else { static assert(tpairs.length == 0, "ProtocolPool expected even number of parameters. Index and message type."); enum CheckPairs = 0; } }
__traits(compiles、sometext)の呼び出しは
ここでは明確
ではないかもしれませんが、これはsometextがコンパイルされるかどうかをチェックするコンパイラーへの明示的な要求です。 組み込みのTraitsの詳細については、
こちらをご覧ください 。 そして、テンプレート宣言の直後に、
静的アサートを介してそれを呼び出します。 このテンプレートを呼び出すことはできますが、コンパイラは明らかに意味のない表現を誓いますが、それは時々邪魔になります。
コード mixin template ProtocolPool(IndexType, SerializerBackend, pairs...) { template CheckPairs(tpairs...) {
注意深い読者(誰かがこの行に到達した場合)は、リスト内の値の出現回数をカウントする
CountValInListテンプレートを定義しなかったことに気付いた
はずです。
コード生成
素晴らしい、すべての誤用はカットされ、適切に処理されます。 そのようなエラーメッセージから、科学的な突く方法を使用する正しい方法を見つけることはかなり可能です(これはあなたがドキュメントを書くことからあなたを救いません!)。 次に、タスク自体について考える必要があります。 使いやすさとスピードの間の妥協が必要です。停止してください。これとその両方を同時に取得できます。 プログラマーの参加なしに、巨大なスイッチを自動的に生成します。
このテンプレートは、次のような行を生成します。
コード switch(id) { case(0): return cast(Message)(func!(SerializerBackend, AMsg)(args)); break; case(1): return cast(Message)(func!(SerializerBackend, BMsg)(args)); break; case(2): return cast(Message)(func!(SerializerBackend, CMsg)(args)); break; default: break; }
これで、結果の文字列をディスパッチ用の関数に混在させることができます。
コード内のこの関数の呼び出しは次のようになります。
コード void readMsg(Stream stream) { int id; stream.read(id); writeln("Got message id is ",id); auto message = dispatchMessage!(deserialize)(id, stream, "MSG"); writeln("Calling message"); message(); }
実際、最も難しい部分は書かれていますが、便利なメッセージ設計にはあらゆる種類の利点しかありません。 誰も手動でそれをしたくないのですか?! 次のように行う方がはるかに便利です。
auto stream = constructMessage!AMsg(10, "Hello World!");
IDなし、その他の追加物はありません。 パラメーターはすぐにメッセージコンストラクターに渡され、メッセージはバイトストリームにシリアル化されます。 これを書くことは残っています...タイプごとにメッセージIDを検索できる必要があります。これにはもう1つのテンプレートが必要です:
コード template FindMessageId(Msg, tpairs...) { static if(tpairs.length > 0) { static if(is(tpairs[1] == Msg)) enum FindMessageId = tpairs[0]; else enum FindMessageId = FindMessageId!(Msg, tpairs[2..$]); } else static assert(false, "Cannot find id for message "~ Msg.stringof~". Check protocol list."); }
この時点で、私の小さな人々は、関数型プログラミングの流行に苦しんでいるという考えを持つはずです。 私はすべてのパラダイムを尊重しますが、コンパイル時のテンプレートには変更可能な状態はないため、機能的なスタイルがここで自然に発生します。 これで、メッセージの種類のみを知って、メッセージを作成するのは難しくありません。
コード Stream constructMessage(Msg, T...)(T args) { static assert(is(Msg : Message), Msg.stringof~ " must implement Message interface!"); static assert(__traits(compiles, new Msg(args)), Msg.stringof~ " should implement constructor with formal parameters "~ T.stringof); auto msg = new Msg(args); IndexType sendId = FindMessageId!(Msg, pairs); auto stream = serialize!SerializerBackend(msg, "MSG"); auto fullStream = new MemoryStream; fullStream.write(sendId); fullStream.copyFrom(stream); fullStream.position = 0; return fullStream; }
使用する
この洗練されたシステムができたので、実際にテストする必要があります。 このために、私はunittestを書きました:
コード version(unittest) { class AMsg : Message { int a; string b; this() {} this(int pa, string pb) { a = pa; b = pb; } void opCall() { writeln("AMsg call with ", a, " ", b); } } class BMsg : Message { double a; double b; this() {} this(double pa, double pb) { a = pa; b = pb; } void opCall() { writeln("BMsg call with ", a, " ", b); } } class CMsg : Message { double a; string s; this() {} this(double pa, string ps) { a = pa; s = ps; } void opCall() { writeln("CMsg call ", a, " ", s); } } mixin ProtocolPool!(int, GendocArchive, 0, AMsg, 1, BMsg, 2, CMsg ); } unittest { void readMsg(Stream stream) { int id; stream.read(id); writeln("Got message id is ",id); auto message = dispatchMessage!(deserialize)(id, stream, "MSG"); writeln("Calling message"); message(); }
完全なソースコード
以下の図の完全性のために、Boostライセンスの下に完全なソースがあります。 通常の操作では、モジュールにシリアライザーが必要です。独自のネジを取り付けるか、オレンジを使用できます。