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

Pre*_*mbo 7 c# design-patterns serial-port communication protocols

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

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

|| Start Delimiter (1 byte) | Message ID (1 byte) | Length (1 byte) | Payload (n bytes) | Checksum (1 byte) ||
Run Code Online (Sandbox Code Playgroud)

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

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

我很想听听你的想法.谢谢.

Prembo.

Bil*_*rry 5

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

另外,我会像下面这样构建一个字典(只有一次,然后将其重用于以后的调用):

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(); }
}
Run Code Online (Sandbox Code Playgroud)

编辑:这里发生了什么的一些解释:

第一:

[Accepts(5)]
Run Code Online (Sandbox Code Playgroud)

此行是C#属性(由定义AcceptsAttribute),表示FooMessage该类接受消息ID 5。

第二:

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

第三:

var m = messages[5]();
Run Code Online (Sandbox Code Playgroud)

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

()=>(Message)new FooMessage();
Run Code Online (Sandbox Code Playgroud)

(在.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()
Run Code Online (Sandbox Code Playgroud)

(此处的强制转换对于将已编译的lambda表达式强制转换为而言是必要的Func<Message>。)

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

Value = ()=>(Message)Activator.CreateInstance(t)
Run Code Online (Sandbox Code Playgroud)

但我认为这样做会比较慢(并且必须将此处的演员表更改Func<object>Func<Message>)。

第四:

.SelectMany(o => o.Keys.Select(key => new { Key = key, o.Value }))
Run Code Online (Sandbox Code Playgroud)

这样做是因为我觉得您可能有很多价值,可以将一个AcceptsAttribute以上的内容放置在一个类上(每个类接受一个以上的消息ID)。这也具有忽略没有消息id属性的消息类的好处(否则,Where方法将需要确定该属性是否存在的复杂性)。