What is the correct pattern for implementing TCP framing? Is this a stack of filters?

I am trying to implement a reliable TCP library that will allow users to select the application protocol or implement their own and simply "connect" them to the client / server.

By protocol, I mean simply the ability to determine how a stream should be enclosed in messages.

I use the built-in asynchronous TCP libraries for the rest of the stack and have developed a client that raises events whenever a connection is established, data is being read or written, or an exception is thrown.

I have two options for implementing the framing protocol. The first one that already works is to extend the client class and override the event received by the data so that it is raised only when a full message is received. (i.e., under the hood, I buffer the raw data from the socket and, based on the protocol, decide when I have the full message, and only then pick up the received data.) This is similar to how the Nito.Asynch Library works.

The problem with this approach is that each new protocol requires a new client implementation. I would prefer the client to support an internal filter stack that can be added or removed.

When data is received on the socket, it is passed to the first filter, which buffers until it decides to send the full message (s) with the deleted header or metadata. Then it is passed to the next filter on the stack, etc. Etc.

Thus, filters can be defined / developed independently of the library and entered into the client based on the configuration (at run time).

To do this, I thought of defining filters as pairs of System.IO.Stream implementations (incoming and outgoing) that are stored inside the client.

Data read from the socket will be written to the lower input stream on the stack. Then, the data read from this stream will be written to the next stream, etc., until the last stream (top of the stack) returns the data, and then will be returned by the client. (My plan was to use the CopyTo () function for Stream).

Data written to the client will be written to the upper outgoing stream and copied onto the stack until the lower outgoing stream is written to the underlying socket.

Obviously, there is a lot to think about, and I'm trying to find the right way to behave like a Stream object. Example: what should I do when someone calls Flush () ...?

Is this a good way to achieve this, or am I reinventing the wheel here again?

Nito.Asynch Library

+6
source share
1 answer

I answer my question in the hope that my decision will receive a good critical analysis and perhaps help someone else.

I defined two interfaces for the protocol filter and the data frame. (To be clear in terminology, I avoided the package of words to avoid package confusion, as defined in lower-level protocols.)

Although this is not my own intention, I assume that it can be used on top of any transport protocol (i.e. Named Pipes, TCP, serial).

First, there is a data frame definition. This consists of β€œdata” (payload), as well as any bytes that form the data for the 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) /// ie 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 a protocol filter that reads data from some source (for example, a TCP socket or even another filter, if used on the stack) and writes data back.

The filter should read data (including cropping) and raise a DataReceived event for each full frame read. The payload is accessed through the Data property of the IFramedData instance.

When data is written to the filter, it must "crop" it accordingly, and then raise the DataToSend event each time a full frame of data is ready to be sent. (In my case, this would be immediate, but I tried to allow a protocol that might send messages of a fixed length or input buffers for some other reason before returning a full frame ready to be sent.

 /// <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(); } 

Then I wrote a very simple wrapper around TcpClient that does asynchronous reads and writes and triggers events whenever the filter at the top of the protocol stack raises the DataReceived event or the filter below raises the DataToSend event (I also write data to the socket, but this allows the application to track when the data that he wrote to the client is indeed sent).

+1
source

Source: https://habr.com/ru/post/919579/


All Articles