2

背景:

我发现自己经常使用 F# Records。目前我正在研究一个专有二进制协议(一种设计非常奇怪的协议......)的数据包解析和重放项目。

我们为数据包定义骨架记录。

type bytes = byte array
type packetSkeleton = {
    part1 : bytes
    part2 : bytes 
    ... }

现在,很容易使用它来“剖析”我们的数据包(实际上只是给字节字段命名)。

let dissect (raw : bytes) =
  let slice a b = raw.[a..b]
  { part1 = slice 0 4
    part2 = slice 4 5
    ... }

即使对于较长的数据包,这也很有效,如果切片有可预测的模式,我们甚至可以使用一些简洁的递归函数。

所以我剖析了数据包,提取了我需要的字段,并使用我从剖析中获取的字段创建了一个基于 packetSkeleton 的数据包,现在看起来有点尴尬:

let createAuthStub a b c d e f g h ... =
   { part1 = a; part2 = b
     part3 = d; ...
   }

然后,在创建填充的存根之后,我需要将其反序列化为可以放在网络上的表单:

(* packetSkeleton -> byte array *)
let deserialise (packet : packetSkeleton) =
  [| packet.part1; packet.part2; ... |]

let xab = dissect buf
let authStub = createAuthStub xab.part1 1 2 xab.part9 ...

deserialise authStub |> send

所以最终我有 3 个区域,记录类型,为给定数据包创建记录,以及反序列化的字节数组。有些事情告诉我,就代码清晰度而言,这对我来说是一个糟糕的设计选择,即使在这个早期阶段,我也已经感觉到它开始让我感到震惊。

问题:

a) 我是否为这样的项目使用了正确的数据类型?我的方法正确吗?
b)我是否应该放弃尝试让这段代码感觉干净?

因为我有点通过触摸来编码这个,我会很感激一些见解!

PS 我意识到这个问题非常适合 C,但 F# 更有趣(另外,稍后对解剖器的验证听起来很吸引人)!

4

2 回答 2

2

如果一个数据包可能相当大,packetSkeleton可能会变得笨拙。另一种选择是使用原始字节并定义一个读取/写入每个部分的模块。

module Packet
  let Length = 42
  let GetPart1 src = src.[0..3]
  let SetPart1 src dst = Array.blit src 0 dst 0 4
  let GetPart2 src = src.[4..5]
  let SetPart2 src dst = Array.blit src 0 dst 4 2
  ...

open Packet 

let createAuthStub bytes b c =
  let resp = Array.zeroCreate Packet.Length
  SetPart1 (GetPart1 bytes) 
  SetPart2 b resp
  SetPart3 c resp
  SetPart4 (GetPart9 bytes) 
  resp

这消除了对反序列化功能的需求(并且可能有助于提高性能)。

编辑

创建包装器类型是另一种选择

type Packet(bytes: byte[]) =
  new() = Packet(Array.zeroCreate Packet.Length)
  static member Length = 42
  member x.Part1
    with get() = bytes.[0..3]
    and set value = Array.blit value 0 bytes 0 4
    ...

这可能会减少一些代码:

let createAuthStub (req: Packet) b c =
  let resp = Packet()
  resp.Part1 <- req.Part1
  resp.Part2 <- b
  resp.Part3 <- c
  resp.Part4 <- req.Part9
  resp
于 2012-10-04T02:57:00.137 回答
1

我认为您的方法本质上是合理的-但是,当然,如果不了解更多细节就很难说。

我认为代码中显示的一个关键思想是功能架构的关键,即类型(用于建模问题域)和创建域模型值、处理它并格式化它们的处理功能之间的分离。

在你的情况下:

  • 问题域的类型bytespacketSkeleton模型
  • 该函数处理您的域(我同意丹尼尔的观点,如果将整体作为参数createAuthStub,它可能更具可读性)packetSkeleton
  • 该函数deserialize将您的域转换回字节

我认为这种结构化代码的方式非常好,因为它分离了程序的不同关注点。我什至写了一篇文章,试图将其描述为一种更通用的编程方法

于 2012-10-04T02:33:35.120 回答