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

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

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

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

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

Я задаюсь вопросом, существует ли более изящное решение этой типичной проблемы, используя некоторые функции более современного языка дизайна OO и C#. Какие-либо шаблоны разработки, которые решили бы эту проблему? Событийно-ориентированный по сравнению с опрошенным по сравнению с комбинацией?

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

Prembo.

7
задан dsolimano 17 December 2013 в 14:38
поделиться

2 ответа

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

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

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 может быть назначен объекту типа Func .)

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

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

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

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

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

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

Четвертый:

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

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

5
ответ дан 7 December 2019 в 03:10
поделиться

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

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

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

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

1
ответ дан 7 December 2019 в 03:10
поделиться
Другие вопросы по тегам:

Похожие вопросы: