我正在回答我自己的问题,希望我的解决方案能得到一些好的批评,并可能对其他人有所帮助。
我为协议过滤器和数据帧定义了两个接口。(为了明确术语,我避免使用数据包这个词,以避免与较低级别协议中定义的数据包混淆。)
虽然不是我自己的意图,但我想这可以在任何传输协议(即命名管道、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 实例的“数据”属性访问有效负载。
当数据被写入过滤器时,它应该适当地“构建”它,然后在每次准备好发送完整的数据帧时引发 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();
}
然后,我围绕 TcpClient 编写了一个非常简单的包装器,每当协议堆栈顶部的过滤器引发 DataReceived 事件或底部的过滤器引发 DataToSend 事件时,它都会执行异步读取和写入并引发事件(我还将数据写入套接字,但这允许应用程序监视它写入客户端的数据何时实际发送)。