串行数据的二进制通信协议解析器设计

发布于 2024-09-10 13:30:30 字数 524 浏览 1 评论 0原文

我正在重新审视字节流(串行数据,一次接收 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 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(3

你げ笑在眉眼 2024-09-17 13:30:30

首先,我将数据包解析器与数据流读取器分开(这样我就可以编写测试而不处理流)。然后考虑一个基类,它提供了一种读取数据包的方法和一种写入数据包的方法。

另外,我会构建一个字典(仅一次,然后在将来的调用中重用它),如下所示:

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 类接受消息 id 5。

第二:

是的,字典是在运行时通过反射构建的。您只需要执行一次(我会将其放入一个单例类中,您可以在其上放置一个测试用例,该测试用例可以运行以确保字典正确构建)。

第三:

var m = messages[5]();

此行从字典中获取以下已编译的 lambda 表达式并执行它:(

()=>(Message)new FooMessage();

在 .NET 3.5 中强制转换是必要的,但在 4.0 中则不需要,因为在 4.0 中,delagates 的工作方式发生了协变变化,在 4.0 中是 类型的对象Func 可以分配给 Func 类型的对象。)

此 lambda 表达式是在字典创建期间由 Value 赋值行构建的:(

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

此处的强制转换是必要的将已编译的 lambda 表达式强制转换为 Func。)

我这样做是因为此时我碰巧已经有了可用的类型。您也可以使用:

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

但我相信这会更慢(并且此处的转换是将 Func更改为 Func 所必需的)。

第四:

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

这样做是因为我觉得在一个类上多次放置 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:

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(); }
}

Edit: Some explanations of what is going on here:

First:

[Accepts(5)]

This line is a C# attribute (defined by AcceptsAttribute) says the the FooMessage 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:

var m = messages[5]();

This line gets the following compiled lambda expression out of the dictionary and executes it:

()=>(Message)new FooMessage();

(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>.)

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()

(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)

But I believe that would be slower (and the cast here is necessary to change Func<object> into Func<Message>).

Fourth:

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

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).

书间行客 2024-09-17 13:30:30

我来晚了一点,但我写了一个我认为可以做到这一点的框架。如果不了解更多关于您的协议的信息,我很难编写对象模型,但我认为这不会太难。看看 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.

短暂陪伴 2024-09-17 13:30:30

我通常做的是定义一个抽象基消息类并从该类派生密封消息。然后有一个包含状态机的消息解析器对象来解释字节并构建适当的消息对象。消息解析器对象只有一个方法(向其传递传入字节)和一个可选的事件(当完整消息到达时调用)。

然后,您有两个选项来处理实际消息:

  • 在基本消息类上定义一个抽象方法,并在每个派生消息类中重写它。让消息解析器在消息完全到达后调用此方法。
  • 第二个选项不太面向对象,但可能更容易使用:将消息类保留为数据。消息完成后,通过一个以抽象消息基类作为参数的事件将其发送出去。处理程序通常将它们 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:

  • Define an abstract method on the base message class, overriding it in each of the derived message classes. Have the message parser invoke this method after the message has arrived completely.
  • The second option is less object-oriented, but may be easier to work with: leave the message classes as just data. When the message is complete, send it out via an event that takes the abstract base message class as its parameter. Instead of a switch statement, the handler usually as-casts them to the derived types.

Both of those options are useful in different scenarios.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文