2010-07-19 11 views
6

Sto rivisitando un progetto di parser di protocollo di comunicazione per un flusso di byte (dati seriali, ricevuti 1 byte alla volta).Progettazione del parser per protocollo di comunicazione binaria per dati seriali

La struttura del pacchetto (non modificabile) è:

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

In passato ho implementato tali sistemi in un approccio stato macchina procedurale. All'arrivo di ogni byte di dati, la macchina di stato viene pilotata per vedere dove/se i dati in arrivo si adattano a un pacchetto valido un byte alla volta, e una volta che un intero pacchetto è stato assemblato, un'istruzione switch basata sull'ID messaggio esegue gestore appropriato per il messaggio. In alcune implementazioni, il loop di parser/macchina di stato/gestore di messaggi si trova nella sua stessa thread in modo da non gravare sul gestore di eventi di dati seriali ricevuti e viene attivato da un semaforo che indica che i byte sono stati letti.

Mi chiedo se esiste una soluzione più elegante a questo problema comune, sfruttando alcune delle più moderne funzionalità linguistiche di C# e design OO. Qualche modello di progettazione che risolverebbe questo problema? Combinazione event-driven vs polled vs?

Sono interessato a sentire le tue idee. Grazie.

Prembo.

risposta

4

Prima di tutto separerei il parser di pacchetti dal lettore di flussi di dati (in modo che potessi scrivere test senza occuparmi dello stream). Quindi considera una classe base che fornisce un metodo per leggere in un pacchetto e uno per scrivere un pacchetto.

Inoltre vorrei costruire un dizionario (una volta solo allora riutilizzarlo per le chiamate future) come la seguente:

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

Edit: Alcune spiegazioni di ciò che sta succedendo qui:

primo:

[Accepts(5)] 

questa linea è un attributo di C# (definito da AcceptsAttribute) Dice il classe FooMessage accetta il messaggio id 5.

Secondo:

Sì, il dizionario è in costruzione in fase di esecuzione attraverso la riflessione. Devi solo farlo una volta (lo metterei in una classe singleton che puoi mettere su un caso di test che può essere eseguito per assicurare che il dizionario sia compilato correttamente).

Terzo:

var m = messages[5](); 

Questa linea ottiene la seguente espressione lambda compilata dal dizionario e lo esegue:

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

(Il cast è necessario .NET 3.5, ma non in 4.0 causa alle modifiche covarianti nel modo in cui funzionano i delagati, in 4.0 un oggetto di tipo Func<FooMessage> può essere assegnato a un oggetto del tipo Func<Message>.)

espressione Questo lambda è costruito dalla linea Assegnazione valore durante la creazione del dizionario:

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

(Il cast qui è necessario per lanciare l'espressione lambda compilato Func<Message>.)

ho fatto in questo modo perché mi capita per avere già il tipo disponibile per me in quel punto. Si potrebbe anche usare:

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

ma credo che sarebbe stato più lento (e il cast qui è necessario cambiare Func<object> in Func<Message>).

Quarto:

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

Ciò è stato fatto perché ho sentito che si potrebbe avere un valore nel porre più AcceptsAttribute di una volta su una classe (ad accettare più di un ID messaggio per classe). Questo ha anche il piacevole effetto collaterale di ignorare le classi di messaggi che non hanno un attributo id del messaggio (altrimenti il ​​metodo Where dovrebbe avere la complessità di determinare se l'attributo è presente).

+0

Ciao Bill, grazie per la risposta. Sto provando a capirlo! Cosa fa ... [Accetta (5)] ... notazione significa? Il dizionario viene popolato da reflection in fase di esecuzione? – Prembo

+0

Grazie per la spiegazione dettagliata. Ho imparato molto! È una soluzione molto elegante e scalabile. Eccellente. I – Prembo

1

Quello che faccio in genere è definire una classe di messaggio di base astratta e derivare messaggi sigillati da quella classe. Quindi disporre di un oggetto parser di messaggi che contiene la macchina di stato per interpretare i byte e creare un oggetto messaggio appropriato. L'oggetto parser del messaggio ha solo un metodo (per passarlo i byte in arrivo) e opzionalmente un evento (richiamato quando è arrivato un messaggio completo).

allora avete due opzioni per la gestione dei messaggi attuali:

  • Definire un metodo astratto sulla classe di messaggio di base, ignorando che in ciascuna delle classi di segnalazione derivate. Chiedi al parser del messaggio di richiamare questo metodo dopo che il messaggio è arrivato completamente.
  • La seconda opzione è meno orientata agli oggetti, ma potrebbe essere più semplice lavorare con: lasciare le classi messaggio come solo dati. Quando il messaggio è completo, inviarlo tramite un evento che prende come parametro la classe di messaggio base astratta. Invece di un'istruzione switch, il gestore di solito è as e li trasmette ai tipi derivati.

Entrambe queste opzioni sono utili in diversi scenari.

+0

Grazie per il tuo suggerimento Stephen. È un approccio molto facile da implementare. – Prembo

2

Sono un po 'in ritardo per la festa ma ho scritto un quadro che penso possa fare questo. Senza saperne di più sul tuo protocollo, è difficile per me scrivere il modello dell'oggetto, ma penserei che non sarebbe troppo difficile. Dai un'occhiata a binaryserializer.com.

+0

Grazie per la condivisione - darò un'occhiata. – Prembo

+0

Nessun problema, fammi sapere se è d'aiuto o se devo aggiustare qualcosa :) – Jeff

Problemi correlati