I am trying to implement a robust TCP library which will allow users to select an Application Protocol or implement their own and simply "plug" these into the client/server.
By protocol I mean simply the ability to define how the stream should be framed into messages.
I am using the built-in asynch TCP libraries for the rest of the stack and have developed a client that raises events whenever a connection is established, data is read or written or an exception is raised.
I have two options for implementing the framing protocol. The first, which is already working, is to extend the client class and override the data received event so that this is only raised when a full message has been received. (i.e. Under the hood I buffer the raw data from the socket and based on the protocol decide when I have a full message and only then raise the data received event.) This is similar to how the Nito.Asynch library works.
The problem with this approach is that it means every new protocol requires a new client implementation. I'd prefer for the client to maintain an internal stack of filters that can be added or removed.
When data is received on the socket it is passed to the first filter which buffers until it has decided to pass on a complete message(s) with the header or meta data removed. This is then passed to the next filter in the stack etc etc.
This way filters can be defined/developed independently of the library and injected into the client based on configuration (at runtime).
To achieve this I thought about defining the filters as pairs of implementations of System.IO.Stream (incoming and outgoing) which are held internally by the client.
Data read from the socket would be written to the bottom incoming stream on the stack. Data read from that stream would then be written to the next stream etc until the last stream (top of the stack) returns data and this is then returned by the client. (My plan was to use the CopyTo() function of Stream).
Data written to the client would be written to the top outgoing Stream and copied down the stack until the bottom outgoing Stream writes to the underlying socket.
Obviously there is a lot to consider and I am trying to get my head around the correct way to behave as a Stream object. Example: What do I do when someone calls Flush()...?
Is this a good way of achieving this or am I re-inventing the wheel here?
The Nito.Asynch library
I am answering my own question in the hope that my solution will get some good critique and possibly help someone else.
I defined two interfaces for the protocol filter and data frame. (To be clear on terminology I avoided the word packet to avoid confusion with packets as defined in lower level protocols.)
Although not my own intention I guess this could be used on top of any transport protocol (i.e. Named pipes, TCP, serial).
First there is the definition of a data frame. This consists of the "Data" (payload) and also any bytes that frame the data fro transport as an atomic "message".
/// <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();
}
Then there is the protocol filter which reads data from some source (a TCP socket for example or even another filter if they are used in a stack) and writes data back.
The filter should read in data (including the framing) and raise a DataReceived event for each complete frame read. The payload is accessed via the "Data" property of the IFramedData instance.
When data is written to the filter it should "frame" it appropriately and then raise the DataToSend event each time a complete data frame is ready to send. (In my case this would be immediate but I tried to allow for a protocol that perhaps sends messages of a fixed length or buffers input for some other reason before returning a complete frame ready to send.
/// <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();
}
I have then written a very simple wrapper around TcpClient which does asynch reads and writes and raises events whenever the filter at the top of the protocol stack raises the DataReceived event or the filter at the bottom raises the DataToSend event (I also write the data to the socket but this allows the application to monitor when the data it wrote to the client is actually sent).
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With