2010-07-19 10 views
6

Estoy revisando un diseño del analizador del protocolo de comunicaciones para una secuencia de bytes (datos en serie, recibidos de 1 byte a la vez).Diseño del analizador del protocolo de comunicaciones binarias para datos en serie

La estructura de paquete (no puede ser cambiado) es:

|| Start Delimiter (1 byte) | Message ID (1 byte) | Length (1 byte) | Payload (n bytes) | Checksum (1 byte) || 

En el pasado he implementado tales sistemas en un enfoque del estado de la máquina de procedimiento. Cuando llega cada byte de datos, la máquina de estados se mueve para ver dónde/si los datos entrantes encajan en un paquete válido, un byte a la vez, y una vez que se ha ensamblado un paquete completo, una instrucción switch basada en el ID del mensaje ejecuta el manejador apropiado para el mensaje. En algunas implementaciones, el analizador sintáctico/máquina de estado/controlador de mensajes se asienta en su propio hilo para no sobrecargar el controlador de eventos de datos recibidos en serie, y es activado por un semáforo que indica que se han leído los bytes.

Me pregunto si hay una solución más elegante a este problema común, explotando algunas de las características del lenguaje más moderno del diseño C# y OO. ¿Algún patrón de diseño que resolvería este problema? ¿Evento dirigido vs encuestado vs combinación?

Me interesa conocer sus ideas. Gracias.

Prembo.

Respuesta

4

Antes que nada, separaría el analizador de paquetes del lector de flujo de datos (para poder escribir pruebas sin tener que lidiar con la transmisión). Luego considere una clase base que proporcione un método para leer en un paquete y otro para escribir un paquete.

Además me gustaría construir un diccionario (una sola vez y luego volver a utilizarlo para futuras llamadas) como el siguiente:

class Program { 
    static void Main(string[] args) { 
     var assembly = Assembly.GetExecutingAssembly(); 
     IDictionary<byte, Func<Message>> messages = assembly 
      .GetTypes() 
      .Where(t => typeof(Message).IsAssignableFrom(t) && !t.IsAbstract) 
      .Select(t => new { 
       Keys = t.GetCustomAttributes(typeof(AcceptsAttribute), true) 
         .Cast<AcceptsAttribute>().Select(attr => attr.MessageId), 
       Value = (Func<Message>)Expression.Lambda(
         Expression.Convert(Expression.New(t), typeof(Message))) 
         .Compile() 
      }) 
      .SelectMany(o => o.Keys.Select(key => new { Key = key, o.Value })) 
      .ToDictionary(o => o.Key, v => v.Value); 
      //will give you a runtime error when created if more 
      //than one class accepts the same message id, <= useful test case? 
     var m = messages[5](); // consider a TryGetValue here instead 
     m.Accept(new Packet()); 
     Console.ReadKey(); 
    } 
} 

[Accepts(5)] 
public class FooMessage : Message { 
    public override void Accept(Packet packet) { 
     Console.WriteLine("here"); 
    } 
} 

//turned off for the moment by not accepting any message ids 
public class BarMessage : Message { 
    public override void Accept(Packet packet) { 
     Console.WriteLine("here2"); 
    } 
} 

public class Packet {} 

public class AcceptsAttribute : Attribute { 
    public AcceptsAttribute(byte messageId) { MessageId = messageId; } 

    public byte MessageId { get; private set; } 
} 

public abstract class Message { 
    public abstract void Accept(Packet packet); 
    public virtual Packet Create() { return new Packet(); } 
} 

Editar: Algunas explicaciones de lo que está pasando aquí:

primero:

[Accepts(5)] 

Esta línea es un atributo de C# (definido por AcceptsAttribute) Dice el FooMessage la clase acepta el mensaje de identificación de 5.

Segundo:

Si el diccionario está siendo construida en tiempo de ejecución a través de la reflexión. Solo necesita hacer esto una vez (lo pondría en una clase única que puede poner un caso de prueba que se puede ejecutar para asegurarse de que el diccionario se construye correctamente).

Tercero:

var m = messages[5](); 

Esta línea se hace la siguiente expresión lambda compilado rendimiento del diccionario y lo ejecuta:

()=>(Message)new FooMessage(); 

(El reparto es necesaria en .NET 3.5, pero no en 4.0 debido a los cambios covariantes en cómo funcionan las demoras, en 4.0 se puede asignar un objeto del tipo Func<FooMessage> a un objeto del tipo Func<Message>.)

expresión Este lambda es construido por la línea de asignación de valor durante la creación del diccionario:

Value = (Func<Message>)Expression.Lambda(Expression.Convert(Expression.New(t), typeof(Message))).Compile() 

(El reparto aquí es necesario convertir la expresión lambda compilado a Func<Message>.)

lo hice de esta manera porque da la casualidad ya tengo el tipo disponible para mí en ese punto. También es posible usar:

Value =()=>(Message)Activator.CreateInstance(t) 

Pero creo que sería más lento (y el reparto aquí es necesario cambiar Func<object> en Func<Message>).

Cuarto:

.SelectMany(o => o.Keys.Select(key => new { Key = key, o.Value })) 

Esto se hizo porque sentía que podría tener valor en la colocación de la AcceptsAttribute más de una vez en una clase (a aceptar más de un mensaje de identificación por clase). Esto también tiene el agradable efecto secundario de ignorar las clases de mensajes que no tienen un atributo id de mensaje (de lo contrario, el método Where debería tener la complejidad de determinar si el atributo está presente).

+0

Hola Bill, gracias por esa respuesta. ¡Estoy tratando de entenderlo! ¿Qué significa ... [Se acepta (5)] ... notación significa? ¿Está el diccionario poblado por reflexión en tiempo de ejecución? – Prembo

+0

Gracias por esa explicación detallada. ¡Aprendí mucho! Es una solución muy elegante y escalable. Excelente. I – Prembo

1

Lo que generalmente hago es definir una clase de mensaje base abstracta y derivar mensajes sellados de esa clase. Luego, tenga un objeto analizador de mensaje que contenga la máquina de estado para interpretar los bytes y crear un objeto de mensaje apropiado. El objeto del analizador de mensajes solo tiene un método (para pasarle los bytes entrantes) y opcionalmente un evento (invocado cuando ha llegado un mensaje completo).

A continuación, tiene dos opciones para manejar los mensajes reales:

  • definir un método abstracto en la clase de base de mensaje, ignorando que en cada una de las clases de mensajes derivados. Haga que el analizador de mensajes invoque este método una vez que el mensaje haya llegado por completo.
  • La segunda opción está menos orientada a objetos, pero puede ser más fácil trabajar con ella: deje las clases de mensajes como solo datos. Cuando el mensaje esté completo, envíelo a través de un evento que tome la clase de mensaje base abstracta como su parámetro. En lugar de una instrucción de conmutación, el controlador generalmente as los envía a los tipos derivados.

Ambas opciones son útiles en diferentes escenarios.

+0

Gracias por su sugerencia Stephen. Es un enfoque muy fácil de implementar. – Prembo

2

Llego un poco tarde a la fiesta pero escribí un marco que creo que podría hacer esto. Sin saber más acerca de su protocolo, es difícil para mí escribir el modelo de objetos, pero creo que no sería demasiado difícil. Eche un vistazo al binaryserializer.com.

+0

Gracias por compartir - Voy a echar un vistazo. – Prembo

+0

No hay problema, avíseme si ayuda o si necesito arreglar algo :) – Jeff

Cuestiones relacionadas