tipo print polaca pilas pila notacion listas inversa estructuras estructura ejemplos dinamicas datos colas c# .net tcp

c# - print - ¿Cuál es el patrón correcto para implementar el encuadre TCP? ¿Es una pila de filtros?



pilas estructura de datos ejemplos (1)

Estoy respondiendo mi propia pregunta con la esperanza de que mi solución obtenga una buena crítica y posiblemente ayude a otra persona.

Definí dos interfaces para el filtro de protocolo y el marco de datos. (Para tener claridad sobre la terminología, evité el paquete de palabras para evitar confusiones con los paquetes, tal como se define en los protocolos de nivel inferior).

Aunque no es mi intención, creo que esto podría usarse sobre cualquier protocolo de transporte (es decir, tuberías con nombre, TCP, serie).

Primero está la definición de un marco de datos. Esto consiste en los "datos" (carga útil) y también cualquier octeto que enmarque los datos del transporte como un "mensaje" atómico.

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

Luego está el filtro de protocolo que lee datos de alguna fuente (un socket TCP, por ejemplo o incluso otro filtro si se usan en una pila) y escribe datos nuevamente.

El filtro debe leer los datos (incluido el encuadre) y generar un evento DataReceived para cada lectura de cuadro completo. Se accede a la carga a través de la propiedad "Datos" de la instancia IFramedData.

Cuando los datos se escriben en el filtro, deben "enmarcarlo" adecuadamente y luego elevar el evento DataToSend cada vez que un marco de datos completo esté listo para enviar. (En mi caso, esto sería inmediato, pero traté de permitir un protocolo que tal vez envíe mensajes de una longitud fija o entrada de almacenamientos intermedios por algún otro motivo antes de devolver un cuadro completo listo para enviar.

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

Luego escribí un envoltorio muy simple sobre TcpClient que realiza lecturas asynch, escribe y genera eventos cada vez que el filtro en la parte superior de la pila de protocolos genera el evento DataReceived o el filtro en la parte inferior levanta el evento DataToSend (también escribo los datos el socket pero esto permite que la aplicación controle cuándo se envían realmente los datos que escribió al cliente).

Estoy tratando de implementar una robusta biblioteca TCP que permita a los usuarios seleccionar un protocolo de aplicación o implementar uno propio y simplemente "conectarlos" al cliente / servidor.

Por protocolo me refiero simplemente a la capacidad de definir cómo la secuencia debe enmarcarse en mensajes.

Estoy usando las bibliotecas integradas de asynch TCP para el resto de la pila y he desarrollado un cliente que plantea eventos cada vez que se establece una conexión, se leen o escriben datos o se genera una excepción.

Tengo dos opciones para implementar el protocolo de enmarcado. El primero, que ya está funcionando, es ampliar la clase de cliente y anular el evento de datos recibidos, de modo que esto solo se produzca cuando se haya recibido un mensaje completo. (es decir, debajo del capó guardo los datos sin procesar del zócalo y, según el protocolo, decido cuándo tengo un mensaje completo y luego elevo el evento de datos recibidos). Esto es similar a cómo funciona la biblioteca Nito.Asynch .

El problema con este enfoque es que significa que cada nuevo protocolo requiere una nueva implementación del cliente. Prefiero que el cliente mantenga una pila interna de filtros que se pueden agregar o eliminar.

Cuando los datos se reciben en el socket, se pasan al primer filtro que almacena en búfer hasta que se haya decidido transmitir un mensaje completo con el encabezado o los metadatos eliminados. Esto luego se pasa al siguiente filtro en la pila, etc.

De esta forma, los filtros se pueden definir / desarrollar independientemente de la biblioteca y se pueden inyectar en el cliente en función de la configuración (en tiempo de ejecución).

Para lograr esto, pensé en definir los filtros como pares de implementaciones de System.IO.Stream (entrantes y salientes) que el cliente posee internamente.

Los datos leídos del socket se escribirían en la secuencia entrante inferior en la pila. Los datos leídos de esa secuencia se escribirían en la siguiente secuencia, etc. hasta que la última transmisión (parte superior de la pila) devuelva datos y el cliente los devuelva. (Mi plan era usar la función CopyTo () de Stream).

Los datos escritos en el cliente se escribirían en el flujo saliente superior y se copiarían en la pila hasta que el flujo saliente inferior escriba en el socket subyacente.

Obviamente, hay mucho que considerar y estoy tratando de entender la forma correcta de comportarme como un objeto Stream. Ejemplo: ¿Qué hago cuando alguien llama a Flush () ...?

¿Es esta una buena manera de lograr esto o estoy reinventando la rueda aquí?

La biblioteca Nito.Asynch