2

我正在从包含一系列可变长度描述符的字节流中读取,这些描述符在我的代码中表示为各种结构/类。每个描述符都有一个与所有其他描述符相同的固定长度标头,用于标识其类型。

是否有合适的模型或模式可以用来最好地解析和表示每个描述符,然后根据它的类型执行适当的操作?

4

6 回答 6

9

我已经写了很多这种类型的解析器。

我建议您阅读固定长度标头,然后使用简单的 switch-case 将正确的构造函数分派给您的结构,将固定标头和流传递给该构造函数,以便它可以使用流的可变部分。

于 2009-10-23T11:51:37.793 回答
2

这是文件解析中的常见问题。通常,您读取描述符的已知部分(幸运的是,在这种情况下它是固定长度的,但并非总是如此),并将其分支到那里。通常我在这里使用策略模式,因为我通常希望系统具有广泛的灵活性——但直接开关或工厂也可以工作。

另一个问题是:您是否控制并信任下游代码?含义:工厂/战略实施?如果你这样做了,那么你可以只给他们流和你期望他们消耗的字节数(也许放置一些调试断言,以验证它们确实读取了正确的数量)。

如果您不能信任工厂/策略实现(也许您允许用户代码使用自定义反序列化器),那么我将在流之上构建一个包装器(例如:SubStream来自 protobuf-net),它只允许预期的要消耗的字节数(之后报告 EOF),并且不允许在此块之外进行查找/等操作。我还会进行运行时检查(即使在发布版本中)是否已经消耗了足够的数据 - 但在这种情况下,我可能会读取任何未读数据 - 即,如果我们预计下游代码消耗 20 个字节,但它只读取 12 ,然后跳过下一个 8 并读取我们的下一个描述符。

对此进行扩展;这里的一种策略设计可能类似于:

interface ISerializer {
    object Deserialize(Stream source, int bytes);
    void Serialize(Stream destination, object value);
}

您可以为每个预期标记构建此类序列化程序的字典(如果数量很少,则仅列出一个列表),并解析您的序列化程序,然后调用该Deserialize方法。如果您不认识标记,则(其中之一):

  • 跳过给定的字节数
  • 抛出错误
  • 将额外的字节存储在某处的缓冲区中(允许意外数据的往返)

作为上述的旁注 - 如果系统是在运行时确定的,无论是通过反射还是通过运行时 DSL(等),这种方法(策略)是有用的。如果系统在编译时是完全可预测的(因为它没有改变,或者因为您正在使用代码生成),那么直接的switch方法可能更合适 - 您可能不需要任何额外的接口,因为您可以直接注入相应的代码。

于 2009-11-01T20:49:46.693 回答
2

要记住的关键一件事是,如果您正在从流中读取并且没有检测到有效的标头/消息,请在重试之前只丢弃第一个字节。很多时候,我看到整个数据包或消息被丢弃,这可能导致有效数据丢失。

于 2009-11-03T16:56:18.630 回答
1

This sounds like it might be a job for the Factory Method or perhaps Abstract Factory. Based on the header you choose which factory method to call, and that returns an object of the relevant type.

Whether this is better than simply adding constructors to a switch statement depends on the complexity and the uniformity of the objects you're creating.

于 2009-10-31T02:57:30.787 回答
0

如果您希望它是好的 OO,您可以在对象层次结构中使用访问者模式。我是如何做到的(用于识别从网络捕获的数据包,几乎与您可能需要的相同):

  • 巨大的对象层次结构,只有一个父类

  • 每个类都有一个向其父类注册的静态构造函数,因此父类知道其直接子类(这是 C++,我认为在具有良好反射支持的语言中不需要此步骤)

  • 每个类都有一个静态构造方法来获取字节流的剩余部分,并基于此决定是否由他负责处理该数据

  • 当一个数据包进来时,我只是将它传递给主父类(称为 Packet)的静态构造方法,该方法反过来检查其所有子类是否有责任处理该数据包,并且递归地进行,直到一个层次结构底部的类返回实例化的类。

  • 每个静态“构造函数”方法都从字节流中切出自己的标头,并仅将有效负载传递给其子级。

这种方法的好处是您可以在对象层次结构中的任何位置添加新类型,而无需查看/更改任何其他类。它对数据包非常好用。它是这样的:

  • 以太网包
  • IP包
  • UDPPacket、TCPPacket、ICMPPacket
  • ...

我希望你能看到这个想法。

于 2009-11-05T14:44:41.473 回答
0

我会建议:

fifo = FIFO.new

而(fd 可读){
  从 fd 中读取所有内容并将其粘贴到 fifo
  if (fifo 的前面有一个有效的头部并且
      fifo 对于有效载荷足够大){

      调度构造函数,从 fifo 中删除字节
  }
}

使用这种方法:

  • 您可以对不良有效负载进行一些错误检查,并可能丢弃不良数据
  • 数据未在 fd 的读取缓冲区上等待(对于大型有效负载可能是一个问题)
于 2009-11-03T07:08:37.247 回答