串行数据的二进制通信协议解析器设计
我正在重新审视字节流(串行数据,一次接收 1 个字节)的通信协议解析器设计。
数据包结构(无法更改)是:
|| Start Delimiter (1 byte) | Message ID (1 byte) | Length (1 byte) | Payload (n bytes) | Checksum (1 byte) ||
过去我以过程状态机方法实现了此类系统。当数据的每个字节到达时,状态机被驱动以查看传入数据在哪里/是否适合一次一个字节地进入有效数据包,并且一旦组装了整个数据包,基于消息 ID 的 switch 语句就会执行消息的适当处理程序。在一些实现中,解析器/状态机/消息处理程序循环位于其自己的线程中,以便不给串行数据接收事件处理程序带来负担,并且由指示字节已被读取的信号量触发。
我想知道是否有一个更优雅的解决方案来解决这个常见问题,利用 C# 和 OO 设计的一些更现代的语言功能。有什么设计模式可以解决这个问题吗?事件驱动、轮询、组合?
我有兴趣听听你的想法。谢谢。
普雷姆博。
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) ||
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.
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.
Prembo.
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(3)
首先,我将数据包解析器与数据流读取器分开(这样我就可以编写测试而不处理流)。然后考虑一个基类,它提供了一种读取数据包的方法和一种写入数据包的方法。
另外,我会构建一个字典(仅一次,然后在将来的调用中重用它),如下所示:
编辑:对此处发生的情况的一些解释:
第一:
此行是一个 C# 属性(由
AcceptsAttribute
) 表示 FooMessage 类接受消息 id 5。第二:
是的,字典是在运行时通过反射构建的。您只需要执行一次(我会将其放入一个单例类中,您可以在其上放置一个测试用例,该测试用例可以运行以确保字典正确构建)。
第三:
此行从字典中获取以下已编译的 lambda 表达式并执行它:(
在 .NET 3.5 中强制转换是必要的,但在 4.0 中则不需要,因为在 4.0 中,delagates 的工作方式发生了协变变化,在 4.0 中是
类型的对象Func
可以分配给Func
类型的对象。)此 lambda 表达式是在字典创建期间由 Value 赋值行构建的:(
此处的强制转换是必要的将已编译的 lambda 表达式强制转换为
Func
。)我这样做是因为此时我碰巧已经有了可用的类型。您也可以使用:
但我相信这会更慢(并且此处的转换是将
Func
第四:
这样做是因为我觉得在一个类上多次放置
AcceptsAttribute
可能会有价值(以便每个类接受多个消息 ID)。这还有一个很好的副作用,即忽略没有消息 id 属性的消息类(否则Where 方法需要具有确定该属性是否存在的复杂性)。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:
Edit: Some explanations of what is going on here:
First:
This line is a C# attribute (defined by
AcceptsAttribute
) says the theFooMessage
class accepts the message id of 5.Second:
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).
Third:
This line gets the following compiled lambda expression out of the dictionary and executes it:
(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 typeFunc<Message>
.)This lambda expression is built by the Value assignment line during dictionary creation:
(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:
But I believe that would be slower (and the cast here is necessary to change
Func<object>
intoFunc<Message>
).Fourth:
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).我来晚了一点,但我写了一个我认为可以做到这一点的框架。如果不了解更多关于您的协议的信息,我很难编写对象模型,但我认为这不会太难。看看 binaryserializer.com。
I'm a little late to the party but I wrote a framework that I think could do this. Without knowing more about your protocol, it's hard for me to write the object model but I would think it wouldn't be too hard. Take a look at binaryserializer.com.
我通常做的是定义一个抽象基消息类并从该类派生密封消息。然后有一个包含状态机的消息解析器对象来解释字节并构建适当的消息对象。消息解析器对象只有一个方法(向其传递传入字节)和一个可选的事件(当完整消息到达时调用)。
然后,您有两个选项来处理实际消息:
as
转换为派生类型,而不是 switch 语句。这两个选项在不同的场景中都有用。
What I generally do is define an abstract base message class and derive sealed messages from that class. Then have a message parser object that contains the state machine to interpret the bytes and build an appropriate message object. The message parser object just has a method (to pass it the incoming bytes) and optionally an event (invoked when a full message has arrived).
You then have two options for handling the actual messages:
as
-casts them to the derived types.Both of those options are useful in different scenarios.