Дизайн парсера протокола двоичной связи для последовательных данных

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

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

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

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

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

Мне интересно услышать ваши идеи. Спасибо.

Prembo.


person Prembo    schedule 19.07.2010    source источник


Ответы (3)


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

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

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 должен был бы иметь сложность определения наличия атрибута).

person Community    schedule 19.07.2010
comment
Привет, Билл, спасибо за этот ответ. Я пытаюсь осмыслить это! Что означает запись ... [Accepts (5)] ...? Заполняется ли словарь отражением во время выполнения? - person Prembo; 20.07.2010
comment
Спасибо за подробное объяснение. Я многому научился! Это очень элегантное и масштабируемое решение. Отлично. я - person Prembo; 27.07.2010

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

person Jeff    schedule 08.03.2013
comment
Спасибо, что поделились - посмотрю. - person Prembo; 08.03.2013
comment
Нет проблем, дайте мне знать, поможет ли это или мне нужно что-то исправить :) - person Jeff; 24.03.2013

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

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

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

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

person Stephen Cleary    schedule 19.07.2010
comment
Спасибо за предложение, Стивен. Это очень простой в реализации подход. - person Prembo; 27.07.2010