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

时间:2010-07-19 03:07:22

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

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

数据包结构(不能更改)是:

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

过去,我已经采用程序状态机方法实现了这样的系统。当每个数据字节到达时,状态机被驱动以查看输入数据一次/一个字节是否适合有效数据包,并且一旦整个数据包被组装,基于消息ID的switch语句执行适当的消息处理程序。在一些实现中,解析器/状态机/消息处理程序循环位于其自己的线程中,以便不对负载串行数据接收的事件处理程序负责,并且由指示字节已被读取的信号量触发。

我想知道是否有一个更优雅的解决方案来解决这个常见问题,利用C#和OO设计的一些更现代的语言功能。任何可以解决这个问题的设计模式?事件驱动vs polled vs combination?

我很想听听你的想法。感谢。

Prembo。

3 个答案:

答案 0 :(得分:4)

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

此外,我将构建一个字典(一次只能重复使用,以便将来调用),如下所示:

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

(由于delagates如何工作的协变性变化,在.NET 3.5中必须使用强制转换,但在4.0版本中不需要强制转换,在4.0中,类型为Func<FooMessage>的对象可以分配给{{1}类型的对象}。)

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

Func<Message>

(这里需要使用强制转换将已编译的lambda表达式转换为Value = (Func<Message>)Expression.Lambda(Expression.Convert(Expression.New(t), typeof(Message))).Compile() 。)

我是这样做的,因为我恰好已经拥有了那种可用的类型。你也可以使用:

Func<Message>

但我相信这会慢一点(此处需要将Value = ()=>(Message)Activator.CreateInstance(t) 更改为Func<object>)。

第四:

Func<Message>

这样做是因为我觉得你可能有必要将.SelectMany(o => o.Keys.Select(key => new { Key = key, o.Value })) 多次放在一个类上(每个类接受多个消息id)。这也有忽略没有消息id属性的消息类的好的副作用(否则Where方法需要具有确定属性是否存在的复杂性)。

答案 1 :(得分:2)

我参加派对有点晚了,但我写了一个框架,我认为可以做到这一点。在不了解您的协议的情况下,我很难编写对象模型,但我认为这不会太难。看看binaryserializer.com

答案 2 :(得分:1)

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

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

  • 在基本消息类上定义一个抽象方法,在每个派生消息类中重写它。消息解析器在消息完全到达后调用此方法。
  • 第二个选项不太面向对象,但可能更容易使用:将消息类保留为数据。消息完成后,通过将抽象基类消息类作为参数的事件将其发送出去。处理程序通常as - 而不是switch语句 - 将它们强制转换为派生类型。

这两个选项在不同情况下都很有用。