Разработка синтаксического анализатора двоичных протоколов связи для последовательных данных - PullRequest
6 голосов
/ 19 июля 2010

Я пересматриваю дизайн синтаксического анализатора протокола связи для потока байтов (последовательные данные, полученные по 1 байту за раз).

Структура пакета (не может быть изменена):

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

В прошлом я реализовывал такие системы в методическом подходе конечного автомата. По мере поступления каждого байта данных конечный автомат управляется, чтобы увидеть, где / если входящие данные вписываются в действительный пакет по байт за раз, и как только весь пакет собран, оператор switch на основе идентификатора сообщения выполняет соответствующий обработчик для сообщения. В некоторых реализациях цикл синтаксического анализатора / конечного автомата / обработчика сообщений находится в своем собственном потоке, чтобы не обременять обработчик события полученных последовательных данных, и запускается семафором, указывающим, что байты были прочитаны.

Мне интересно, есть ли более изящное решение этой распространенной проблемы, использующее некоторые из более современных языковых возможностей C # и OO design. Какие-либо шаблоны проектирования, которые решат эту проблему? Управляемый событиями против опрошенного против комбинации?

Мне интересно услышать ваши идеи. Благодаря.

Prembo.

Ответы [ 3 ]

4 голосов
/ 19 июля 2010

Прежде всего, я бы отделил анализатор пакетов от считывателя потока данных (чтобы я мог писать тесты, не имея дело с потоком).Затем рассмотрим базовый класс, который предоставляет метод для чтения в пакете и один для записи пакета.

Кроме того, я бы создал словарь (только один раз, а затем повторно использовал его для будущих вызовов), как показано ниже:

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

Редактировать: Некоторые объяснения того, что здесь происходит:

Первый:

[Accepts(5)]

Эта строка является атрибутом C # (определяется AcceptsAttribute) говорит, что класс FooMessage принимает идентификатор сообщения, равный 5.

Секунда:

Да, словарь создается во время выполнения с помощью отражения.Вам нужно сделать это только один раз (я бы поместил его в одноэлементный класс, в который можно поместить тестовый пример, который можно запустить, чтобы убедиться в правильности построения словаря).

Третий:

var m = messages[5]();

Эта строка получает следующее скомпилированное лямбда-выражение из словаря и выполняет его:

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

(приведение необходимо в .NET 3.5, но не в 4.0 из-за ковариантных изменений в том, какДелагат работает, в 4.0 объект типа Func<FooMessage> может быть назначен объекту типа Func<Message>.)

Это лямбда-выражение строится линией присваивания значения во время создания словаря:

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

(приведение здесь необходимо для приведения скомпилированного лямбда-выражения к Func<Message>.)

Я сделал это таким образом, потому что у меня уже есть тип, доступный мне на тот момент.Вы также можете использовать:

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

Но я считаю, что это будет медленнее (и приведение здесь необходимо изменить Func<object> на Func<Message>).

Четвертый:

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

Это было сделано, потому что я чувствовал, что вы могли бы иметь значение при размещении AcceptsAttribute более одного раза в классе (чтобы принять более одного идентификатора сообщения на класс).Это также имеет приятный побочный эффект игнорирования классов сообщений, которые не имеют атрибута идентификатора сообщения (в противном случае метод Where должен был бы иметь сложность определения наличия атрибута).

2 голосов
/ 08 марта 2013

Я немного опоздал на вечеринку, но я написал основу, которая, я думаю, могла бы сделать это.Не зная больше о вашем протоколе, мне сложно написать объектную модель, но я думаю, что это не будет слишком сложно.Взгляните на binaryserializer.com .

1 голос
/ 19 июля 2010

Что я обычно делаю, это определяю абстрактный базовый класс сообщений и получаю запечатанные сообщения из этого класса.Затем создайте объект анализатора сообщений, который содержит конечный автомат для интерпретации байтов и построения соответствующего объекта сообщения.Объект парсера сообщений просто имеет метод (для передачи ему входящих байтов) и, необязательно, событие (вызывается при получении полного сообщения).

Затем у вас есть две опции для обработки фактических сообщений:

  • Определите абстрактный метод для базового класса сообщений, переопределив его в каждом из производных классов сообщений.Попросите анализатор сообщений вызывать этот метод после того, как сообщение полностью получено.
  • Второй вариант менее объектно-ориентирован, но с ним может быть проще работать: оставить классы сообщений как данные.Когда сообщение завершено, отправьте его через событие, которое принимает абстрактный базовый класс сообщения в качестве параметра.Вместо оператора switch обработчик обычно as передает их в производные типы.

Обе эти опции полезны в различных сценариях.

...