6

ユーザーがアプリケーションプロトコルを選択するか、独自のプロトコルを実装して、これらをクライアント/サーバーに「プラグイン」できるようにする堅牢なTCPライブラリを実装しようとしています。

プロトコルとは、ストリームをメッセージにフレーム化する方法を定義する機能を意味します。

スタックの残りの部分に組み込みの非同期TCPライブラリを使用しており、接続が確立されたり、データの読み取りまたは書き込みが行われたり、例外が発生したりするたびにイベントを発生させるクライアントを開発しました。

フレーミングプロトコルを実装するための2つのオプションがあります。1つ目は、すでに機能しており、クライアントクラスを拡張し、データ受信イベントをオーバーライドして、完全なメッセージが受信された場合にのみこれが発生するようにすることです。(つまり、内部でソケットから生データをバッファリングし、プロトコルに基づいて、完全なメッセージを受け取るタイミングを決定し、データ受信イベントを発生させます。)これは、Nito.Asynchライブラリの動作と似ています。

このアプローチの問題は、すべての新しいプロトコルが新しいクライアントの実装を必要とすることを意味することです。クライアントが追加または削除できるフィルターの内部スタックを維持することを望んでいます。

ソケットでデータを受信すると、最初のフィルターに渡されます。このフィルターは、ヘッダーまたはメタデータが削除された完全なメッセージを渡すことを決定するまでバッファリングします。次に、これはスタックなどの次のフィルターに渡されます。

このようにして、フィルターをライブラリーとは独立して定義/開発し、構成に基づいて(実行時に)クライアントに注入することができます。

これを実現するために、クライアントによって内部的に保持されるSystem.IO.Stream(着信と発信)の実装のペアとしてフィルターを定義することを考えました。

ソケットから読み取られたデータは、スタックの一番下の着信ストリームに書き込まれます。そのストリームから読み取られたデータは、最後のストリーム(スタックの最上位)がデータを返すまで次のストリームなどに書き込まれ、その後、これがクライアントによって返されます。(私の計画は、StreamのCopyTo()関数を使用することでした)。

クライアントに書き込まれるデータは、一番上の発信ストリームに書き込まれ、一番下の発信ストリームが基になるソケットに書き込むまでスタックにコピーされます。

明らかに考慮すべきことがたくさんあり、私はStreamオブジェクトとして動作する正しい方法に頭を悩ませようとしています。例:誰かがFlush()を呼び出したときにどうすればよいですか...?

これはこれを達成するための良い方法ですか、それとも私はここで車輪の再発明をしていますか?

Nito.Asynchライブラリ

4

1 に答える 1

1

私の解決策がいくつかの良い批評を受け取り、おそらく他の誰かを助けることを期待して、私は自分の質問に答えています。

プロトコル フィルターとデータ フレーム用に 2 つのインターフェイスを定義しました。(用語を明確にするために、下位レベルのプロトコルで定義されているパケットとの混同を避けるために、パケットという言葉を避けました。)

私自身の意図ではありませんが、これは任意のトランスポート プロトコル (つまり、名前付きパイプ、TCP、シリアル) の上で使用できると思います。

まず、データフレームの定義があります。これは、「データ」(ペイロード) と、アトミックな「メッセージ」として転送されるデータを構成するバイトから構成されます。

/// <summary>
/// A packet of data with some form of meta data which frames the payload for transport in via a stream.
/// </summary>
public interface IFramedData
{
    /// <summary>
    /// Get the data payload from the framed data (excluding any bytes that are used to frame the data)
    /// i.e. The received data minus protocl specific framing
    /// </summary>
    public readonly byte[] Data { get; }

    /// <summary>
    /// Get the framed data (payload including framing bytes) ready to send
    /// </summary>
    /// <returns>Framed data</returns>
    public byte[] ToBytes();
}

次に、何らかのソース (たとえば、TCP ソケット、またはスタックで使用されている場合は別のフィルター) からデータを読み取り、データを書き戻すプロトコルフィルターがあります。

フィルターはデータ (フレーミングを含む) を読み取り、完全なフレーム読み取りごとに DataReceived イベントを発生させる必要があります。ペイロードには、IFramedData インスタンスの「Data」プロパティを介してアクセスします。

データがフィルターに書き込まれると、データを適切に「フレーム化」し、完全なデータ フレームを送信する準備ができるたびに DataToSend イベントを発生させる必要があります。(私の場合、これは即時ですが、おそらく固定長のメッセージを送信するか、送信の準備ができた完全なフレームを返す前に何らかの理由で入力をバッファリングするプロトコルを許可しようとしました.

/// <summary>
/// A protocol filter can be used to read and write data from/to a Stream and frame/deframe the messages.
/// </summary>
/// <typeparam name="TFramedData">The data frame that is handled by this filter</typeparam>
public interface IProtocolFilter<TFramedData> where TFramedData : IFramedData
{
    /// <summary>
    /// Should be raised whenever a complete data frame is ready to send.
    /// </summary>
    /// <remarks>
    /// May be raised after a call to <see cref="FlushSend()"/>
    /// </remarks>
    public event Action<TFramedData> DataToSend;

    /// <summary>
    /// Should be raised whenever a complete data frame has been received.
    /// </summary>
    /// <remarks>
    /// May be raised after a call to <see cref="FlushReceive()"/>
    /// </remarks>
    public event Action<TFramedData> DataReceived;

    /// <summary>
    /// Should be raised if any data written or read breaks the protocol.
    /// This could be due to any asynchronous operation that cannot be raised by the calling function.
    /// </summary>
    /// <remarks>
    /// Behaviour may be protocol specific such as flushing the read or write cache or even resetting the connection.
    /// </remarks>
    public event Action<Exception> ProtocolException;

    /// <summary>
    /// Read data into the recieve buffer
    /// </summary>
    /// <remarks>
    /// This may raise the DataReceived event (possibly more than once if multiple complete frames are read)
    /// </remarks>
    /// <param name="buffer">Data buffer</param>
    /// <param name="offset">Position within the buffer where data must start being read.</param>
    /// <param name="count">Number of bytes to read.</param>
    /// <returns></returns>
    public int Read(byte[] buffer, int offset, int count);

    /// <summary>
    /// Write data to the send buffer.
    /// </summary>
    /// <remarks>
    /// This may raise the DataToSend event (possibly more than once if the protocl requires the data is broken into multiple frames)
    /// </remarks>
    /// <param name="buffer">Data buffer</param>
    /// <param name="offset">Position within the buffer where data must start being read.</param>
    /// <param name="count">Number of bytes to read from the buffer</param>
    public void Write(byte[] buffer, int offset, int count);

    /// <summary>
    /// Flush any data from the receive buffer and if appropriate, raise a DataReceived event.
    /// </summary>
    public void FlushReceive();

    /// <summary>
    /// Flush any data from the send buffer and if appropriate, raise a DataToSend event.
    /// </summary>
    public void FlushSend();
}

次に、プロトコルスタックの上部にあるフィルターが DataReceived イベントを発生させるか、下部にあるフィルターが DataToSend イベントを発生させるたびに、非同期の読み取りと書き込みを行い、イベントを発生させる TcpClient の周りに非常に単純なラッパーを作成しました (データも書き込みますこれにより、アプリケーションは、クライアントに書き込んだデータが実際に送信されるタイミングを監視できます)。

于 2012-07-05T14:28:04.463 に答える