c# - 串行数据的二进制通信协议(protocol)解析器设计

标签 c# design-patterns serial-port protocols communication

我正在重新审视字节流(串行数据,一次接收 1 个字节)的通信协议(protocol)解析器设计。

数据包结构(无法更改)是:

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

过去,我以过程状态机方法实现了此类系统。随着数据的每个字节到达,状态机被驱动以一次一个字节地查看传入数据在哪里/是否适合有效数据包,并且一旦整个数据包被组装好,基于消息 ID 的 switch 语句执行消息的适当处理程序。在某些实现中,解析器/状态机/消息处理程序循环位于其自己的线程中,以免给串行数据接收事件处理程序增加负担,并由指示已读取字节的信号量触发。

我想知道是否有更优雅的解决方案来解决这个常见问题,利用 C# 和 OO 设计的一些更现代的语言功能。有什么设计模式可以解决这个问题吗?事件驱动、轮询还是组合?

我很想听听您的想法。谢谢。

普兰博。

最佳答案

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

此外,我会像下面这样构建一个字典(只有一次,然后才能在以后的调用中重用它):

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 的消息 ID。

第二个:

是的,字典是在运行时通过反射构建的。你只需要这样做一次(我会把它放到一个单例类中,你可以在它上面放一个可以运行的测试用例,以确保字典正确构建)。

第三:

var m = messages[5]();

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

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

(强制转换在 .NET 3.5 中是必需的,但在 4.0 中不是必需的,因为在 4.0 中,类型为 Func<FooMessage> 的对象可以分配给类型为 Func<Message> 的对象。)

这个 lambda 表达式是在字典创建期间由赋值行构建的:

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

(此处的转换是将编译后的 lambda 表达式转换为 Func<Message> 所必需的。)

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

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

但我相信那样会更慢(这里的转换对于将 Func<object> 更改为 Func<Message> 是必要的)。

第四:

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

这样做是因为我觉得您将 AcceptsAttribute 放置可能有值(value)在一个类(class)上不止一次(每个类(class)接受多个消息 ID)。这还有一个很好的副作用,即忽略没有消息 id 属性的消息类(否则 Where 方法需要具有确定属性是否存在的复杂性)。

关于c# - 串行数据的二进制通信协议(protocol)解析器设计,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/3278227/

相关文章:

python - Windows 或 Ubuntu VBox 上的串行端口从 Python 与 Arduino 对话

c++ - 打开 COM 端口时调试断言失败

c# - "16-bit ms-dos subsystem"错误

c# - MVC4 TDD - System.ArgumentNullException : Value cannot be null.

c# - 如何使用WPF绘制二叉 TreeView ?

java - 从子类设置父类(super class)中字段的类型 (Java)

c# - 如何从枚举中选择随机值?

ruby - 如何使用 Ruby 中的单例设计模式创建类?

java - 字符串,重复,但在Java中不从头开始(菱形模式

Java SerialPort 写入使 cpu 使用率上升至 100%