6


4

シリアルデータ用のバイナリ通信プロトコルパーサーの設計

バイトストリーム(シリアルデータ、一度に1バイトを受信)の通信プロトコルパーサーの設計を再検討しています。

パケット構造(変更不可)は次のとおりです。

|| Start Delimiter (1 byte) | Message ID (1 byte) | Length (1 byte) | Payload (n bytes) | Checksum (1 byte) ||

過去に、私はそのようなシステムを手続き型ステートマシンアプローチで実装しました。 データの各バイトが到着すると、ステートマシンが駆動され、受信データがバイト単位で有効なパケットに収まるかどうかが確認され、パケット全体が組み立てられると、メッセージIDに基づいたswitchステートメントが実行されますメッセージの適切なハンドラ。 一部の実装では、パーサー/ステートマシン/メッセージハンドラーループは、シリアルデータ受信イベントハンドラーに負担をかけないように独自のスレッドに置かれ、バイトが読み取られたことを示すセマフォによってトリガーされます。

この一般的な問題に対してより洗練された解決策があり、C#およびOO設計のより現代的な言語機能のいくつかを活用できるかどうか疑問に思っています。 この問題を解決する設計パターンはありますか? イベント駆動型vsポーリング型vs組み合わせ?

あなたのアイデアを聞いてみたいです。 ありがとう。

プレンボ。

3 Answer


4


まず、パケットパーサーをデータストリームリーダーから分離します(ストリームを処理せずにテストを記述できるようにするため)。 次に、パケットを読み込むメソッドとパケットを書き込むメソッドを提供する基本クラスを検討します。

さらに、次のような辞書を作成します(一度だけ、その後の呼び出しで再利用します)。

class Program {
    static void Main(string[] args) {
        var assembly = Assembly.GetExecutingAssembly();
        IDictionary> messages = assembly
            .GetTypes()
            .Where(t => typeof(Message).IsAssignableFrom(t) && !t.IsAbstract)
            .Select(t => new {
                Keys = t.GetCustomAttributes(typeof(AcceptsAttribute), true)
                       .Cast().Select(attr => attr.MessageId),
                Value = (Func)Expression.Lambda(
                        Expression.Convert(Expression.New(t), typeof(Message)))
                        .Compile()
            })
            .SelectMany(o => o.Keys.Select(key => new { Key = key, o.Value }))
            .ToDictionary(o => o.Key, v => v.Value);
            //will give you a runtime error when created if more
            //than one class accepts the same message id, <= useful test case?
        var m = messages[5](); // consider a TryGetValue here instead
        m.Accept(new Packet());
        Console.ReadKey();
    }
}

[Accepts(5)]
public class FooMessage : Message {
    public override void Accept(Packet packet) {
        Console.WriteLine("here");
    }
}

//turned off for the moment by not accepting any message ids
public class BarMessage : Message {
    public override void Accept(Packet packet) {
        Console.WriteLine("here2");
    }
}

public class Packet {}

public class AcceptsAttribute : Attribute {
    public AcceptsAttribute(byte messageId) { MessageId = messageId; }

    public byte MessageId { get; private set; }
}

public abstract class Message {
    public abstract void Accept(Packet packet);
    public virtual Packet Create() { return new Packet(); }
}

'' '' '

編集:ここで何が起こっているかの説明:

最初:

[Accepts(5)]

この行はC#属性( `AcceptsAttribute`で定義)です。`FooMessage`クラスはメッセージID 5を受け入れます。

第二:

はい、辞書は実行時にリフレクションを介して構築されています。 これは一度だけ行う必要があります(辞書を正しく作成するために実行できるテストケースを配置できるシングルトンクラスに配置します)。

三番:

var m = messages[5]();

この行は、辞書から次のコンパイル済みラムダ式を取得して実行します。

()=>(Message)new FooMessage();

(キャストは.NET 3.5で必要ですが、遅延ではどのように機能するかについての共変的な変更のため4.0では不要です。4.0では、タイプ Func`のオブジェクトをタイプ Func`のオブジェクトに割り当てることができます。)

このラムダ式は、ディクショナリの作成中に値割り当て行によって構築されます。

Value = (Func)Expression.Lambda(Expression.Convert(Expression.New(t), typeof(Message))).Compile()

(ここでのキャストは、コンパイルされたラムダ式を `Func`にキャストするために必要です。)

その時点で既に利用可能なタイプをすでに持っているため、私はこのようにしました。 あなたも使用することができます:

Value = ()=>(Message)Activator.CreateInstance(t)

しかし、それはもっと遅いと思います(そして、ここでのキャストは、 Func`を Func`に変更するために必要です)。

第4:

.SelectMany(o => o.Keys.Select(key => new { Key = key, o.Value }))

これは、クラスに(AcceptsAttribute)を複数回配置する(クラスごとに複数のメッセージIDを受け入れる)ことに価値があると感じたためです。 これには、メッセージID属性を持たないメッセージクラスを無視するという良い副作用もあります(そうでない場合、Whereメソッドは属性が存在するかどうかを判断する複雑さが必要になります)。


2


私はパーティーに少し遅れましたが、私はこれができると思うフレームワークを書きました。 あなたのプロトコルについてもっと知らなければ、オブジェクトモデルを書くことは私にとって難しいですが、私はそれはそれほど難しくないと思います。 http://binaryserializer.com [binaryserializer.com]をご覧ください。


1


私が通常行うことは、抽象基本メッセージクラスを定義し、そのクラスから封印されたメッセージを派生させることです。 次に、ステートマシンを含むメッセージパーサーオブジェクトを使用して、バイトを解釈し、適切なメッセージオブジェクトを作成します。 メッセージパーサーオブジェクトには、メソッド(着信バイトを渡す)と、オプションでイベント(完全なメッセージが到着したときに呼び出される)があります。

次に、実際のメッセージを処理するための2つのオプションがあります。

  • 基本メッセージクラスで抽象メソッドを定義し、それをオーバーライドします 派生メッセージクラスのそれぞれ。 メッセージが完全に到着した後に、メッセージパーサーにこのメソッドを呼び出させます。

  • 2番目のオプションはオブジェクト指向ではありませんが、作業しやすいかもしれません with:メッセージクラスを単なるデータとして残します。 メッセージが完成したら、抽象基本メッセージクラスをパラメーターとして受け取るイベントを介してメッセージを送信します。 switchステートメントの代わりに、ハンドラーは通常、それらを派生型に「as」キャストします。

これらのオプションは両方とも、さまざまなシナリオで役立ちます。