2010-07-19 10 views
6

Ich besuche ein Kommunikationsprotokoll Parser Design für einen Strom von Bytes (serielle Daten, erhalten 1 Byte zu einer Zeit).Binär-Kommunikationsprotokoll-Parser-Design für serielle Daten

Die Paketstruktur (kann nicht geändert werden) ist:

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

In der Vergangenheit habe ich solche Systeme in einer prozeduralen Zustandsmaschine Ansatz umgesetzt haben. Wenn jedes Datenbyte eintrifft, wird die Zustandsmaschine angesteuert, um zu sehen, wo/wenn die eingehenden Daten Byte für Byte in ein gültiges Paket passen, und nachdem ein ganzes Paket zusammengestellt wurde, führt eine auf der Nachrichten-ID basierende Umschaltanweisung die geeigneter Handler für die Nachricht. In einigen Implementierungen sitzt die Parser/Zustandsmaschine/Nachrichtenbehandlungsschleife in ihrem eigenen Thread, um den Ereignishandler für empfangene serielle Daten nicht zu belasten, und wird durch einen Semaphor ausgelöst, der anzeigt, dass Bytes gelesen wurden.

Ich frage mich, ob es eine elegantere Lösung für dieses allgemeine Problem gibt, die einige der moderneren Sprachfunktionen von C# und OO Design ausnutzt. Irgendwelche Entwurfsmuster, die dieses Problem lösen würden? Ereignisgesteuert vs vs vs Kombination?

Ich bin daran interessiert, Ihre Ideen zu hören. Vielen Dank.

Prembo.

Antwort

4

Zuerst würde ich den Paket-Parser vom Datenstromleser trennen (damit ich Tests schreiben konnte, ohne mit dem Strom zu handeln). Betrachten Sie dann eine Basisklasse, die eine Methode zum Einlesen eines Pakets und eine zum Schreiben eines Pakets bereitstellt.

Zusätzlich würde ich ein Wörterbuch bauen (einmal nur dann für zukünftige Anrufe wiederverwendet werden) wie folgt aus:

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: Einige Erklärungen, was hier vor sich geht:

Erstens:

[Accepts(5)] 

Diese Linie ist ein C# Attribut (definiert durch AcceptsAttribute) Sagt, die die FooMessage Klasse die Message-ID 5.

Zweite akzeptiert:

Ja das Wörterbuch wird gebaut zur Laufzeit über Reflexion. Sie müssen dies nur einmal tun (ich würde es in eine Singleton-Klasse einfügen, in die Sie einen Testfall einfügen können, der ausgeführt werden kann, um sicherzustellen, dass das Wörterbuch korrekt erstellt wird).

Drittens:

var m = messages[5](); 

Diese Zeile wird die kompilierte folgenden Lambda-Ausdruck des Wörterbuchs aus und führt es aus:

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

(Die Besetzung ist notwendig in .NET 3.5, aber nicht in 4.0 durch zu den kovarianten Änderungen, wie Absetzarbeiten funktionieren, kann in 4.0 ein Objekt vom Typ Func<FooMessage> einem Objekt des Typs Func<Message> zugeordnet werden.)

Dieses Lambda-Ausdruck wird durch die Wertzuweisung Leitung während Wörterbucherstellung gebaut:

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

(Die Besetzung ist hier notwendig, um den kompilierten Lambda-Ausdruck zu Func<Message> werfen.)

ich, dass diese Art und Weise getan, weil ich zufällig den Typ, der mir zu diesem Zeitpunkt zur Verfügung steht. Sie können auch verwenden:

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

Aber ich glaube, dass langsamer sein würde (und die Besetzung ist hier notwendig Func<object> in Func<Message> zu ändern).

Vierter:

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

Dies geschieht, weil ich das Gefühl, dass Sie Wert bei der Platzierung des AcceptsAttribute mehr als einmal auf einer Klasse haben könnten (mehr als eine Nachrichten-ID pro Klasse zu akzeptieren). Dies hat auch den positiven Nebeneffekt, dass Nachrichtenklassen ignoriert werden, die kein Message-ID-Attribut haben (andernfalls müsste die Where-Methode die Komplexität haben, zu bestimmen, ob das Attribut vorhanden ist).

+0

Hallo Bill, Danke für diese Antwort. Ich versuche, mich darum zu kümmern! Was bedeutet ... [Akzeptiert (5)] ... Notation bedeuten? Wird das Wörterbuch zur Laufzeit mit Reflektion aufgefüllt? – Prembo

+0

Danke für diese detaillierte Erklärung. Ich habe viel gelernt! Es ist eine sehr elegante und skalierbare Lösung. Ausgezeichnet. I – Prembo

1

Was ich im Allgemeinen tue, ist eine abstrakte Basis-Nachrichtenklasse zu definieren und versiegelte Nachrichten von dieser Klasse abzuleiten. Verfügen Sie dann über ein Nachrichtenparser-Objekt, das die Statusmaschine enthält, um die Bytes zu interpretieren und ein entsprechendes Nachrichtenobjekt zu erstellen. Das Nachrichtenparser-Objekt hat nur eine Methode (um die eingehenden Bytes zu übergeben) und optional ein Ereignis (wird aufgerufen, wenn eine vollständige Nachricht angekommen ist).

Sie haben dann zwei Möglichkeiten für die aktuellen Meldungen Handhabung:

  • definieren eine abstrakte Methode auf der Basis Nachrichtenklasse, es in jedem der abgeleiteten Nachrichtenklassen überschrieben. Lassen Sie den Message-Parser diese Methode aufrufen, nachdem die Nachricht vollständig angekommen ist.
  • Die zweite Option ist weniger objektorientiert, aber möglicherweise einfacher zu arbeiten: Lassen Sie die Nachrichtenklassen als nur Daten. Wenn die Nachricht vollständig ist, senden Sie sie über ein Ereignis, das die abstrakte Basisnachrichtenklasse als Parameter verwendet. Anstelle einer switch-Anweisung sendet der Handler normalerweise as - sendet sie an die abgeleiteten Typen.

Diese beiden Optionen sind in verschiedenen Szenarien nützlich.

+0

Vielen Dank für Ihren Vorschlag Stephen. Es ist ein sehr einfach zu implementierender Ansatz. – Prembo

2

Ich bin ein wenig spät, um die Partei, aber ich schrieb dies einen Rahmen, was ich denken konnte tun. Ohne mehr über Ihr Protokoll zu wissen, fällt es mir schwer, das Objektmodell zu schreiben, aber ich denke, es wäre nicht zu schwer. Werfen Sie einen Blick auf binaryserializer.com.

+0

Danke für den Austausch - Ich werde es mir ansehen. – Prembo

+0

Kein Problem, lass es mich wissen, wenn es hilft oder wenn ich etwas reparieren muss :) – Jeff

Verwandte Themen