Я пересматриваю коммуникационный дизайн синтаксического анализатора протокола для потока байтов (последовательные данные, получил 1 байт за один раз).
Структура пакета (не может быть изменен):
|| Start Delimiter (1 byte) | Message ID (1 byte) | Length (1 byte) | Payload (n bytes) | Checksum (1 byte) ||
В прошлом я реализовал такие системы в процедурном подходе конечного автомата. Когда каждый байт данных прибывает, конечный автомат управляется для наблюдения, где/если входящие совпадения данных в допустимый пакет, байт за один раз, и однажды целый пакет был собран, оператор переключения на основе Идентификатора сообщения, выполняют соответствующий обработчик для сообщения. В некоторых реализациях цикл машины/обработчика сообщений синтаксического анализатора/состояния находится в своем собственном потоке, чтобы не обременить последовательные данные, получил обработчик событий и инициирован семафором, указывающим, что байты были считаны.
Я задаюсь вопросом, существует ли более изящное решение этой типичной проблемы, используя некоторые функции более современного языка дизайна OO и C#. Какие-либо шаблоны разработки, которые решили бы эту проблему? Событийно-ориентированный по сравнению с опрошенным по сравнению с комбинацией?
Мне интересно слышать Ваши идеи.Спасибо.
Prembo.
Прежде всего, я бы отделил парсер пакетов от считывателя потока данных (чтобы я мог писать тесты, не работая с потоком). Затем рассмотрим базовый класс, который предоставляет метод для чтения в пакете и один для записи пакета.
Кроме того, я бы создал словарь (только один раз, а затем повторно использовать его для будущих вызовов) вроде следующего:
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 должен был бы иметь сложность определения наличия атрибута).
Обычно я определяю абстрактный базовый класс сообщений и получаю запечатанные сообщения из этого класса. Затем создайте объект парсера сообщений, который содержит конечный автомат для интерпретации байтов и создания соответствующего объекта сообщения. У объекта парсера сообщений есть только метод (для передачи ему входящих байтов) и, возможно, событие (вызываемое при получении полного сообщения).
Затем у вас есть два варианта обработки фактических сообщений:
as
передает их производным типам. Оба эти варианта полезны в разных сценариях.