问题描述
我正在重新设计一个字节流(串行数据,一次接收 1 个字节)的通信协议解析器设计.
I'm revisiting a communications protocol parser design for a stream of bytes (serial data, received 1 byte at a time).
数据包结构(不可更改)为:
The packet structure (can't be changed) is:
|| Start Delimiter (1 byte) | Message ID (1 byte) | Length (1 byte) | Payload (n bytes) | Checksum (1 byte) ||
过去,我以过程状态机方法实现了此类系统.当每个字节的数据到达时,状态机被驱动来查看输入数据在哪里/是否适合一次一个字节的有效数据包,一旦整个数据包组装完毕,基于消息 ID 的 switch 语句将执行消息的适当处理程序.在某些实现中,解析器/状态机/消息处理程序循环位于其自己的线程中,以免对串行数据接收事件处理程序造成负担,并由指示字节已读取的信号量触发.
In the past I have implemented such systems in a procedural state-machine approach. As each byte of data arrives, the state machine is driven to see where/if the incoming data fits into a valid packet a byte at a time, and once a whole packet has been assembled, a switch statement based on the Message ID executes the appropriate handler for the message. In some implementations, the parser/state machine/message handler loop sits in its own thread so as not to burden the serial data received event handler, and is triggered by a semaphore indicating bytes have been read.
我想知道这个常见问题是否有更优雅的解决方案,利用 C# 和 OO 设计的一些更现代的语言特性.有什么设计模式可以解决这个问题吗?事件驱动 vs 轮询 vs 组合?
I'm wondering if there is a more elegant solution to this common problem, exploiting some of the more modern language features of C# and OO design. Any design patterns that would solve this problem? Event-driven vs polled vs combination?
我很想听听你的想法.谢谢.
I'm interested to hear your ideas. Thanks.
普雷姆博.
推荐答案
首先我将数据包解析器与数据流读取器分开(这样我就可以在不处理流的情况下编写测试).然后考虑一个基类,它提供了一个读取数据包的方法和一个写入数据包的方法.
First of all I would separate the packet parser from the data stream reader (so that I could write tests without dealing with the stream). Then consider a base class which provides a method to read in a packet and one to write a packet.
另外,我会构建一个字典(只有一次,然后在以后的调用中重复使用它),如下所示:
Additionally I would build a dictionary (one time only then reuse it for future calls) like the following:
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(); }
}
<小时>
对这里发生的事情的一些解释:
Some explanations of what is going on here:
第一:
[Accepts(5)]
这一行是一个 C# 属性(由 AcceptsAttribute
定义)表示 FooMessage
类接受 5 的消息 id.
This line is a C# attribute (defined by AcceptsAttribute
) says the the FooMessage
class accepts the message id of 5.
第二:
是的,字典是在运行时通过反射构建的.您只需执行一次(我会将其放入一个单例类中,您可以在其上放置一个测试用例,以确保字典正确构建).
Yes the dictionary is being built at runtime via reflection. You need only to do this once (I would put it into a singleton class that you can put a test case on it that can be run to ensure that the dictionary builds correctly).
第三:
var m = messages[5]();
这一行从字典中获取以下已编译的 lambda 表达式并执行它:
This line gets the following compiled lambda expression out of the dictionary and executes it:
()=>(Message)new FooMessage();
(强制转换在 .NET 3.5 中是必要的,但在 4.0 中由于 delegates 工作方式的协变变化,在 4.0 中可以将类型为 Func
的对象分配给类型 Func
.)
(The cast is necessary in .NET 3.5 but not in 4.0 due to the covariant changes in how delagates work, in 4.0 an object of type Func<FooMessage>
can be assigned to an object of the type Func<Message>
.)
这个 lambda 表达式是在字典创建期间由赋值行构建的:
This lambda expression is built by the Value assignment line during dictionary creation:
Value = (Func<Message>)Expression.Lambda(Expression.Convert(Expression.New(t), typeof(Message))).Compile()
(这里的转换是将编译后的 lambda 表达式转换为 Func
所必需的.)
(The cast here is necessary to cast the compiled lambda expression to Func<Message>
.)
我这样做是因为那时我碰巧已经有了可用的类型.你也可以使用:
I did that this way because I happen to already have the type available to me at that point. You could also use:
Value = ()=>(Message)Activator.CreateInstance(t)
但我相信这会更慢(并且这里的演员表对于将 Func<object>
更改为 Func
But I believe that would be slower (and the cast here is necessary to change Func<object>
into Func<Message>
).
第四:
.SelectMany(o => o.Keys.Select(key => new { Key = key, o.Value }))
这样做是因为我觉得在一个类上多次放置 AcceptsAttribute
可能有价值(每个类接受多个消息 id).这也有一个很好的副作用,即忽略没有消息 id 属性的消息类(否则 Where 方法需要确定属性是否存在的复杂性).
This was done because I felt that you might have value in placing the AcceptsAttribute
more than once on a class(to accept more than one message id per class). This also has the nice side affect of ignoring message classes that do not have a message id attribute (otherwise the Where method would need to have the complexity of determining if the attribute is present).
这篇关于串行数据的二进制通信协议解析器设计的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持编程学习网!